@agentuity/cli 2.0.0-beta.1 → 2.0.1
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/bin/cli.ts +3 -1
- package/dist/cmd/build/ci.d.ts +1 -1
- package/dist/cmd/build/ci.d.ts.map +1 -1
- package/dist/cmd/build/ci.js +70 -63
- package/dist/cmd/build/ci.js.map +1 -1
- package/dist/cmd/build/index.d.ts.map +1 -1
- package/dist/cmd/build/index.js +0 -3
- package/dist/cmd/build/index.js.map +1 -1
- package/dist/cmd/build/vite/agent-discovery.d.ts.map +1 -1
- package/dist/cmd/build/vite/agent-discovery.js +26 -2
- package/dist/cmd/build/vite/agent-discovery.js.map +1 -1
- package/dist/cmd/build/vite/route-discovery.d.ts +5 -0
- package/dist/cmd/build/vite/route-discovery.d.ts.map +1 -1
- package/dist/cmd/build/vite/route-discovery.js +13 -11
- package/dist/cmd/build/vite/route-discovery.js.map +1 -1
- package/dist/cmd/build/vite/static-renderer.d.ts +3 -2
- package/dist/cmd/build/vite/static-renderer.d.ts.map +1 -1
- package/dist/cmd/build/vite/static-renderer.js +28 -58
- package/dist/cmd/build/vite/static-renderer.js.map +1 -1
- package/dist/cmd/build/vite/vite-builder.d.ts.map +1 -1
- package/dist/cmd/build/vite/vite-builder.js +33 -0
- 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 +71 -32
- 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/cloud/sandbox/create.d.ts.map +1 -1
- package/dist/cmd/cloud/sandbox/create.js +5 -0
- package/dist/cmd/cloud/sandbox/create.js.map +1 -1
- package/dist/cmd/cloud/sandbox/exec.d.ts.map +1 -1
- package/dist/cmd/cloud/sandbox/exec.js +76 -66
- package/dist/cmd/cloud/sandbox/exec.js.map +1 -1
- package/dist/cmd/cloud/sandbox/job/index.d.ts.map +1 -1
- package/dist/cmd/cloud/sandbox/job/index.js +12 -1
- package/dist/cmd/cloud/sandbox/job/index.js.map +1 -1
- package/dist/cmd/cloud/sandbox/job/logs.d.ts +3 -0
- package/dist/cmd/cloud/sandbox/job/logs.d.ts.map +1 -0
- package/dist/cmd/cloud/sandbox/job/logs.js +124 -0
- package/dist/cmd/cloud/sandbox/job/logs.js.map +1 -0
- package/dist/cmd/cloud/sandbox/run.d.ts.map +1 -1
- package/dist/cmd/cloud/sandbox/run.js +14 -2
- package/dist/cmd/cloud/sandbox/run.js.map +1 -1
- package/dist/cmd/cloud/sandbox/snapshot/build.js +2 -2
- package/dist/cmd/cloud/sandbox/snapshot/build.js.map +1 -1
- package/dist/cmd/coder/hub-url.d.ts.map +1 -1
- package/dist/cmd/coder/hub-url.js +3 -1
- package/dist/cmd/coder/hub-url.js.map +1 -1
- package/dist/cmd/coder/start.js +6 -6
- package/dist/cmd/coder/start.js.map +1 -1
- package/dist/cmd/coder/tui-init.d.ts +2 -2
- package/dist/cmd/coder/tui-init.js +2 -2
- package/dist/cmd/coder/tui-init.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/errors.d.ts +24 -10
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +42 -12
- package/dist/errors.js.map +1 -1
- package/dist/schema-generator.d.ts.map +1 -1
- package/dist/schema-generator.js +2 -12
- package/dist/schema-generator.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 +25 -9
- package/dist/tui.js.map +1 -1
- package/dist/utils/stream-capture.d.ts +9 -0
- package/dist/utils/stream-capture.d.ts.map +1 -0
- package/dist/utils/stream-capture.js +34 -0
- package/dist/utils/stream-capture.js.map +1 -0
- package/dist/utils/stream-url.d.ts +23 -0
- package/dist/utils/stream-url.d.ts.map +1 -0
- package/dist/utils/stream-url.js +153 -0
- package/dist/utils/stream-url.js.map +1 -0
- 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 +9 -7
- package/src/cmd/build/ci.ts +82 -80
- package/src/cmd/build/index.ts +0 -4
- package/src/cmd/build/vite/agent-discovery.ts +30 -5
- package/src/cmd/build/vite/route-discovery.ts +25 -12
- package/src/cmd/build/vite/static-renderer.ts +33 -64
- package/src/cmd/build/vite/vite-builder.ts +36 -0
- package/src/cmd/cloud/deploy-fork.ts +90 -33
- package/src/cmd/cloud/deploy.ts +68 -12
- package/src/cmd/cloud/sandbox/create.ts +7 -0
- package/src/cmd/cloud/sandbox/exec.ts +102 -90
- package/src/cmd/cloud/sandbox/job/index.ts +12 -1
- package/src/cmd/cloud/sandbox/job/logs.ts +139 -0
- package/src/cmd/cloud/sandbox/run.ts +16 -2
- package/src/cmd/cloud/sandbox/snapshot/build.ts +2 -2
- package/src/cmd/coder/hub-url.ts +3 -1
- package/src/cmd/coder/start.ts +6 -6
- package/src/cmd/coder/tui-init.ts +4 -4
- package/src/cmd/project/show.ts +9 -0
- package/src/cmd/support/report.ts +21 -10
- package/src/errors.ts +44 -12
- package/src/schema-generator.ts +2 -12
- package/src/steps.ts +38 -0
- package/src/tui.ts +24 -9
- package/src/utils/stream-capture.ts +39 -0
- package/src/utils/stream-url.ts +226 -0
- package/src/utils/zip.ts +22 -10
package/src/errors.ts
CHANGED
|
@@ -1,22 +1,51 @@
|
|
|
1
1
|
import type { Logger } from './types';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Standard exit codes for the CLI
|
|
4
|
+
* Standard exit codes for the CLI.
|
|
5
|
+
*
|
|
6
|
+
* Values start at 10 to avoid collisions with Unix signal numbers (1-9)
|
|
7
|
+
* and shell conventions (e.g. 2 = misuse of builtins, 9 = SIGKILL/OOM).
|
|
8
|
+
* Range 10-125 is safe — below shell-reserved 126-128 and signal-death
|
|
9
|
+
* codes 128+N.
|
|
10
|
+
*
|
|
11
|
+
* 0 and 1 are kept as universal success/failure codes.
|
|
5
12
|
*/
|
|
6
13
|
export enum ExitCode {
|
|
7
14
|
SUCCESS = 0,
|
|
8
15
|
GENERAL_ERROR = 1,
|
|
9
|
-
VALIDATION_ERROR =
|
|
10
|
-
AUTH_ERROR =
|
|
11
|
-
NOT_FOUND =
|
|
12
|
-
PERMISSION_ERROR =
|
|
13
|
-
NETWORK_ERROR =
|
|
14
|
-
FILE_ERROR =
|
|
15
|
-
USER_CANCELLED =
|
|
16
|
-
BUILD_FAILED =
|
|
17
|
-
SECURITY_ERROR =
|
|
16
|
+
VALIDATION_ERROR = 10,
|
|
17
|
+
AUTH_ERROR = 11,
|
|
18
|
+
NOT_FOUND = 12,
|
|
19
|
+
PERMISSION_ERROR = 13,
|
|
20
|
+
NETWORK_ERROR = 14,
|
|
21
|
+
FILE_ERROR = 15,
|
|
22
|
+
USER_CANCELLED = 16,
|
|
23
|
+
BUILD_FAILED = 17,
|
|
24
|
+
SECURITY_ERROR = 18,
|
|
25
|
+
PAYMENT_REQUIRED = 19,
|
|
26
|
+
UPGRADE_REQUIRED = 20,
|
|
18
27
|
}
|
|
19
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Human-readable descriptions for each exit code.
|
|
31
|
+
* This is the single source of truth consumed by the schema generator and AI help.
|
|
32
|
+
*/
|
|
33
|
+
export const exitCodeDescriptions: Record<number, string> = {
|
|
34
|
+
[ExitCode.SUCCESS]: 'Success',
|
|
35
|
+
[ExitCode.GENERAL_ERROR]: 'General error',
|
|
36
|
+
[ExitCode.VALIDATION_ERROR]: 'Validation error (invalid arguments or options)',
|
|
37
|
+
[ExitCode.AUTH_ERROR]: 'Authentication error (login required or credentials invalid)',
|
|
38
|
+
[ExitCode.NOT_FOUND]: 'Resource not found (project, file, deployment, etc.)',
|
|
39
|
+
[ExitCode.PERMISSION_ERROR]: 'Permission denied (insufficient access rights)',
|
|
40
|
+
[ExitCode.NETWORK_ERROR]: 'Network error (API unreachable or timeout)',
|
|
41
|
+
[ExitCode.FILE_ERROR]: 'File system error (file read/write failed)',
|
|
42
|
+
[ExitCode.USER_CANCELLED]: 'User cancelled (operation aborted by user)',
|
|
43
|
+
[ExitCode.BUILD_FAILED]: 'Build failed',
|
|
44
|
+
[ExitCode.SECURITY_ERROR]: 'Security error (malware detected)',
|
|
45
|
+
[ExitCode.PAYMENT_REQUIRED]: 'Payment required (plan upgrade needed)',
|
|
46
|
+
[ExitCode.UPGRADE_REQUIRED]: 'Upgrade required (CLI version too old)',
|
|
47
|
+
};
|
|
48
|
+
|
|
20
49
|
/**
|
|
21
50
|
* Standard error codes for the CLI
|
|
22
51
|
*/
|
|
@@ -152,7 +181,11 @@ export function getExitCode(errorCode: ErrorCode): ExitCode {
|
|
|
152
181
|
|
|
153
182
|
// Payment required - user needs to upgrade their plan
|
|
154
183
|
case ErrorCode.PAYMENT_REQUIRED:
|
|
155
|
-
return ExitCode.
|
|
184
|
+
return ExitCode.PAYMENT_REQUIRED;
|
|
185
|
+
|
|
186
|
+
// Upgrade required - CLI version too old
|
|
187
|
+
case ErrorCode.UPGRADE_REQUIRED:
|
|
188
|
+
return ExitCode.UPGRADE_REQUIRED;
|
|
156
189
|
|
|
157
190
|
// Resource conflicts and other errors
|
|
158
191
|
case ErrorCode.RESOURCE_ALREADY_EXISTS:
|
|
@@ -162,7 +195,6 @@ export function getExitCode(errorCode: ErrorCode): ExitCode {
|
|
|
162
195
|
case ErrorCode.RUNTIME_ERROR:
|
|
163
196
|
case ErrorCode.INTERNAL_ERROR:
|
|
164
197
|
case ErrorCode.NOT_IMPLEMENTED:
|
|
165
|
-
case ErrorCode.UPGRADE_REQUIRED:
|
|
166
198
|
default:
|
|
167
199
|
return ExitCode.GENERAL_ERROR;
|
|
168
200
|
}
|
package/src/schema-generator.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
2
|
import type { CommandDefinition, SubcommandDefinition, CommandSchemas } from './types';
|
|
3
|
+
import { exitCodeDescriptions } from './errors';
|
|
3
4
|
import { parseArgsSchema, parseOptionsSchema } from './schema-parser';
|
|
4
5
|
import * as z from 'zod';
|
|
5
6
|
|
|
@@ -313,18 +314,7 @@ export function generateCLISchema(
|
|
|
313
314
|
name: 'agentuity',
|
|
314
315
|
version,
|
|
315
316
|
description: 'Agentuity CLI',
|
|
316
|
-
exitCodes: {
|
|
317
|
-
0: 'Success',
|
|
318
|
-
1: 'General error',
|
|
319
|
-
2: 'Validation error (invalid arguments or options)',
|
|
320
|
-
3: 'Authentication error (login required or credentials invalid)',
|
|
321
|
-
4: 'Resource not found (project, file, deployment, etc.)',
|
|
322
|
-
5: 'Permission denied (insufficient access rights)',
|
|
323
|
-
6: 'Network error (API unreachable or timeout)',
|
|
324
|
-
7: 'File system error (file read/write failed)',
|
|
325
|
-
8: 'User cancelled (operation aborted by user)',
|
|
326
|
-
9: 'Build failed',
|
|
327
|
-
},
|
|
317
|
+
exitCodes: { ...exitCodeDescriptions },
|
|
328
318
|
globalOptions: [
|
|
329
319
|
{
|
|
330
320
|
name: 'config',
|
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
|
@@ -25,6 +25,12 @@ function ensureCursorRestoration(): void {
|
|
|
25
25
|
exitHandlerInstalled = true;
|
|
26
26
|
|
|
27
27
|
const restoreCursor = () => {
|
|
28
|
+
// Only write ANSI escape sequences when stderr is a real terminal.
|
|
29
|
+
// Writing to non-TTY streams (pipes, command substitution, etc.)
|
|
30
|
+
// pollutes captured output with invisible control characters.
|
|
31
|
+
if (!process.stderr.isTTY) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
28
34
|
// Skip cursor restoration in CI - terminals don't support these sequences
|
|
29
35
|
if (process.env.CI) {
|
|
30
36
|
return;
|
|
@@ -293,8 +299,13 @@ export function getSeverityColor(severity: string): (text: string) => string {
|
|
|
293
299
|
export function success(message: string): void {
|
|
294
300
|
const color = getColor('success');
|
|
295
301
|
const reset = getColor('reset');
|
|
296
|
-
|
|
297
|
-
|
|
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
|
+
}
|
|
298
309
|
}
|
|
299
310
|
|
|
300
311
|
/**
|
|
@@ -1242,18 +1253,23 @@ export async function spinner<T>(
|
|
|
1242
1253
|
const { getOutputOptions, shouldDisableProgress } = await import('./output');
|
|
1243
1254
|
const outputOptions = getOutputOptions();
|
|
1244
1255
|
const noProgress = outputOptions ? shouldDisableProgress(outputOptions) : false;
|
|
1256
|
+
const isJsonMode = outputOptions?.json === true;
|
|
1245
1257
|
|
|
1246
|
-
// If
|
|
1247
|
-
// the callback without animation
|
|
1248
|
-
|
|
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) {
|
|
1249
1263
|
try {
|
|
1250
1264
|
const result =
|
|
1251
1265
|
options.type === 'progress'
|
|
1252
1266
|
? await options.callback(() => {})
|
|
1253
1267
|
: options.type === 'logger'
|
|
1254
1268
|
? await options.callback((logMessage: string) => {
|
|
1255
|
-
// In
|
|
1256
|
-
|
|
1269
|
+
// In JSON mode, don't write logs to stdout
|
|
1270
|
+
if (!isJsonMode) {
|
|
1271
|
+
process.stdout.write(logMessage + '\n');
|
|
1272
|
+
}
|
|
1257
1273
|
})
|
|
1258
1274
|
: options.type === 'countdown'
|
|
1259
1275
|
? await options.callback()
|
|
@@ -1263,7 +1279,6 @@ export async function spinner<T>(
|
|
|
1263
1279
|
|
|
1264
1280
|
// If clearOnSuccess is true, don't show success message
|
|
1265
1281
|
// Also skip success message in JSON mode
|
|
1266
|
-
const isJsonMode = outputOptions?.json === true;
|
|
1267
1282
|
if (!options.clearOnSuccess && !isJsonMode) {
|
|
1268
1283
|
const successColor = getColor('success');
|
|
1269
1284
|
console.error(`${successColor}${ICONS.success} ${message}${reset}`);
|
|
@@ -1273,7 +1288,7 @@ export async function spinner<T>(
|
|
|
1273
1288
|
} catch (err) {
|
|
1274
1289
|
const clearOnError =
|
|
1275
1290
|
(options.type === 'progress' || options.type === 'simple') && options.clearOnError;
|
|
1276
|
-
if (!clearOnError) {
|
|
1291
|
+
if (!clearOnError && !isJsonMode) {
|
|
1277
1292
|
const errorColor = getColor('error');
|
|
1278
1293
|
console.error(`${errorColor}${ICONS.error} ${message}${reset}`);
|
|
1279
1294
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createWriteStream } from 'node:fs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Stream a ReadableStream of raw bytes to a file on disk.
|
|
5
|
+
*
|
|
6
|
+
* This mirrors the pattern used by the deploy fork wrapper to capture child
|
|
7
|
+
* process stdout/stderr without accumulating the output in memory. Returns
|
|
8
|
+
* the total number of bytes written.
|
|
9
|
+
*/
|
|
10
|
+
export async function captureStreamToFile(
|
|
11
|
+
stream: ReadableStream<Uint8Array>,
|
|
12
|
+
filePath: string
|
|
13
|
+
): Promise<number> {
|
|
14
|
+
const writer = createWriteStream(filePath);
|
|
15
|
+
const reader = stream.getReader();
|
|
16
|
+
let totalBytes = 0;
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
while (true) {
|
|
20
|
+
const { done, value } = await reader.read();
|
|
21
|
+
if (done) break;
|
|
22
|
+
|
|
23
|
+
const ok = writer.write(value);
|
|
24
|
+
totalBytes += value.byteLength;
|
|
25
|
+
|
|
26
|
+
// Respect backpressure: wait for drain when the internal buffer is full
|
|
27
|
+
if (!ok) {
|
|
28
|
+
await new Promise<void>((resolve) => writer.once('drain', resolve));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
} finally {
|
|
32
|
+
await new Promise<void>((resolve, reject) => {
|
|
33
|
+
writer.once('error', reject);
|
|
34
|
+
writer.end(resolve);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return totalBytes;
|
|
39
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { writeAndDrain } from '@agentuity/server';
|
|
2
|
+
import type { Logger } from '@agentuity/core';
|
|
3
|
+
import * as tui from '../tui';
|
|
4
|
+
|
|
5
|
+
export interface StreamUrlOptions {
|
|
6
|
+
signal?: AbortSignal;
|
|
7
|
+
follow?: boolean;
|
|
8
|
+
timestamps?: boolean;
|
|
9
|
+
grep?: string;
|
|
10
|
+
tail?: number;
|
|
11
|
+
json?: boolean;
|
|
12
|
+
label?: string;
|
|
13
|
+
raw?: boolean;
|
|
14
|
+
v2?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface StreamUrlResult {
|
|
18
|
+
bytesRead: number;
|
|
19
|
+
chunks: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class StreamFetchError extends Error {
|
|
23
|
+
constructor(
|
|
24
|
+
public status: number,
|
|
25
|
+
public statusText: string,
|
|
26
|
+
message: string
|
|
27
|
+
) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.name = 'StreamFetchError';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function escapeRegExp(str: string): string {
|
|
34
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function streamUrlToWritable(
|
|
38
|
+
url: string,
|
|
39
|
+
writable: NodeJS.WritableStream,
|
|
40
|
+
logger: Logger,
|
|
41
|
+
options: StreamUrlOptions = {}
|
|
42
|
+
): Promise<StreamUrlResult> {
|
|
43
|
+
const {
|
|
44
|
+
signal,
|
|
45
|
+
follow,
|
|
46
|
+
timestamps,
|
|
47
|
+
grep,
|
|
48
|
+
tail,
|
|
49
|
+
json,
|
|
50
|
+
label = 'stream',
|
|
51
|
+
raw = false,
|
|
52
|
+
v2 = false,
|
|
53
|
+
} = options;
|
|
54
|
+
const streamStart = Date.now();
|
|
55
|
+
let bytesRead = 0;
|
|
56
|
+
let chunks = 0;
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const fetchUrl = new URL(url);
|
|
60
|
+
|
|
61
|
+
if (follow || v2) {
|
|
62
|
+
fetchUrl.searchParams.set('v', '2');
|
|
63
|
+
}
|
|
64
|
+
if (follow) {
|
|
65
|
+
fetchUrl.searchParams.set('follow', 'true');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const redactedUrl =
|
|
69
|
+
fetchUrl.origin + fetchUrl.pathname + (fetchUrl.search ? '?REDACTED' : '');
|
|
70
|
+
logger.debug('[%s] fetching: %s', label, redactedUrl);
|
|
71
|
+
const response = await fetch(fetchUrl.href, { signal });
|
|
72
|
+
logger.debug(
|
|
73
|
+
'[%s] response status=%d in %dms',
|
|
74
|
+
label,
|
|
75
|
+
response.status,
|
|
76
|
+
Date.now() - streamStart
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
if (!response.ok || !response.body) {
|
|
80
|
+
logger.debug('[%s] not ok or no body', label);
|
|
81
|
+
if (!json) {
|
|
82
|
+
tui.error(`Failed to fetch stream: ${response.status} ${response.statusText}`);
|
|
83
|
+
}
|
|
84
|
+
throw new StreamFetchError(
|
|
85
|
+
response.status,
|
|
86
|
+
response.statusText,
|
|
87
|
+
`Failed to fetch stream: ${response.status} ${response.statusText}`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const reader = response.body.getReader();
|
|
92
|
+
|
|
93
|
+
if (raw) {
|
|
94
|
+
while (true) {
|
|
95
|
+
const { done, value } = await reader.read();
|
|
96
|
+
if (done) {
|
|
97
|
+
logger.debug(
|
|
98
|
+
'[%s] EOF after %dms (%d chunks, %d bytes)',
|
|
99
|
+
label,
|
|
100
|
+
Date.now() - streamStart,
|
|
101
|
+
chunks,
|
|
102
|
+
bytesRead
|
|
103
|
+
);
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (value) {
|
|
108
|
+
chunks++;
|
|
109
|
+
bytesRead += value.length;
|
|
110
|
+
if (chunks <= 3 || chunks % 100 === 0) {
|
|
111
|
+
logger.debug(
|
|
112
|
+
'[%s] chunk #%d: %d bytes (total: %d bytes, +%dms)',
|
|
113
|
+
label,
|
|
114
|
+
chunks,
|
|
115
|
+
value.length,
|
|
116
|
+
bytesRead,
|
|
117
|
+
Date.now() - streamStart
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
await writeAndDrain(writable, value);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
const decoder = new TextDecoder();
|
|
125
|
+
let leftover = '';
|
|
126
|
+
const grepPattern = grep ? new RegExp(escapeRegExp(grep), 'i') : null;
|
|
127
|
+
const needsFiltering = tail !== undefined || grepPattern !== null;
|
|
128
|
+
const tailBuffer: string[] = [];
|
|
129
|
+
const maxTail = tail ?? Infinity;
|
|
130
|
+
const liveOutput = follow && needsFiltering;
|
|
131
|
+
|
|
132
|
+
const outputLine = async (line: string) => {
|
|
133
|
+
if (json) {
|
|
134
|
+
const obj = {
|
|
135
|
+
timestamp: new Date().toISOString(),
|
|
136
|
+
stream: label,
|
|
137
|
+
message: line,
|
|
138
|
+
};
|
|
139
|
+
await writeAndDrain(writable, Buffer.from(JSON.stringify(obj) + '\n'));
|
|
140
|
+
} else {
|
|
141
|
+
const formatted = timestamps ? formatLineWithTimestamp(line) : line;
|
|
142
|
+
await writeAndDrain(writable, Buffer.from(formatted + '\n'));
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const processFilteredLine = async (line: string) => {
|
|
147
|
+
if (grepPattern && !grepPattern.test(line)) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (tail !== undefined) {
|
|
151
|
+
tailBuffer.push(line);
|
|
152
|
+
if (tailBuffer.length > maxTail) {
|
|
153
|
+
tailBuffer.shift();
|
|
154
|
+
}
|
|
155
|
+
if (liveOutput) {
|
|
156
|
+
await outputLine(line);
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
await outputLine(line);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
while (true) {
|
|
164
|
+
const { done, value } = await reader.read();
|
|
165
|
+
if (done) {
|
|
166
|
+
if (leftover) {
|
|
167
|
+
if (needsFiltering) {
|
|
168
|
+
await processFilteredLine(leftover);
|
|
169
|
+
} else {
|
|
170
|
+
await outputLine(leftover);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
logger.debug(
|
|
174
|
+
'[%s] EOF after %dms (%d chunks, %d bytes)',
|
|
175
|
+
label,
|
|
176
|
+
Date.now() - streamStart,
|
|
177
|
+
chunks,
|
|
178
|
+
bytesRead
|
|
179
|
+
);
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (value) {
|
|
184
|
+
chunks++;
|
|
185
|
+
bytesRead += value.length;
|
|
186
|
+
const text = leftover + decoder.decode(value, { stream: true });
|
|
187
|
+
const lines = text.split('\n');
|
|
188
|
+
leftover = lines.pop() ?? '';
|
|
189
|
+
|
|
190
|
+
for (const line of lines) {
|
|
191
|
+
if (needsFiltering) {
|
|
192
|
+
await processFilteredLine(line);
|
|
193
|
+
} else {
|
|
194
|
+
await outputLine(line);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (!liveOutput && needsFiltering && tailBuffer.length > 0) {
|
|
201
|
+
for (const line of tailBuffer) {
|
|
202
|
+
await outputLine(line);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return { bytesRead, chunks };
|
|
208
|
+
} catch (err) {
|
|
209
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
210
|
+
logger.debug('[%s] aborted after %dms', label, Date.now() - streamStart);
|
|
211
|
+
return { bytesRead, chunks };
|
|
212
|
+
}
|
|
213
|
+
logger.debug('[%s] error after %dms: %s', label, Date.now() - streamStart, err);
|
|
214
|
+
throw err;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function formatLineWithTimestamp(line: string): string {
|
|
219
|
+
const timestamp = new Date().toLocaleTimeString('en-US', {
|
|
220
|
+
hour12: false,
|
|
221
|
+
hour: '2-digit',
|
|
222
|
+
minute: '2-digit',
|
|
223
|
+
second: '2-digit',
|
|
224
|
+
});
|
|
225
|
+
return `${tui.muted(timestamp)} ${line}`;
|
|
226
|
+
}
|
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
|