@sylphx/flow 3.24.1 → 3.25.0
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/CHANGELOG.md +16 -0
- package/package.json +1 -1
- package/src/commands/flow/execute-v2.ts +12 -1
- package/src/commands/sessions-command.ts +87 -0
- package/src/index.ts +2 -0
- package/src/targets/claude-code.ts +32 -16
- package/src/targets/functional/claude-session.ts +155 -0
- package/src/utils/errors.ts +12 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @sylphx/flow
|
|
2
2
|
|
|
3
|
+
## 3.25.0 (2026-02-17)
|
|
4
|
+
|
|
5
|
+
### ✨ Features
|
|
6
|
+
|
|
7
|
+
- **flow:** smart --resume with auto-detection and sessions command ([ce6a3f8](https://github.com/SylphxAI/flow/commit/ce6a3f820c99fe47cb5c6701fc219bd599583470))
|
|
8
|
+
|
|
9
|
+
### 🔧 Chores
|
|
10
|
+
|
|
11
|
+
- fix biome formatting in settings.json and package.json ([02bd051](https://github.com/SylphxAI/flow/commit/02bd051b88d29b645786c8812610400960adbcfe))
|
|
12
|
+
|
|
13
|
+
## 3.24.2 (2026-02-17)
|
|
14
|
+
|
|
15
|
+
### 🐛 Bug Fixes
|
|
16
|
+
|
|
17
|
+
- **flow:** graceful exit when Claude Code exits with non-zero code ([44892b5](https://github.com/SylphxAI/flow/commit/44892b51beda34b49020ed1d0324cfcc3e4b28f9))
|
|
18
|
+
|
|
3
19
|
## 3.24.1 (2026-02-15)
|
|
4
20
|
|
|
5
21
|
### 🐛 Bug Fixes
|
package/package.json
CHANGED
|
@@ -19,7 +19,7 @@ import { TargetInstaller } from '../../services/target-installer.js';
|
|
|
19
19
|
import type { RunCommandOptions } from '../../types.js';
|
|
20
20
|
import { extractAgentInstructions, loadAgentContent } from '../../utils/agent-enhancer.js';
|
|
21
21
|
import { showAttachSummary, showHeader } from '../../utils/display/banner.js';
|
|
22
|
-
import { CLIError, UserCancelledError } from '../../utils/errors.js';
|
|
22
|
+
import { CLIError, ChildProcessExitError, UserCancelledError } from '../../utils/errors.js';
|
|
23
23
|
import { log, promptConfirm, promptSelect } from '../../utils/prompts/index.js';
|
|
24
24
|
import { ensureTargetInstalled, promptForTargetSelection } from '../../utils/target-selection.js';
|
|
25
25
|
import { resolvePrompt } from './prompt.js';
|
|
@@ -340,6 +340,17 @@ export async function executeFlowV2(
|
|
|
340
340
|
process.exit(0);
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
+
// Child process already displayed its own error via inherited stdio —
|
|
344
|
+
// cleanup silently and exit with the same code.
|
|
345
|
+
if (error instanceof ChildProcessExitError) {
|
|
346
|
+
try {
|
|
347
|
+
await executor.cleanup(projectPath);
|
|
348
|
+
} catch (cleanupError) {
|
|
349
|
+
debug('cleanup after child exit failed:', cleanupError);
|
|
350
|
+
}
|
|
351
|
+
process.exit(error.exitCode);
|
|
352
|
+
}
|
|
353
|
+
|
|
343
354
|
console.error(chalk.red('\n Error:'), error);
|
|
344
355
|
|
|
345
356
|
try {
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sessions Command
|
|
3
|
+
*
|
|
4
|
+
* Lists Claude Code sessions for the current project so users can
|
|
5
|
+
* pick one to resume, instead of relying on Claude Code's unreliable picker.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import { Command } from 'commander';
|
|
10
|
+
import { type SessionInfo, listProjectSessions } from '../targets/functional/claude-session.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Format bytes into a human-readable size string.
|
|
14
|
+
*/
|
|
15
|
+
function formatSize(bytes: number): string {
|
|
16
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
17
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
|
|
18
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(0)} MB`;
|
|
19
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Format a date into a relative time string.
|
|
24
|
+
*/
|
|
25
|
+
function formatRelativeTime(date: Date): string {
|
|
26
|
+
const now = Date.now();
|
|
27
|
+
const diffMs = now - date.getTime();
|
|
28
|
+
const diffSec = Math.floor(diffMs / 1000);
|
|
29
|
+
const diffMin = Math.floor(diffSec / 60);
|
|
30
|
+
const diffHour = Math.floor(diffMin / 60);
|
|
31
|
+
const diffDay = Math.floor(diffHour / 24);
|
|
32
|
+
|
|
33
|
+
if (diffSec < 60) return 'just now';
|
|
34
|
+
if (diffMin < 60) return `${diffMin} minute${diffMin === 1 ? '' : 's'} ago`;
|
|
35
|
+
if (diffHour < 24) return `${diffHour} hour${diffHour === 1 ? '' : 's'} ago`;
|
|
36
|
+
if (diffDay === 1) return 'yesterday';
|
|
37
|
+
if (diffDay < 7) return `${diffDay} days ago`;
|
|
38
|
+
if (diffDay < 30) return `${Math.floor(diffDay / 7)} week${Math.floor(diffDay / 7) === 1 ? '' : 's'} ago`;
|
|
39
|
+
return date.toLocaleDateString();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Format a session row for the table output.
|
|
44
|
+
*/
|
|
45
|
+
function formatSessionRow(session: SessionInfo, isLatest: boolean): string {
|
|
46
|
+
const id = chalk.white(session.id);
|
|
47
|
+
const time = formatRelativeTime(session.modifiedAt).padEnd(18);
|
|
48
|
+
const size = formatSize(session.sizeBytes).padStart(8);
|
|
49
|
+
const marker = isLatest ? chalk.green(' \u2190 latest') : '';
|
|
50
|
+
|
|
51
|
+
return ` ${id} ${chalk.dim(time)} ${chalk.dim(size)}${marker}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const sessionsCommand = new Command('sessions')
|
|
55
|
+
.description('List Claude Code sessions for the current project')
|
|
56
|
+
.action(async () => {
|
|
57
|
+
const cwd = process.cwd();
|
|
58
|
+
const sessions = await listProjectSessions(cwd);
|
|
59
|
+
|
|
60
|
+
if (sessions.length === 0) {
|
|
61
|
+
console.log(chalk.yellow('\n No sessions found for this project.\n'));
|
|
62
|
+
console.log(chalk.dim(` Project: ${cwd}`));
|
|
63
|
+
console.log(chalk.dim(' Start a new session with: flow\n'));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log(`\n${chalk.cyan.bold(' Sessions')} ${chalk.dim(`for ${cwd}`)}\n`);
|
|
68
|
+
|
|
69
|
+
// Header
|
|
70
|
+
console.log(
|
|
71
|
+
` ${chalk.dim.bold('ID'.padEnd(36))} ${chalk.dim.bold('Last active'.padEnd(18))} ${chalk.dim.bold('Size'.padStart(8))}`
|
|
72
|
+
);
|
|
73
|
+
console.log(chalk.dim(` ${'─'.repeat(36)} ${'─'.repeat(18)} ${'─'.repeat(8)}`));
|
|
74
|
+
|
|
75
|
+
// Rows
|
|
76
|
+
for (let i = 0; i < sessions.length; i++) {
|
|
77
|
+
console.log(formatSessionRow(sessions[i], i === 0));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Footer hint
|
|
81
|
+
const latestId = sessions[0].id;
|
|
82
|
+
const shortId = latestId.slice(0, 8);
|
|
83
|
+
console.log('');
|
|
84
|
+
console.log(chalk.dim(` Resume latest: ${chalk.white(`flow --resume`)}`));
|
|
85
|
+
console.log(chalk.dim(` Resume by ID: ${chalk.white(`flow --resume ${shortId}`)}`));
|
|
86
|
+
console.log('');
|
|
87
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
upgradeCommand,
|
|
18
18
|
} from './commands/flow-command.js';
|
|
19
19
|
import { hookCommand } from './commands/hook-command.js';
|
|
20
|
+
import { sessionsCommand } from './commands/sessions-command.js';
|
|
20
21
|
import { settingsCommand } from './commands/settings-command.js';
|
|
21
22
|
import { UserCancelledError } from './utils/errors.js';
|
|
22
23
|
|
|
@@ -73,6 +74,7 @@ export function createCLI(): Command {
|
|
|
73
74
|
program.addCommand(doctorCommand);
|
|
74
75
|
program.addCommand(upgradeCommand);
|
|
75
76
|
program.addCommand(hookCommand);
|
|
77
|
+
program.addCommand(sessionsCommand);
|
|
76
78
|
program.addCommand(settingsCommand);
|
|
77
79
|
|
|
78
80
|
return program;
|
|
@@ -11,9 +11,10 @@ import {
|
|
|
11
11
|
pathUtils,
|
|
12
12
|
yamlUtils,
|
|
13
13
|
} from '../utils/config/target-utils.js';
|
|
14
|
-
import { CLIError } from '../utils/errors.js';
|
|
14
|
+
import { CLIError, ChildProcessExitError } from '../utils/errors.js';
|
|
15
15
|
import { sanitize } from '../utils/security/security.js';
|
|
16
16
|
import { DEFAULT_CLAUDE_CODE_ENV } from './functional/claude-code-logic.js';
|
|
17
|
+
import { findLastSessionId } from './functional/claude-session.js';
|
|
17
18
|
import {
|
|
18
19
|
detectTargetConfig,
|
|
19
20
|
stripFrontMatter,
|
|
@@ -32,11 +33,6 @@ interface ClaudeCodeAgentMetadata {
|
|
|
32
33
|
model?: string;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
/** Error with exit code from child process */
|
|
36
|
-
interface ProcessExitError extends Error {
|
|
37
|
-
code: number | null;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
36
|
/** Type guard for Node.js errors with errno/code properties */
|
|
41
37
|
function isNodeError(error: unknown): error is NodeJS.ErrnoException {
|
|
42
38
|
return error instanceof Error && 'code' in error;
|
|
@@ -211,6 +207,24 @@ export const claudeCodeTarget: Target = {
|
|
|
211
207
|
|
|
212
208
|
Please begin your response with a comprehensive summary of all the instructions and context provided above.`;
|
|
213
209
|
|
|
210
|
+
// Resolve session ID for --resume (auto-detect or passthrough)
|
|
211
|
+
let resolvedResumeId: string | undefined;
|
|
212
|
+
if (options.resume) {
|
|
213
|
+
if (typeof options.resume === 'string') {
|
|
214
|
+
resolvedResumeId = options.resume;
|
|
215
|
+
} else {
|
|
216
|
+
// Auto-resolve: find last session for current project
|
|
217
|
+
const lastSession = await findLastSessionId(process.cwd());
|
|
218
|
+
if (lastSession) {
|
|
219
|
+
resolvedResumeId = lastSession;
|
|
220
|
+
if (options.verbose) {
|
|
221
|
+
console.log(chalk.dim(` Resolved session: ${resolvedResumeId}`));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// If null, fall through to bare --resume (Claude's own picker as fallback)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
214
228
|
if (options.dryRun) {
|
|
215
229
|
// Build the command for display
|
|
216
230
|
const dryRunArgs = ['claude', '--dangerously-skip-permissions'];
|
|
@@ -222,8 +236,8 @@ Please begin your response with a comprehensive summary of all the instructions
|
|
|
222
236
|
}
|
|
223
237
|
if (options.resume) {
|
|
224
238
|
dryRunArgs.push('--resume');
|
|
225
|
-
if (
|
|
226
|
-
dryRunArgs.push(
|
|
239
|
+
if (resolvedResumeId) {
|
|
240
|
+
dryRunArgs.push(resolvedResumeId);
|
|
227
241
|
}
|
|
228
242
|
}
|
|
229
243
|
dryRunArgs.push('--system-prompt', '"<agent content>"');
|
|
@@ -255,8 +269,8 @@ Please begin your response with a comprehensive summary of all the instructions
|
|
|
255
269
|
}
|
|
256
270
|
if (options.resume) {
|
|
257
271
|
args.push('--resume');
|
|
258
|
-
if (
|
|
259
|
-
args.push(
|
|
272
|
+
if (resolvedResumeId) {
|
|
273
|
+
args.push(resolvedResumeId);
|
|
260
274
|
}
|
|
261
275
|
}
|
|
262
276
|
|
|
@@ -299,9 +313,9 @@ Please begin your response with a comprehensive summary of all the instructions
|
|
|
299
313
|
if (code === 0) {
|
|
300
314
|
resolve();
|
|
301
315
|
} else {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
reject(
|
|
316
|
+
// Child already displayed its error via inherited stdio — use
|
|
317
|
+
// ChildProcessExitError so upstream handlers exit silently.
|
|
318
|
+
reject(new ChildProcessExitError(code ?? 1));
|
|
305
319
|
}
|
|
306
320
|
});
|
|
307
321
|
|
|
@@ -313,13 +327,15 @@ Please begin your response with a comprehensive summary of all the instructions
|
|
|
313
327
|
});
|
|
314
328
|
});
|
|
315
329
|
} catch (error: unknown) {
|
|
330
|
+
// ChildProcessExitError means the child already printed its own error —
|
|
331
|
+
// let it propagate unmodified so upstream can exit silently.
|
|
332
|
+
if (error instanceof ChildProcessExitError) {
|
|
333
|
+
throw error;
|
|
334
|
+
}
|
|
316
335
|
if (isNodeError(error)) {
|
|
317
336
|
if (error.code === 'ENOENT') {
|
|
318
337
|
throw new CLIError('Claude Code not found. Please install it first.', 'CLAUDE_NOT_FOUND');
|
|
319
338
|
}
|
|
320
|
-
if (error.code !== undefined) {
|
|
321
|
-
throw new CLIError(`Claude Code exited with code ${error.code}`, 'CLAUDE_ERROR');
|
|
322
|
-
}
|
|
323
339
|
throw new CLIError(`Failed to execute Claude Code: ${error.message}`, 'CLAUDE_ERROR');
|
|
324
340
|
}
|
|
325
341
|
if (error instanceof Error) {
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code Session Resolver
|
|
3
|
+
*
|
|
4
|
+
* Resolves session IDs from Claude Code's data files, bypassing the
|
|
5
|
+
* built-in session picker which is unreliable with large project directories.
|
|
6
|
+
*
|
|
7
|
+
* Data model:
|
|
8
|
+
* - ~/.claude/projects/{encoded-path}/ — session .jsonl files (filename = session ID)
|
|
9
|
+
* - ~/.claude/history.jsonl — global log with {timestamp, project, sessionId} per user message
|
|
10
|
+
* - Snapshot-only files have type: "file-history-snapshot" and are tiny (<5KB)
|
|
11
|
+
* - Real sessions have type: "user" messages and are much larger
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from 'node:fs/promises';
|
|
15
|
+
import { open } from 'node:fs/promises';
|
|
16
|
+
import os from 'node:os';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
|
|
19
|
+
/** Minimum file size in bytes to consider a session file "real" (not just a snapshot) */
|
|
20
|
+
const MIN_SESSION_SIZE_BYTES = 2048;
|
|
21
|
+
|
|
22
|
+
/** Number of bytes to read from the tail of history.jsonl */
|
|
23
|
+
const HISTORY_TAIL_BYTES = 65_536;
|
|
24
|
+
|
|
25
|
+
/** Session info returned by listProjectSessions */
|
|
26
|
+
export interface SessionInfo {
|
|
27
|
+
id: string;
|
|
28
|
+
modifiedAt: Date;
|
|
29
|
+
sizeBytes: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Encode a project path the same way Claude Code does.
|
|
34
|
+
* /Users/kyle/flow → -Users-kyle-flow
|
|
35
|
+
*/
|
|
36
|
+
function encodeProjectPath(projectPath: string): string {
|
|
37
|
+
return projectPath.replace(/\//g, '-');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get the Claude Code home directory.
|
|
42
|
+
*/
|
|
43
|
+
function getClaudeHome(): string {
|
|
44
|
+
return path.join(os.homedir(), '.claude');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Find the most recent session ID for a project by reading the tail of history.jsonl.
|
|
49
|
+
*
|
|
50
|
+
* Reads only the last ~64KB of the file (not the full 20MB+ file) for performance.
|
|
51
|
+
* Returns null if no sessions found for the given project path.
|
|
52
|
+
*/
|
|
53
|
+
export async function findLastSessionId(projectPath: string): Promise<string | null> {
|
|
54
|
+
const historyPath = path.join(getClaudeHome(), 'history.jsonl');
|
|
55
|
+
|
|
56
|
+
let fd: Awaited<ReturnType<typeof open>> | null = null;
|
|
57
|
+
try {
|
|
58
|
+
fd = await open(historyPath, 'r');
|
|
59
|
+
const stat = await fd.stat();
|
|
60
|
+
|
|
61
|
+
if (stat.size === 0) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Read the tail of the file
|
|
66
|
+
const readSize = Math.min(HISTORY_TAIL_BYTES, stat.size);
|
|
67
|
+
const offset = stat.size - readSize;
|
|
68
|
+
const buffer = Buffer.alloc(readSize);
|
|
69
|
+
await fd.read(buffer, 0, readSize, offset);
|
|
70
|
+
|
|
71
|
+
const content = buffer.toString('utf-8');
|
|
72
|
+
const lines = content.split('\n').filter(Boolean);
|
|
73
|
+
|
|
74
|
+
// Walk backwards through lines to find the last session for this project
|
|
75
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
76
|
+
const line = lines[i];
|
|
77
|
+
|
|
78
|
+
// Skip partial lines at the start of the buffer (if we started mid-line)
|
|
79
|
+
if (i === 0 && offset > 0) {
|
|
80
|
+
// First line in buffer may be truncated — skip it
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const entry = JSON.parse(line);
|
|
86
|
+
if (entry.sessionId && entry.project === projectPath) {
|
|
87
|
+
return entry.sessionId;
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
// Skip malformed lines
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return null;
|
|
96
|
+
} catch (error) {
|
|
97
|
+
// File doesn't exist or can't be read — no sessions
|
|
98
|
+
if (isNodeError(error) && error.code === 'ENOENT') {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
throw error;
|
|
102
|
+
} finally {
|
|
103
|
+
await fd?.close();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* List all real sessions for a project from the project directory.
|
|
109
|
+
*
|
|
110
|
+
* Reads ~/.claude/projects/{encoded-path}/, stats each .jsonl file,
|
|
111
|
+
* filters out snapshot-only files (< 2KB), returns sorted by mtime descending.
|
|
112
|
+
*/
|
|
113
|
+
export async function listProjectSessions(projectPath: string): Promise<SessionInfo[]> {
|
|
114
|
+
const encoded = encodeProjectPath(projectPath);
|
|
115
|
+
const projectDir = path.join(getClaudeHome(), 'projects', encoded);
|
|
116
|
+
|
|
117
|
+
let entries: Awaited<ReturnType<typeof fs.readdir>>;
|
|
118
|
+
try {
|
|
119
|
+
entries = await fs.readdir(projectDir);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (isNodeError(error) && error.code === 'ENOENT') {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const jsonlFiles = entries.filter((f) => f.endsWith('.jsonl'));
|
|
128
|
+
|
|
129
|
+
// Stat all files in parallel
|
|
130
|
+
const statResults = await Promise.all(
|
|
131
|
+
jsonlFiles.map(async (filename) => {
|
|
132
|
+
const filePath = path.join(projectDir, filename);
|
|
133
|
+
try {
|
|
134
|
+
const stat = await fs.stat(filePath);
|
|
135
|
+
return {
|
|
136
|
+
id: filename.replace(/\.jsonl$/, ''),
|
|
137
|
+
modifiedAt: stat.mtime,
|
|
138
|
+
sizeBytes: stat.size,
|
|
139
|
+
};
|
|
140
|
+
} catch {
|
|
141
|
+
// File may have been deleted between readdir and stat
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
return statResults
|
|
148
|
+
.filter((s): s is SessionInfo => s !== null && s.sizeBytes >= MIN_SESSION_SIZE_BYTES)
|
|
149
|
+
.sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime());
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Type guard for Node.js errors with errno/code properties */
|
|
153
|
+
function isNodeError(error: unknown): error is NodeJS.ErrnoException {
|
|
154
|
+
return error instanceof Error && 'code' in error;
|
|
155
|
+
}
|
package/src/utils/errors.ts
CHANGED
|
@@ -20,3 +20,15 @@ export class CLIError extends Error {
|
|
|
20
20
|
this.name = 'CLIError';
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Child process exited with non-zero code.
|
|
26
|
+
* Since stdio is inherited, the child already displayed its own error —
|
|
27
|
+
* upstream handlers should cleanup and exit silently without printing anything.
|
|
28
|
+
*/
|
|
29
|
+
export class ChildProcessExitError extends Error {
|
|
30
|
+
constructor(public exitCode: number) {
|
|
31
|
+
super(`Child process exited with code ${exitCode}`);
|
|
32
|
+
this.name = 'ChildProcessExitError';
|
|
33
|
+
}
|
|
34
|
+
}
|