Skip to content

Commit 532a3c6

Browse files
committed
fix: make sure the backup downloading is stream to allow bigger workspaces | refactor: cleanup of fs / path imports
1 parent 1fec1a5 commit 532a3c6

File tree

14 files changed

+154
-100
lines changed

14 files changed

+154
-100
lines changed

packages/cli/src/commands/analyze.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { mkdir } from 'fs/promises';
2-
import { writeFileSync, existsSync, mkdirSync } from 'fs';
3-
import { dirname, join } from 'path';
1+
import { mkdir, writeFile, access } from 'node:fs/promises';
2+
import { dirname, join } from 'node:path';
43
import { log, outro, intro, spinner } from '@clack/prompts';
54
import { metaApiGet, replacePlaceholders, sanitizeFileName } from '@calycode/utils';
65
import {
@@ -12,7 +11,6 @@ import {
1211
import { resolveConfigs } from '../utils/commands/context-resolution';
1312
import { findProjectRoot } from '../utils/commands/project-root-finder';
1413

15-
// [ ] CORE, but needs fs access.
1614
async function fetchFunctionsInXanoScript(instance, workspace, branch, printOutput = false, core) {
1715
intro('Starting to analyze functions.');
1816
let branchFunctions = {};
@@ -81,12 +79,14 @@ async function fetchFunctionsInXanoScript(instance, workspace, branch, printOutp
8179

8280
// Ensure the parent directory exists
8381
const parentDir = dirname(filePath);
84-
if (!existsSync(parentDir)) {
85-
mkdirSync(parentDir, { recursive: true });
82+
try {
83+
await access(parentDir);
84+
} catch {
85+
await mkdir(parentDir, { recursive: true });
8686
}
8787

8888
// Write the file
89-
writeFileSync(filePath, branchFunctions[item].script);
89+
await writeFile(filePath, branchFunctions[item].script);
9090
}
9191
s.stop(`Xano Script files are ready -> ${outputDir}`);
9292
} catch (err) {

packages/cli/src/commands/backups.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import path, { join } from 'path';
2-
import { readdirSync } from 'fs';
1+
import { basename, join } from 'node:path';
2+
import { readdir } from 'node:fs/promises';
33
import { openAsBlob } from 'node:fs';
44
import { select, confirm, outro } from '@clack/prompts';
55
import { replacePlaceholders } from '@calycode/utils';
@@ -48,7 +48,7 @@ async function restorationWizard({ instance, workspace, sourceBackup, forceConfi
4848

4949
let availableBackups;
5050
try {
51-
availableBackups = readdirSync(backupsDir);
51+
availableBackups = await readdir(backupsDir);
5252
} catch {
5353
outro(`No backups directory found for branch "${branchConfig.label}".`);
5454
process.exit(1);
@@ -82,7 +82,7 @@ async function restorationWizard({ instance, workspace, sourceBackup, forceConfi
8282
}
8383

8484
const formData = new FormData();
85-
formData.append('file', await openAsBlob(backupFilePath), path.basename(backupFilePath));
85+
formData.append('file', await openAsBlob(backupFilePath), basename(backupFilePath));
8686
formData.append('password', '');
8787

8888
// Pass on the formdata to the core implementation

packages/cli/src/commands/generate-code.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { log, outro, intro, spinner } from '@clack/prompts';
2-
import { metaApiGet, normalizeApiGroupName, replacePlaceholders, dirname } from '@calycode/utils';
2+
import { metaApiGet, normalizeApiGroupName, replacePlaceholders } from '@calycode/utils';
33
import {
44
addApiGroupOptions,
55
addFullContextOptions,

packages/cli/src/commands/generate-repo.ts

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import { existsSync, readdirSync, lstatSync, rmdirSync, unlinkSync } from 'fs';
1+
import { mkdir, access, readdir, lstat, rm, unlink } from 'node:fs/promises';
22
import { log, intro, outro } from '@clack/prompts';
33
import { load } from 'js-yaml';
4-
import { mkdir } from 'fs/promises';
54
import { joinPath, dirname, replacePlaceholders, fetchAndExtractYaml } from '@calycode/utils';
65
import {
76
addFullContextOptions,
@@ -14,21 +13,30 @@ import { resolveConfigs } from '../utils/commands/context-resolution';
1413
import { findProjectRoot } from '../utils/commands/project-root-finder';
1514

1615
/**
17-
* Clears the contents of a directory.
16+
* Recursively removes all files and subdirectories in a directory.
1817
* @param {string} directory - The directory to clear.
1918
*/
20-
function clearDirectory(directory) {
21-
if (existsSync(directory)) {
22-
readdirSync(directory).forEach((file) => {
19+
async function clearDirectory(directory: string): Promise<void> {
20+
try {
21+
await access(directory);
22+
} catch {
23+
// Directory does not exist; nothing to clear
24+
return;
25+
}
26+
27+
const files = await readdir(directory);
28+
await Promise.all(
29+
files.map(async (file) => {
2330
const curPath = joinPath(directory, file);
24-
if (lstatSync(curPath).isDirectory()) {
25-
clearDirectory(curPath);
26-
rmdirSync(curPath);
31+
const stat = await lstat(curPath);
32+
if (stat.isDirectory()) {
33+
await clearDirectory(curPath);
34+
await rm(curPath, { recursive: true, force: true }); // removes the (now-empty) dir
2735
} else {
28-
unlinkSync(curPath);
36+
await unlink(curPath);
2937
}
30-
});
31-
}
38+
})
39+
);
3240
}
3341

3442
async function generateRepo({

packages/cli/src/commands/generate-xanoscript-repo.ts

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { existsSync, readdirSync, lstatSync, rmdirSync, unlinkSync, mkdirSync } from 'fs';
1+
import { access, readdir, lstat, rm, unlink, mkdir } from 'node:fs/promises';
22
import { joinPath, dirname } from '@calycode/utils';
33
import { attachCliEventHandlers } from '../utils/event-listener';
44
import { replacePlaceholders } from '@calycode/utils';
@@ -8,21 +8,30 @@ import { resolveConfigs } from '../utils/commands/context-resolution';
88
import { findProjectRoot } from '../utils/commands/project-root-finder';
99

1010
/**
11-
* Clears the contents of a directory.
11+
* Recursively removes all files and subdirectories in a directory.
1212
* @param {string} directory - The directory to clear.
1313
*/
14-
function clearDirectory(directory) {
15-
if (existsSync(directory)) {
16-
readdirSync(directory).forEach((file) => {
14+
async function clearDirectory(directory: string): Promise<void> {
15+
try {
16+
await access(directory);
17+
} catch {
18+
// Directory does not exist; nothing to clear
19+
return;
20+
}
21+
22+
const files = await readdir(directory);
23+
await Promise.all(
24+
files.map(async (file) => {
1725
const curPath = joinPath(directory, file);
18-
if (lstatSync(curPath).isDirectory()) {
19-
clearDirectory(curPath);
20-
rmdirSync(curPath);
26+
const stat = await lstat(curPath);
27+
if (stat.isDirectory()) {
28+
await clearDirectory(curPath);
29+
await rm(curPath, { recursive: true, force: true });
2130
} else {
22-
unlinkSync(curPath);
31+
await unlink(curPath);
2332
}
24-
});
25-
}
33+
})
34+
);
2635
}
2736

2837
async function generateXanoscriptRepo({ instance, workspace, branch, core, printOutput = false }) {
@@ -46,7 +55,7 @@ async function generateXanoscriptRepo({ instance, workspace, branch, core, print
4655
});
4756

4857
clearDirectory(outputDir);
49-
mkdirSync(outputDir, { recursive: true });
58+
await mkdir(outputDir, { recursive: true });
5059

5160
const plannedWrites: { path: string; content: string }[] = await core.buildXanoscriptRepo({
5261
instance: context.instance,

packages/cli/src/commands/run-tests.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import fs from 'fs/promises';
1+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
22
import { intro, log, spinner } from '@clack/prompts';
33
import { normalizeApiGroupName, replacePlaceholders } from '@calycode/utils';
44
import {
@@ -52,7 +52,7 @@ async function runTest({
5252

5353
// Take the core implementation for test running:
5454
// for now testconfig has to exist on the machine prior to running the tests.
55-
const testConfigFileContent = await fs.readFile(testConfigPath, { encoding: 'utf-8' });
55+
const testConfigFileContent = await readFile(testConfigPath, { encoding: 'utf-8' });
5656
const testConfig = JSON.parse(testConfigFileContent);
5757
const s = spinner();
5858
s.start('Running tests based on the provided spec');
@@ -81,8 +81,8 @@ async function runTest({
8181
api_group_normalized_name: normalizeApiGroupName(outcome.group.name),
8282
});
8383

84-
await fs.mkdir(apiGroupTestPath, { recursive: true });
85-
await fs.writeFile(
84+
await mkdir(apiGroupTestPath, { recursive: true });
85+
await writeFile(
8686
`${apiGroupTestPath}/${testFileName}`,
8787
JSON.stringify(outcome.results, null, 2)
8888
);

packages/cli/src/commands/serve.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { spawn } from 'child_process';
1+
import { spawn } from 'node:child_process';
22
import { normalizeApiGroupName, replacePlaceholders } from '@calycode/utils';
33
import { addApiGroupOptions, addFullContextOptions, chooseApiGroupOrAll } from '../utils/index';
44
import { resolveConfigs } from '../utils/commands/context-resolution';

packages/cli/src/features/code-gen/open-api-generator.ts

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
1-
import { spawn } from 'child_process';
2-
import { resolve, join } from 'path';
3-
import { mkdirSync, createWriteStream } from 'fs';
1+
import { createWriteStream } from 'node:fs';
2+
import { mkdir } from 'node:fs/promises';
3+
import { resolve, join } from 'node:path';
4+
import { spawn } from 'node:child_process';
45

5-
// [ ] CLI only feature
6-
export function runOpenApiGenerator({
6+
export async function runOpenApiGenerator({
77
input,
88
output,
99
generator,
1010
additionalArgs = [],
11-
logger = false, // If true, log to file, else discard logs
11+
logger = false,
1212
}) {
13-
return new Promise((resolvePromise, reject) => {
1413
// Always use npx and the official package
1514
const cliBin = process.platform === 'win32' ? 'npx.cmd' : 'npx';
1615
const inputPath = resolve(input).replace(/\\/g, '/');
@@ -31,23 +30,22 @@ export function runOpenApiGenerator({
3130
let logStream = null;
3231
let logPath = null;
3332

34-
// If logger is true, prepare log file and stream
3533
if (logger) {
3634
const logsDir = join(process.cwd(), 'output', '_logs');
37-
mkdirSync(logsDir, { recursive: true });
35+
await mkdir(logsDir, { recursive: true });
3836
logPath = join(logsDir, `openapi-generator-${Date.now()}.log`);
3937
logStream = createWriteStream(logPath);
4038
}
4139

42-
// Start the process
40+
return new Promise((resolvePromise, reject) => {
4341
const proc = spawn(cliBin, cliArgs, { shell: true, stdio: ['ignore', 'pipe', 'pipe'] });
4442

4543
// Always suppress console output!
4644
if (logger && logStream) {
4745
proc.stdout.pipe(logStream);
4846
proc.stderr.pipe(logStream);
4947
} else {
50-
proc.stdout.resume(); // prevent backpressure
48+
proc.stdout.resume();
5149
proc.stderr.resume();
5250
}
5351

@@ -75,3 +73,4 @@ export function runOpenApiGenerator({
7573
});
7674
});
7775
}
76+

packages/cli/src/node-config-storage.ts

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@
66
* - ~/.xano-tools/config.json (global configuration)
77
* - ~/.xano-tools/tokens/ (API tokens with restricted permissions)
88
*/
9-
import fs from 'fs';
10-
import path from 'path';
11-
import os from 'os';
9+
import fs from 'node:fs';
10+
import path from 'node:path';
11+
import { tmpdir, homedir } from 'node:os';
12+
import { join } from 'node:path';
13+
import { Readable } from 'node:stream';
1214
import { x } from 'tar';
13-
import { tmpdir } from 'os';
14-
import { join } from 'path';
1515
import { ConfigStorage, InstanceConfig } from '@calycode/types';
1616

17-
const BASE_DIR = path.join(os.homedir(), '.xano-tools');
17+
const BASE_DIR = path.join(homedir(), '.xano-tools');
1818
const GLOBAL_CONFIG_PATH = path.join(BASE_DIR, 'config.json');
1919
const TOKENS_DIR = path.join(BASE_DIR, 'tokens');
2020
const DEFAULT_LOCAL_CONFIG_FILE = 'instance.config.json';
@@ -226,7 +226,7 @@ export const nodeConfigStorage: ConfigStorage = {
226226
getStartDir() {
227227
return process.cwd();
228228
},
229-
//
229+
//
230230
// ----- FILESYSTEM OPS -----
231231
async mkdir(dirPath, options) {
232232
await fs.promises.mkdir(dirPath, options);
@@ -237,6 +237,30 @@ export const nodeConfigStorage: ConfigStorage = {
237237
async writeFile(filePath, data) {
238238
await fs.promises.writeFile(filePath, data);
239239
},
240+
async streamToFile(
241+
destinationPath: string,
242+
source: ReadableStream | NodeJS.ReadableStream
243+
): Promise<void> {
244+
const dest = fs.createWriteStream(destinationPath, { mode: 0o600 });
245+
let nodeStream: NodeJS.ReadableStream;
246+
247+
// Convert if necessary
248+
if (typeof (source as any).pipe === 'function') {
249+
// already a NodeJS stream
250+
nodeStream = source as NodeJS.ReadableStream;
251+
} else {
252+
// WHATWG stream (from fetch in Node 18+)
253+
// Can only use fromWeb if available in the environment
254+
nodeStream = Readable.fromWeb(source as any);
255+
}
256+
257+
await new Promise<void>((resolve, reject) => {
258+
nodeStream.pipe(dest);
259+
dest.on('finish', () => resolve());
260+
dest.on('error', (err) => reject(err));
261+
nodeStream.on('error', (err) => reject(err));
262+
});
263+
},
240264
async readFile(filePath) {
241265
return await fs.promises.readFile(filePath); // returns Buffer
242266
},

packages/cli/src/utils/commands/project-root-finder.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1-
import path from 'path';
2-
import fs from 'fs';
1+
import { join, dirname } from 'node:path';
2+
import { access } from 'node:fs/promises';
33

44
async function findProjectRoot(startDir = process.cwd(), sentinel = 'instance.config.json') {
55
let dir = startDir;
6-
while (dir !== path.dirname(dir)) {
7-
if (fs.existsSync(path.join(dir, sentinel))) return dir;
8-
dir = path.dirname(dir);
6+
while (dir !== dirname(dir)) {
7+
try {
8+
await access(join(dir, sentinel));
9+
return dir;
10+
} catch {
11+
dir = dirname(dir);
12+
}
913
}
1014
throw new Error(`Project root not found (missing ${sentinel})`);
1115
}

0 commit comments

Comments
 (0)