@agentuity/cli 1.0.58 → 1.0.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cmd/build/vite/static-render-worker.d.ts +4 -0
- package/dist/cmd/build/vite/static-render-worker.d.ts.map +1 -0
- package/dist/cmd/build/vite/static-render-worker.js +58 -0
- package/dist/cmd/build/vite/static-render-worker.js.map +1 -0
- package/dist/cmd/build/vite/vite-build-worker.d.ts +2 -0
- package/dist/cmd/build/vite/vite-build-worker.d.ts.map +1 -0
- package/dist/cmd/build/vite/vite-build-worker.js +50 -0
- package/dist/cmd/build/vite/vite-build-worker.js.map +1 -0
- package/dist/cmd/build/vite/vite-builder.d.ts +1 -0
- package/dist/cmd/build/vite/vite-builder.d.ts.map +1 -1
- package/dist/cmd/build/vite/vite-builder.js +261 -23
- package/dist/cmd/build/vite/vite-builder.js.map +1 -1
- package/dist/cmd/cloud/deploy-fork.d.ts +10 -0
- package/dist/cmd/cloud/deploy-fork.d.ts.map +1 -1
- package/dist/cmd/cloud/deploy-fork.js +41 -23
- package/dist/cmd/cloud/deploy-fork.js.map +1 -1
- package/dist/cmd/cloud/deploy.d.ts.map +1 -1
- package/dist/cmd/cloud/deploy.js +53 -11
- package/dist/cmd/cloud/deploy.js.map +1 -1
- package/dist/cmd/project/show.d.ts.map +1 -1
- package/dist/cmd/project/show.js +9 -0
- package/dist/cmd/project/show.js.map +1 -1
- package/dist/cmd/support/report.d.ts.map +1 -1
- package/dist/cmd/support/report.js +19 -10
- package/dist/cmd/support/report.js.map +1 -1
- package/dist/steps.d.ts.map +1 -1
- package/dist/steps.js +38 -0
- package/dist/steps.js.map +1 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +19 -9
- package/dist/tui.js.map +1 -1
- package/dist/utils/zip.d.ts.map +1 -1
- package/dist/utils/zip.js +19 -10
- package/dist/utils/zip.js.map +1 -1
- package/package.json +8 -8
- package/src/cmd/build/vite/static-render-worker.ts +72 -0
- package/src/cmd/build/vite/vite-build-worker.ts +58 -0
- package/src/cmd/build/vite/vite-builder.ts +295 -23
- package/src/cmd/cloud/deploy-fork.ts +56 -22
- package/src/cmd/cloud/deploy.ts +68 -12
- package/src/cmd/project/show.ts +9 -0
- package/src/cmd/support/report.ts +21 -10
- package/src/steps.ts +38 -0
- package/src/tui.ts +18 -9
- package/src/utils/zip.ts +22 -10
|
@@ -34,6 +34,16 @@ export interface ForkDeployResult {
|
|
|
34
34
|
success: boolean;
|
|
35
35
|
exitCode: number;
|
|
36
36
|
diagnostics?: ClientDiagnostics;
|
|
37
|
+
/** Deploy result passed back from child process via temp file */
|
|
38
|
+
deployResult?: {
|
|
39
|
+
urls?: {
|
|
40
|
+
deployment: string;
|
|
41
|
+
latest: string;
|
|
42
|
+
custom?: string[];
|
|
43
|
+
dashboard: string;
|
|
44
|
+
};
|
|
45
|
+
logs?: string[];
|
|
46
|
+
};
|
|
37
47
|
}
|
|
38
48
|
|
|
39
49
|
/**
|
|
@@ -77,6 +87,7 @@ export async function runForkedDeploy(options: ForkDeployOptions): Promise<ForkD
|
|
|
77
87
|
const reportFile = join(tmpdir(), `agentuity-deploy-${deploymentId}.json`);
|
|
78
88
|
const cleanLogsFile = join(tmpdir(), `agentuity-deploy-${deploymentId}-logs.txt`);
|
|
79
89
|
const rawLogsFile = join(tmpdir(), `agentuity-deploy-${deploymentId}-raw.txt`);
|
|
90
|
+
const deployResultFile = join(tmpdir(), `agentuity-deploy-${deploymentId}-result.json`);
|
|
80
91
|
const rawLogsWriter = createWriteStream(rawLogsFile);
|
|
81
92
|
let proc: Subprocess | null = null;
|
|
82
93
|
let cancelled = false;
|
|
@@ -152,13 +163,7 @@ export async function runForkedDeploy(options: ForkDeployOptions): Promise<ForkD
|
|
|
152
163
|
process.on('SIGTERM', sigtermHandler);
|
|
153
164
|
|
|
154
165
|
try {
|
|
155
|
-
const childArgs = [
|
|
156
|
-
'agentuity',
|
|
157
|
-
'deploy',
|
|
158
|
-
'--child-mode',
|
|
159
|
-
`--report-file=${reportFile}`,
|
|
160
|
-
...args,
|
|
161
|
-
];
|
|
166
|
+
const childArgs = ['deploy', '--child-mode', `--report-file=${reportFile}`, ...args];
|
|
162
167
|
|
|
163
168
|
// Pass the deployment info via environment variable (same format as CI builds)
|
|
164
169
|
const deploymentEnvValue = JSON.stringify({
|
|
@@ -167,14 +172,19 @@ export async function runForkedDeploy(options: ForkDeployOptions): Promise<ForkD
|
|
|
167
172
|
publicKey: deployment.publicKey,
|
|
168
173
|
});
|
|
169
174
|
|
|
170
|
-
|
|
175
|
+
// Re-exec the same entry point that the parent is running so that
|
|
176
|
+
// local/dev builds test the current code instead of a stale global
|
|
177
|
+
// install. process.execPath is the bun binary; Bun.main is the
|
|
178
|
+
// script entry (e.g. bin/cli.ts or the compiled binary).
|
|
179
|
+
const cmd = [process.execPath, Bun.main, ...childArgs];
|
|
180
|
+
logger.debug('Spawning child deploy process: %s', cmd.join(' '));
|
|
171
181
|
|
|
172
182
|
// Get terminal dimensions to pass to child
|
|
173
183
|
const columns = process.stdout.columns || 80;
|
|
174
184
|
const rows = process.stdout.rows || 24;
|
|
175
185
|
|
|
176
186
|
proc = spawn({
|
|
177
|
-
cmd
|
|
187
|
+
cmd,
|
|
178
188
|
cwd: projectDir,
|
|
179
189
|
env: {
|
|
180
190
|
...process.env,
|
|
@@ -190,6 +200,8 @@ export async function runForkedDeploy(options: ForkDeployOptions): Promise<ForkD
|
|
|
190
200
|
LINES: String(rows),
|
|
191
201
|
// Enable clean log collection for Pulse streaming
|
|
192
202
|
AGENTUITY_CLEAN_LOGS_FILE: cleanLogsFile,
|
|
203
|
+
// Pass result file path for child to write deploy URLs/logs back
|
|
204
|
+
AGENTUITY_DEPLOY_RESULT_FILE: deployResultFile,
|
|
193
205
|
},
|
|
194
206
|
stdin: 'inherit',
|
|
195
207
|
stdout: 'pipe',
|
|
@@ -246,20 +258,34 @@ export async function runForkedDeploy(options: ForkDeployOptions): Promise<ForkD
|
|
|
246
258
|
}
|
|
247
259
|
}
|
|
248
260
|
|
|
261
|
+
// Read deploy result (URLs, logs) from child process
|
|
262
|
+
let deployResult: ForkDeployResult['deployResult'] | undefined;
|
|
263
|
+
if (existsSync(deployResultFile)) {
|
|
264
|
+
try {
|
|
265
|
+
const resultContent = readFileSync(deployResultFile, 'utf-8');
|
|
266
|
+
deployResult = JSON.parse(resultContent);
|
|
267
|
+
unlinkSync(deployResultFile);
|
|
268
|
+
} catch (err) {
|
|
269
|
+
logger.debug('Failed to read deploy result file: %s', err);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
249
273
|
// Stream clean logs to Pulse (prefer clean logs over raw output)
|
|
250
274
|
if (buildLogsStreamURL) {
|
|
251
|
-
let
|
|
275
|
+
let streamedCleanLogs = false;
|
|
252
276
|
if (existsSync(cleanLogsFile)) {
|
|
253
277
|
try {
|
|
254
|
-
|
|
278
|
+
const cleanLogs = Bun.file(cleanLogsFile);
|
|
279
|
+
if (cleanLogs.size > 0) {
|
|
280
|
+
await streamToPulse(buildLogsStreamURL, sdkKey, cleanLogs, logger);
|
|
281
|
+
streamedCleanLogs = true;
|
|
282
|
+
}
|
|
255
283
|
unlinkSync(cleanLogsFile);
|
|
256
284
|
} catch (err) {
|
|
257
|
-
logger.debug('Failed to
|
|
285
|
+
logger.debug('Failed to stream clean logs file: %s', err);
|
|
258
286
|
}
|
|
259
287
|
}
|
|
260
|
-
if (
|
|
261
|
-
await streamToPulse(buildLogsStreamURL, sdkKey, logsContent, logger);
|
|
262
|
-
} else if (existsSync(rawLogsFile)) {
|
|
288
|
+
if (!streamedCleanLogs && existsSync(rawLogsFile)) {
|
|
263
289
|
// Stream raw logs file directly to Pulse without loading into memory
|
|
264
290
|
await streamToPulse(buildLogsStreamURL, sdkKey, Bun.file(rawLogsFile), logger);
|
|
265
291
|
}
|
|
@@ -299,24 +325,32 @@ export async function runForkedDeploy(options: ForkDeployOptions): Promise<ForkD
|
|
|
299
325
|
return { success: false, exitCode, diagnostics };
|
|
300
326
|
}
|
|
301
327
|
|
|
302
|
-
return { success: true, exitCode, diagnostics };
|
|
328
|
+
return { success: true, exitCode, diagnostics, deployResult };
|
|
303
329
|
} catch (err) {
|
|
304
330
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
305
331
|
logger.error('Fork deploy error: %s', errorMessage);
|
|
306
332
|
|
|
307
333
|
if (buildLogsStreamURL) {
|
|
308
|
-
let
|
|
334
|
+
let streamedCleanLogs = false;
|
|
309
335
|
if (existsSync(cleanLogsFile)) {
|
|
310
336
|
try {
|
|
311
|
-
|
|
337
|
+
const cleanLogs = Bun.file(cleanLogsFile);
|
|
338
|
+
if (cleanLogs.size > 0) {
|
|
339
|
+
await streamToPulse(buildLogsStreamURL, sdkKey, cleanLogs, logger);
|
|
340
|
+
streamedCleanLogs = true;
|
|
341
|
+
}
|
|
312
342
|
unlinkSync(cleanLogsFile);
|
|
313
343
|
} catch {
|
|
314
344
|
// ignore
|
|
315
345
|
}
|
|
316
346
|
}
|
|
317
|
-
if (
|
|
318
|
-
|
|
319
|
-
|
|
347
|
+
if (streamedCleanLogs) {
|
|
348
|
+
await streamToPulse(
|
|
349
|
+
buildLogsStreamURL,
|
|
350
|
+
sdkKey,
|
|
351
|
+
`\n\n--- FORK ERROR ---\n${errorMessage}\n`,
|
|
352
|
+
logger
|
|
353
|
+
);
|
|
320
354
|
} else {
|
|
321
355
|
// Append error to raw logs file and stream it without loading into memory
|
|
322
356
|
try {
|
|
@@ -383,7 +417,7 @@ export async function runForkedDeploy(options: ForkDeployOptions): Promise<ForkD
|
|
|
383
417
|
process.off('SIGTERM', sigtermHandler);
|
|
384
418
|
|
|
385
419
|
// Clean up temp files
|
|
386
|
-
for (const file of [reportFile, cleanLogsFile, rawLogsFile]) {
|
|
420
|
+
for (const file of [reportFile, cleanLogsFile, rawLogsFile, deployResultFile]) {
|
|
387
421
|
if (existsSync(file)) {
|
|
388
422
|
try {
|
|
389
423
|
unlinkSync(file);
|
package/src/cmd/cloud/deploy.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { createPublicKey } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
createReadStream,
|
|
4
|
+
createWriteStream,
|
|
5
|
+
existsSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
unlinkSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from 'node:fs';
|
|
3
10
|
import { tmpdir } from 'node:os';
|
|
4
11
|
import { join, resolve } from 'node:path';
|
|
12
|
+
import { pipeline } from 'node:stream/promises';
|
|
13
|
+
import { createGzip } from 'node:zlib';
|
|
5
14
|
import { StructuredError } from '@agentuity/core';
|
|
6
15
|
import {
|
|
7
16
|
type BuildMetadata,
|
|
@@ -403,6 +412,8 @@ export const deploySubcommand = createSubcommand({
|
|
|
403
412
|
success: true,
|
|
404
413
|
deploymentId: initialDeployment.id,
|
|
405
414
|
projectId: project.projectId,
|
|
415
|
+
logs: result.deployResult?.logs,
|
|
416
|
+
urls: result.deployResult?.urls,
|
|
406
417
|
};
|
|
407
418
|
}
|
|
408
419
|
let useExistingDeployment = false;
|
|
@@ -821,9 +832,10 @@ export const deploySubcommand = createSubcommand({
|
|
|
821
832
|
endCodeUploadDiagnostic();
|
|
822
833
|
|
|
823
834
|
progress(70);
|
|
824
|
-
ctx.logger.trace('
|
|
825
|
-
//
|
|
826
|
-
|
|
835
|
+
ctx.logger.trace('Cancelling upload response body');
|
|
836
|
+
// No response payload is needed for successful uploads.
|
|
837
|
+
// Cancel to release resources without buffering into memory.
|
|
838
|
+
await resp.body?.cancel();
|
|
827
839
|
ctx.logger.trace('Deleting encrypted zip');
|
|
828
840
|
await zipfile.delete();
|
|
829
841
|
} finally {
|
|
@@ -874,27 +886,48 @@ export const deploySubcommand = createSubcommand({
|
|
|
874
886
|
|
|
875
887
|
bytes += asset.size;
|
|
876
888
|
|
|
877
|
-
let body:
|
|
889
|
+
let body: Blob;
|
|
890
|
+
let gzTempPath: string | undefined;
|
|
878
891
|
if (asset.contentEncoding === 'gzip') {
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
892
|
+
// Gzip to a temp file so Bun.file() can provide
|
|
893
|
+
// Content-Length to S3 (streaming bodies use chunked
|
|
894
|
+
// transfer encoding which S3 rejects).
|
|
895
|
+
gzTempPath = join(
|
|
896
|
+
tmpdir(),
|
|
897
|
+
`agentuity-asset-${deployment.id}-${Date.now()}-${asset.filename.replace(/\//g, '_')}.gz`
|
|
898
|
+
);
|
|
899
|
+
await pipeline(
|
|
900
|
+
createReadStream(filePath),
|
|
901
|
+
createGzip(),
|
|
902
|
+
createWriteStream(gzTempPath)
|
|
903
|
+
);
|
|
882
904
|
headers['Content-Encoding'] = 'gzip';
|
|
883
|
-
body =
|
|
905
|
+
body = Bun.file(gzTempPath);
|
|
906
|
+
const compressedSize = body.size;
|
|
884
907
|
ctx.logger.trace(
|
|
885
|
-
`
|
|
908
|
+
`Gzip compressed ${asset.filename} (${asset.size} -> ${compressedSize} bytes)`
|
|
886
909
|
);
|
|
887
910
|
} else {
|
|
888
911
|
body = Bun.file(filePath);
|
|
889
912
|
}
|
|
890
913
|
|
|
914
|
+
const assetGzTempPath = gzTempPath;
|
|
891
915
|
promises.push(
|
|
892
916
|
fetch(assetUrl, {
|
|
893
917
|
method: 'PUT',
|
|
894
|
-
duplex: 'half',
|
|
895
918
|
headers,
|
|
896
919
|
body,
|
|
897
920
|
signal: stepCtx.signal,
|
|
921
|
+
}).then((response) => {
|
|
922
|
+
// Clean up temp gzip file after upload completes
|
|
923
|
+
if (assetGzTempPath) {
|
|
924
|
+
try {
|
|
925
|
+
unlinkSync(assetGzTempPath);
|
|
926
|
+
} catch {
|
|
927
|
+
// ignore — file may already be cleaned up
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
return response;
|
|
898
931
|
})
|
|
899
932
|
);
|
|
900
933
|
}
|
|
@@ -1194,7 +1227,7 @@ export const deploySubcommand = createSubcommand({
|
|
|
1194
1227
|
}
|
|
1195
1228
|
|
|
1196
1229
|
// Show deployment URLs
|
|
1197
|
-
if (complete?.publicUrls) {
|
|
1230
|
+
if (complete?.publicUrls && !options.json) {
|
|
1198
1231
|
const lines: string[] = [];
|
|
1199
1232
|
if (complete.publicUrls.custom?.length) {
|
|
1200
1233
|
for (const url of complete.publicUrls.custom) {
|
|
@@ -1237,6 +1270,29 @@ export const deploySubcommand = createSubcommand({
|
|
|
1237
1270
|
}
|
|
1238
1271
|
clearGlobalCollector();
|
|
1239
1272
|
|
|
1273
|
+
// Write deploy result to file for fork parent to consume
|
|
1274
|
+
const deployResultFile = process.env.AGENTUITY_DEPLOY_RESULT_FILE;
|
|
1275
|
+
if (deployResultFile) {
|
|
1276
|
+
try {
|
|
1277
|
+
const resultData = {
|
|
1278
|
+
urls: complete?.publicUrls
|
|
1279
|
+
? {
|
|
1280
|
+
deployment:
|
|
1281
|
+
complete.publicUrls.vanityDeployment ??
|
|
1282
|
+
complete.publicUrls.deployment,
|
|
1283
|
+
latest: complete.publicUrls.vanityProject ?? complete.publicUrls.latest,
|
|
1284
|
+
custom: complete.publicUrls.custom,
|
|
1285
|
+
dashboard,
|
|
1286
|
+
}
|
|
1287
|
+
: undefined,
|
|
1288
|
+
logs,
|
|
1289
|
+
};
|
|
1290
|
+
writeFileSync(deployResultFile, JSON.stringify(resultData));
|
|
1291
|
+
} catch {
|
|
1292
|
+
// Non-fatal: result file is optional
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1240
1296
|
return {
|
|
1241
1297
|
success: true,
|
|
1242
1298
|
deploymentId: deployment.id,
|
package/src/cmd/project/show.ts
CHANGED
|
@@ -12,6 +12,14 @@ const ProjectShowResponseSchema = z.object({
|
|
|
12
12
|
orgId: z.string().describe('Organization ID'),
|
|
13
13
|
secrets: z.record(z.string(), z.string()).optional().describe('Project secrets (masked)'),
|
|
14
14
|
env: z.record(z.string(), z.string()).optional().describe('Environment variables'),
|
|
15
|
+
urls: z
|
|
16
|
+
.object({
|
|
17
|
+
dashboard: z.string().describe('Dashboard URL for the project'),
|
|
18
|
+
app: z.string().describe('Public URL for the latest deployment'),
|
|
19
|
+
custom: z.array(z.string()).describe('Custom domain URLs'),
|
|
20
|
+
})
|
|
21
|
+
.optional()
|
|
22
|
+
.describe('Project URLs'),
|
|
15
23
|
});
|
|
16
24
|
|
|
17
25
|
export const showSubcommand = createSubcommand({
|
|
@@ -60,6 +68,7 @@ export const showSubcommand = createSubcommand({
|
|
|
60
68
|
orgId: project.orgId,
|
|
61
69
|
secrets: project.secrets,
|
|
62
70
|
env: project.env,
|
|
71
|
+
urls: project.urls,
|
|
63
72
|
};
|
|
64
73
|
},
|
|
65
74
|
});
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { createSubcommand } from '../../types';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { createWriteStream, readFileSync } from 'node:fs';
|
|
4
4
|
import { join, basename } from 'node:path';
|
|
5
5
|
import { tmpdir } from 'node:os';
|
|
6
6
|
import { getLogSessionsInCurrentWindow } from '../../internal-logger';
|
|
7
7
|
import * as tui from '../../tui';
|
|
8
8
|
import { randomBytes } from 'node:crypto';
|
|
9
|
-
import
|
|
9
|
+
import archiver from 'archiver';
|
|
10
10
|
import { APIResponseSchema } from '@agentuity/server';
|
|
11
11
|
import { StructuredError } from '@agentuity/core';
|
|
12
12
|
|
|
@@ -61,10 +61,19 @@ async function createReportZip(sessionDirs: string[]): Promise<string> {
|
|
|
61
61
|
throw NoSessionDirectoriesError();
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
// Create zip in temp directory
|
|
64
|
+
// Create zip in temp directory, streaming to disk instead of buffering in memory
|
|
65
65
|
const tempZip = join(tmpdir(), `agentuity-report-${randomBytes(8).toString('hex')}.zip`);
|
|
66
66
|
|
|
67
|
-
const
|
|
67
|
+
const output = createWriteStream(tempZip);
|
|
68
|
+
const zip = archiver('zip', { zlib: { level: 9 } });
|
|
69
|
+
|
|
70
|
+
const writeDone = new Promise<void>((resolve, reject) => {
|
|
71
|
+
output.on('close', resolve);
|
|
72
|
+
output.on('error', reject);
|
|
73
|
+
zip.on('error', reject);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
zip.pipe(output);
|
|
68
77
|
|
|
69
78
|
for (const sessionDir of sessionDirs) {
|
|
70
79
|
const sessionFile = join(sessionDir, 'session.json');
|
|
@@ -75,14 +84,15 @@ async function createReportZip(sessionDirs: string[]): Promise<string> {
|
|
|
75
84
|
|
|
76
85
|
// Add files with session ID prefix to avoid conflicts
|
|
77
86
|
if (await Bun.file(sessionFile).exists()) {
|
|
78
|
-
zip.
|
|
87
|
+
zip.file(sessionFile, { name: `${sessionId}/session.json` });
|
|
79
88
|
}
|
|
80
89
|
if (await Bun.file(logsFile).exists()) {
|
|
81
|
-
zip.
|
|
90
|
+
zip.file(logsFile, { name: `${sessionId}/logs.jsonl` });
|
|
82
91
|
}
|
|
83
92
|
}
|
|
84
93
|
|
|
85
|
-
zip.
|
|
94
|
+
await zip.finalize();
|
|
95
|
+
await writeDone;
|
|
86
96
|
|
|
87
97
|
return tempZip;
|
|
88
98
|
}
|
|
@@ -95,14 +105,15 @@ async function uploadReport(
|
|
|
95
105
|
zipPath: string,
|
|
96
106
|
logger: import('../../types').Logger
|
|
97
107
|
): Promise<void> {
|
|
98
|
-
|
|
108
|
+
// Use Bun.file() to stream the zip to S3 without loading it into memory.
|
|
109
|
+
// Bun automatically sets Content-Length from the file size.
|
|
110
|
+
const file = Bun.file(zipPath);
|
|
99
111
|
|
|
100
112
|
const response = await fetch(presignedUrl, {
|
|
101
113
|
method: 'PUT',
|
|
102
|
-
body:
|
|
114
|
+
body: file,
|
|
103
115
|
headers: {
|
|
104
116
|
'Content-Type': 'application/zip',
|
|
105
|
-
'Content-Length': fileBuffer.length.toString(),
|
|
106
117
|
},
|
|
107
118
|
});
|
|
108
119
|
|
package/src/steps.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type { LogLevel } from './types';
|
|
|
10
10
|
import { ValidationInputError, ValidationOutputError, type IssuesType } from '@agentuity/server';
|
|
11
11
|
import { clearLastLines, isTTYLike } from './tui';
|
|
12
12
|
import { appendLog, isLogCollectionEnabled } from './log-collector';
|
|
13
|
+
import { getOutputOptions, isJSONMode } from './output';
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Error thrown when step execution is interrupted by a signal (e.g., Ctrl+C).
|
|
@@ -700,6 +701,43 @@ async function runStepsPlain(steps: Step[]): Promise<void> {
|
|
|
700
701
|
* Run a series of steps with animated progress
|
|
701
702
|
*/
|
|
702
703
|
export async function runSteps(steps: Step[], logLevel?: LogLevel): Promise<void> {
|
|
704
|
+
const outputOptions = getOutputOptions();
|
|
705
|
+
|
|
706
|
+
// In JSON mode, skip all UI rendering
|
|
707
|
+
if (outputOptions && isJSONMode(outputOptions)) {
|
|
708
|
+
const abortController = new AbortController();
|
|
709
|
+
for (const step of steps) {
|
|
710
|
+
if (abortController.signal.aborted) break;
|
|
711
|
+
if (step) {
|
|
712
|
+
const ctx: StepContext = {
|
|
713
|
+
signal: abortController.signal,
|
|
714
|
+
progress: () => {},
|
|
715
|
+
};
|
|
716
|
+
let outcome: StepOutcome;
|
|
717
|
+
try {
|
|
718
|
+
outcome = await step.run(ctx);
|
|
719
|
+
} catch (err) {
|
|
720
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
721
|
+
throw new StepInterruptError();
|
|
722
|
+
}
|
|
723
|
+
outcome = {
|
|
724
|
+
status: 'error',
|
|
725
|
+
message: err instanceof Error ? err.message : String(err),
|
|
726
|
+
cause: err instanceof Error ? err : undefined,
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
if (outcome.status === 'error') {
|
|
730
|
+
if (outcome.cause instanceof Error && outcome.cause.name === 'AbortError') {
|
|
731
|
+
throw new StepInterruptError();
|
|
732
|
+
}
|
|
733
|
+
const errorMsg = outcome.message || 'An unknown error occurred';
|
|
734
|
+
throw new Error(errorMsg);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
|
|
703
741
|
const useTUI = isTTYLike() && (!logLevel || ['info', 'warn', 'error'].includes(logLevel));
|
|
704
742
|
|
|
705
743
|
if (useTUI) {
|
package/src/tui.ts
CHANGED
|
@@ -299,8 +299,13 @@ export function getSeverityColor(severity: string): (text: string) => string {
|
|
|
299
299
|
export function success(message: string): void {
|
|
300
300
|
const color = getColor('success');
|
|
301
301
|
const reset = getColor('reset');
|
|
302
|
-
|
|
303
|
-
|
|
302
|
+
if (process.stderr.isTTY) {
|
|
303
|
+
// Clear line first to ensure no leftover content from previous output
|
|
304
|
+
process.stderr.write(`\r\x1b[2K${color}${ICONS.success} ${message}${reset}\n`);
|
|
305
|
+
} else {
|
|
306
|
+
// No ANSI control sequences for non-TTY streams (pipes, command substitution)
|
|
307
|
+
process.stderr.write(`${ICONS.success} ${message}\n`);
|
|
308
|
+
}
|
|
304
309
|
}
|
|
305
310
|
|
|
306
311
|
/**
|
|
@@ -1248,18 +1253,23 @@ export async function spinner<T>(
|
|
|
1248
1253
|
const { getOutputOptions, shouldDisableProgress } = await import('./output');
|
|
1249
1254
|
const outputOptions = getOutputOptions();
|
|
1250
1255
|
const noProgress = outputOptions ? shouldDisableProgress(outputOptions) : false;
|
|
1256
|
+
const isJsonMode = outputOptions?.json === true;
|
|
1251
1257
|
|
|
1252
|
-
// If
|
|
1253
|
-
// the callback without animation
|
|
1254
|
-
|
|
1258
|
+
// If stderr is not a real terminal or progress disabled, just execute
|
|
1259
|
+
// the callback without animation. We check stderr specifically because
|
|
1260
|
+
// the spinner writes ANSI sequences to stderr — isTTYLike() may return
|
|
1261
|
+
// true when stdout is a TTY but stderr is piped (e.g. 2>&1 in $()).
|
|
1262
|
+
if (!process.stderr.isTTY || noProgress) {
|
|
1255
1263
|
try {
|
|
1256
1264
|
const result =
|
|
1257
1265
|
options.type === 'progress'
|
|
1258
1266
|
? await options.callback(() => {})
|
|
1259
1267
|
: options.type === 'logger'
|
|
1260
1268
|
? await options.callback((logMessage: string) => {
|
|
1261
|
-
// In
|
|
1262
|
-
|
|
1269
|
+
// In JSON mode, don't write logs to stdout
|
|
1270
|
+
if (!isJsonMode) {
|
|
1271
|
+
process.stdout.write(logMessage + '\n');
|
|
1272
|
+
}
|
|
1263
1273
|
})
|
|
1264
1274
|
: options.type === 'countdown'
|
|
1265
1275
|
? await options.callback()
|
|
@@ -1269,7 +1279,6 @@ export async function spinner<T>(
|
|
|
1269
1279
|
|
|
1270
1280
|
// If clearOnSuccess is true, don't show success message
|
|
1271
1281
|
// Also skip success message in JSON mode
|
|
1272
|
-
const isJsonMode = outputOptions?.json === true;
|
|
1273
1282
|
if (!options.clearOnSuccess && !isJsonMode) {
|
|
1274
1283
|
const successColor = getColor('success');
|
|
1275
1284
|
console.error(`${successColor}${ICONS.success} ${message}${reset}`);
|
|
@@ -1279,7 +1288,7 @@ export async function spinner<T>(
|
|
|
1279
1288
|
} catch (err) {
|
|
1280
1289
|
const clearOnError =
|
|
1281
1290
|
(options.type === 'progress' || options.type === 'simple') && options.clearOnError;
|
|
1282
|
-
if (!clearOnError) {
|
|
1291
|
+
if (!clearOnError && !isJsonMode) {
|
|
1283
1292
|
const errorColor = getColor('error');
|
|
1284
1293
|
console.error(`${errorColor}${ICONS.error} ${message}${reset}`);
|
|
1285
1294
|
}
|
package/src/utils/zip.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { createWriteStream, lstatSync } from 'node:fs';
|
|
2
|
+
import { mkdir } from 'node:fs/promises';
|
|
3
|
+
import { dirname, relative } from 'node:path';
|
|
3
4
|
import { Glob } from 'bun';
|
|
4
|
-
import
|
|
5
|
+
import archiver from 'archiver';
|
|
5
6
|
import { toForwardSlash } from './normalize-path';
|
|
6
7
|
|
|
7
8
|
interface Options {
|
|
@@ -10,7 +11,20 @@ interface Options {
|
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export async function zipDir(dir: string, outdir: string, options?: Options) {
|
|
13
|
-
|
|
14
|
+
await mkdir(dirname(outdir), { recursive: true });
|
|
15
|
+
const output = createWriteStream(outdir);
|
|
16
|
+
const zip = archiver('zip', {
|
|
17
|
+
zlib: { level: 9 },
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const writeDone = new Promise<void>((resolve, reject) => {
|
|
21
|
+
output.on('close', resolve);
|
|
22
|
+
output.on('error', reject);
|
|
23
|
+
zip.on('error', reject);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
zip.pipe(output);
|
|
27
|
+
|
|
14
28
|
const files = await Array.fromAsync(
|
|
15
29
|
new Glob('**/*').scan({ cwd: dir, absolute: true, dot: true, followSymlinks: false })
|
|
16
30
|
);
|
|
@@ -31,11 +45,8 @@ export async function zipDir(dir: string, outdir: string, options?: Options) {
|
|
|
31
45
|
// across machines and would cause EISDIR errors on extraction.
|
|
32
46
|
const stat = lstatSync(file);
|
|
33
47
|
if (!stat.isSymbolicLink() && !stat.isDirectory()) {
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
// with incorrect Unix permission bits, causing EACCES errors when extracted on Linux.
|
|
37
|
-
const data = readFileSync(file);
|
|
38
|
-
zip.addFile(rel, data, '', 0o644);
|
|
48
|
+
// Set explicit Unix permissions (0o644) for portability across OSes.
|
|
49
|
+
zip.file(file, { name: rel, mode: 0o644 });
|
|
39
50
|
}
|
|
40
51
|
} catch (err) {
|
|
41
52
|
throw new Error(`Failed to add file to zip: ${rel} (${file})`, { cause: err });
|
|
@@ -48,7 +59,8 @@ export async function zipDir(dir: string, outdir: string, options?: Options) {
|
|
|
48
59
|
await Bun.sleep(10); // give some time for the progress bar to render
|
|
49
60
|
}
|
|
50
61
|
}
|
|
51
|
-
await zip.
|
|
62
|
+
await zip.finalize();
|
|
63
|
+
await writeDone;
|
|
52
64
|
if (options?.progress) {
|
|
53
65
|
options.progress(100);
|
|
54
66
|
await Bun.sleep(100); // give some time for the progress bar to render
|