@indigo-gr/claude-code-station 0.3.3 → 0.5.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 +71 -0
- package/LICENSE +1 -1
- package/README.md +89 -5
- package/bin/ccs +739 -30
- package/bin/ccs-config-edit.ts +431 -0
- package/bin/ccs-config.ts +324 -44
- package/bin/ccs-delete.sh +2 -1
- package/bin/ccs-doctor.ts +432 -0
- package/bin/ccs-groups.ts +167 -0
- package/bin/ccs-init.ts +58 -14
- package/bin/ccs-list-json.ts +124 -0
- package/bin/ccs-list.ts +121 -10
- package/bin/ccs-machine.ts +467 -0
- package/bin/ccs-open.ts +304 -0
- package/bin/ccs-preview-session.ts +14 -5
- package/bin/ccs-preview.ts +75 -6
- package/bin/ccs-repo.ts +433 -0
- package/bin/ccs-sanitize.ts +18 -4
- package/bin/ccs-scan.ts +81 -10
- package/bin/ccs-sessions.ts +174 -0
- package/bin/ccs-tsx.sh +16 -0
- package/bin/ccs-update-check.ts +131 -0
- package/bin/ccs-utils.ts +5 -0
- package/bin/package.json +3 -0
- package/docs/design/machine-interface.md +860 -0
- package/docs/design/repos-yml-schema.md +225 -76
- package/docs/v0.2.0-review-notes.md +1 -1
- package/docs/v0.2.1-backlog.md +2 -2
- package/package.json +4 -3
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ccs-machine.ts — the machine interface's common ground.
|
|
3
|
+
*
|
|
4
|
+
* GUI launchers, scripts and other tools call ccs without a terminal:
|
|
5
|
+
* `ccs doctor`, `ccs list --json`, `ccs sessions --json`, `ccs repo
|
|
6
|
+
* add|remove`, `ccs config get|set` and `ccs open`. Every one of them shares what lives here: the outer JSON object,
|
|
7
|
+
* the exit codes and their status words, the two reason vocabularies, flag
|
|
8
|
+
* parsing, and the SIGINT rule. The contract is docs/design/machine-interface.md
|
|
9
|
+
* (chapter 1); change that document before changing the shapes here.
|
|
10
|
+
*
|
|
11
|
+
* Run as a script, this module dispatches every machine word but `doctor`
|
|
12
|
+
* (bin/ccs runs bin/ccs-doctor.ts itself, in two stages):
|
|
13
|
+
*
|
|
14
|
+
* tsx ccs-machine.ts list --json [--with-paths]
|
|
15
|
+
* tsx ccs-machine.ts sessions --json [--project <name>] [--limit <n>] …
|
|
16
|
+
* tsx ccs-machine.ts repo add|remove … config get|set …
|
|
17
|
+
* tsx ccs-machine.ts open … (through bin/ccs: it starts the command)
|
|
18
|
+
*
|
|
19
|
+
* None of these commands opens fzf, reads stdin, asks anything, goes to the
|
|
20
|
+
* network, or creates the config template. Only `repo add` may create
|
|
21
|
+
* repos.yml, and only `sessions` (its scan) writes the cache.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { readFileSync } from "node:fs";
|
|
25
|
+
import { dirname, join } from "node:path";
|
|
26
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
27
|
+
|
|
28
|
+
import { ConfigError } from "./ccs-config.ts";
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Shapes
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
/** The machine interface's own version: minor = additions, major = breaks. */
|
|
35
|
+
export const SCHEMA_VERSION = "1.0.0";
|
|
36
|
+
|
|
37
|
+
export const PRODUCT_ID = "ccs";
|
|
38
|
+
|
|
39
|
+
/** The package whose package.json may name the version (as bin/ccs reads it). */
|
|
40
|
+
const CCS_PACKAGE = "@indigo-gr/claude-code-station";
|
|
41
|
+
|
|
42
|
+
/** Status words, one per exit code (machine-interface.md 1.4). */
|
|
43
|
+
export type Status =
|
|
44
|
+
| "ok"
|
|
45
|
+
| "failed"
|
|
46
|
+
| "usage"
|
|
47
|
+
| "unverifiable"
|
|
48
|
+
| "missing_prerequisite"
|
|
49
|
+
| "not_found"
|
|
50
|
+
| "rejected"
|
|
51
|
+
| "partial"
|
|
52
|
+
| "interrupted";
|
|
53
|
+
|
|
54
|
+
export const EXIT_CODE: Readonly<Record<Status, number>> = {
|
|
55
|
+
ok: 0,
|
|
56
|
+
failed: 1,
|
|
57
|
+
usage: 2,
|
|
58
|
+
unverifiable: 3,
|
|
59
|
+
missing_prerequisite: 4,
|
|
60
|
+
not_found: 5,
|
|
61
|
+
rejected: 6,
|
|
62
|
+
partial: 7,
|
|
63
|
+
interrupted: 130,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export type Vocab = "ccs.health" | "ccs.cli";
|
|
67
|
+
|
|
68
|
+
/** Reason words of every command but doctor (machine-interface.md 1.5). */
|
|
69
|
+
export type CliCode =
|
|
70
|
+
| "ok"
|
|
71
|
+
| "unknown_option"
|
|
72
|
+
| "missing_argument"
|
|
73
|
+
| "extra_argument"
|
|
74
|
+
| "invalid_argument"
|
|
75
|
+
| "json_required"
|
|
76
|
+
| "conflicting_options"
|
|
77
|
+
| "config_not_readable"
|
|
78
|
+
| "cache_not_readable"
|
|
79
|
+
| "config_missing"
|
|
80
|
+
| "config_invalid"
|
|
81
|
+
| "cache_missing"
|
|
82
|
+
| "no_tty"
|
|
83
|
+
| "unsupported_os"
|
|
84
|
+
| "terminal_not_set"
|
|
85
|
+
| "terminal_app_missing"
|
|
86
|
+
| "unsafe_install_path"
|
|
87
|
+
| "dependency_missing"
|
|
88
|
+
| "repo_not_found"
|
|
89
|
+
| "session_not_found"
|
|
90
|
+
| "path_outside_home"
|
|
91
|
+
| "path_not_found"
|
|
92
|
+
| "path_not_directory"
|
|
93
|
+
| "path_taken"
|
|
94
|
+
| "name_taken"
|
|
95
|
+
| "unsafe_characters"
|
|
96
|
+
| "needs_quoting"
|
|
97
|
+
| "invalid_value"
|
|
98
|
+
| "invalid_uuid"
|
|
99
|
+
| "key_not_settable"
|
|
100
|
+
| "last_entry"
|
|
101
|
+
| "flow_style_repos"
|
|
102
|
+
| "not_launchable"
|
|
103
|
+
| "repo_disabled"
|
|
104
|
+
| "session_cwd_unknown"
|
|
105
|
+
| "command_unsafe"
|
|
106
|
+
| "config_changed"
|
|
107
|
+
| "config_locked"
|
|
108
|
+
| "write_failed"
|
|
109
|
+
| "validation_after_write_failed"
|
|
110
|
+
| "restore_failed"
|
|
111
|
+
| "launch_failed"
|
|
112
|
+
| "invalid_package_config"
|
|
113
|
+
| "internal_error"
|
|
114
|
+
| "interrupted";
|
|
115
|
+
|
|
116
|
+
/** The status each ccs.cli word goes with (the table in 1.5). */
|
|
117
|
+
export const CLI_CODE_STATUS: Readonly<Record<CliCode, Status>> = {
|
|
118
|
+
ok: "ok",
|
|
119
|
+
unknown_option: "usage",
|
|
120
|
+
missing_argument: "usage",
|
|
121
|
+
extra_argument: "usage",
|
|
122
|
+
invalid_argument: "usage",
|
|
123
|
+
json_required: "usage",
|
|
124
|
+
conflicting_options: "usage",
|
|
125
|
+
config_not_readable: "unverifiable",
|
|
126
|
+
cache_not_readable: "unverifiable",
|
|
127
|
+
config_missing: "missing_prerequisite",
|
|
128
|
+
config_invalid: "missing_prerequisite",
|
|
129
|
+
cache_missing: "missing_prerequisite",
|
|
130
|
+
no_tty: "missing_prerequisite",
|
|
131
|
+
unsupported_os: "missing_prerequisite",
|
|
132
|
+
terminal_not_set: "missing_prerequisite",
|
|
133
|
+
terminal_app_missing: "missing_prerequisite",
|
|
134
|
+
unsafe_install_path: "missing_prerequisite",
|
|
135
|
+
dependency_missing: "missing_prerequisite",
|
|
136
|
+
repo_not_found: "not_found",
|
|
137
|
+
session_not_found: "not_found",
|
|
138
|
+
path_outside_home: "rejected",
|
|
139
|
+
path_not_found: "rejected",
|
|
140
|
+
path_not_directory: "rejected",
|
|
141
|
+
path_taken: "rejected",
|
|
142
|
+
name_taken: "rejected",
|
|
143
|
+
unsafe_characters: "rejected",
|
|
144
|
+
needs_quoting: "rejected",
|
|
145
|
+
invalid_value: "rejected",
|
|
146
|
+
invalid_uuid: "rejected",
|
|
147
|
+
key_not_settable: "rejected",
|
|
148
|
+
last_entry: "rejected",
|
|
149
|
+
flow_style_repos: "rejected",
|
|
150
|
+
not_launchable: "rejected",
|
|
151
|
+
repo_disabled: "rejected",
|
|
152
|
+
session_cwd_unknown: "rejected",
|
|
153
|
+
command_unsafe: "rejected",
|
|
154
|
+
config_changed: "failed",
|
|
155
|
+
config_locked: "failed",
|
|
156
|
+
write_failed: "failed",
|
|
157
|
+
validation_after_write_failed: "failed",
|
|
158
|
+
restore_failed: "partial",
|
|
159
|
+
launch_failed: "failed",
|
|
160
|
+
invalid_package_config: "failed",
|
|
161
|
+
internal_error: "failed",
|
|
162
|
+
interrupted: "interrupted",
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
export interface Reason {
|
|
166
|
+
vocab: Vocab;
|
|
167
|
+
code: string;
|
|
168
|
+
/** config_invalid: the config check's word (ConfigErrorCode). */
|
|
169
|
+
detail?: string;
|
|
170
|
+
/** config_invalid: index into repos[] when one entry is at fault. */
|
|
171
|
+
entry_index?: number;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** The outer object every command prints (1.3), in this key order. */
|
|
175
|
+
export interface Envelope {
|
|
176
|
+
schema_version: string;
|
|
177
|
+
product_id: string;
|
|
178
|
+
product_version: string;
|
|
179
|
+
command: string;
|
|
180
|
+
written_at: string;
|
|
181
|
+
status: Status;
|
|
182
|
+
reasons: Reason[];
|
|
183
|
+
[key: string]: unknown;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** What a command decided: its exit status, reasons and own keys. */
|
|
187
|
+
export interface Outcome {
|
|
188
|
+
status: Status;
|
|
189
|
+
reasons: Reason[];
|
|
190
|
+
/** Keys after `reasons` (only on success, per 1.3). */
|
|
191
|
+
body?: Record<string, unknown>;
|
|
192
|
+
/** One line for people on stderr (never a repo path or name). */
|
|
193
|
+
note?: string;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
// Product version
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
|
|
200
|
+
const VERSION_RE = /^[0-9A-Za-z.+-]{1,32}$/;
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* The version `ccs --version` prints: package.json next to bin/, only when it
|
|
204
|
+
* is this package's; `0.0.0-dev` otherwise (copy install, unreadable file).
|
|
205
|
+
* Same rule as bin/ccs's read_package_version.
|
|
206
|
+
*/
|
|
207
|
+
export function productVersion(binDir: string = BIN_DIR): string {
|
|
208
|
+
try {
|
|
209
|
+
const raw = JSON.parse(readFileSync(join(binDir, "..", "package.json"), "utf-8"));
|
|
210
|
+
if (
|
|
211
|
+
raw &&
|
|
212
|
+
typeof raw === "object" &&
|
|
213
|
+
raw.name === CCS_PACKAGE &&
|
|
214
|
+
typeof raw.version === "string" &&
|
|
215
|
+
VERSION_RE.test(raw.version)
|
|
216
|
+
) {
|
|
217
|
+
return raw.version;
|
|
218
|
+
}
|
|
219
|
+
} catch {
|
|
220
|
+
// fall through
|
|
221
|
+
}
|
|
222
|
+
return "0.0.0-dev";
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const BIN_DIR = dirname(fileURLToPath(import.meta.url));
|
|
226
|
+
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
// Building and printing
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
export function envelope(
|
|
232
|
+
command: string,
|
|
233
|
+
status: Status,
|
|
234
|
+
reasons: Reason[],
|
|
235
|
+
body: Record<string, unknown> = {},
|
|
236
|
+
): Envelope {
|
|
237
|
+
return {
|
|
238
|
+
schema_version: SCHEMA_VERSION,
|
|
239
|
+
product_id: PRODUCT_ID,
|
|
240
|
+
product_version: productVersion(),
|
|
241
|
+
command,
|
|
242
|
+
written_at: new Date().toISOString(),
|
|
243
|
+
status,
|
|
244
|
+
reasons,
|
|
245
|
+
...body,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** One ccs.cli reason, its status taken from the table. */
|
|
250
|
+
export function cliOutcome(
|
|
251
|
+
code: CliCode,
|
|
252
|
+
extra: Partial<Reason> = {},
|
|
253
|
+
note?: string,
|
|
254
|
+
): Outcome {
|
|
255
|
+
return {
|
|
256
|
+
status: CLI_CODE_STATUS[code],
|
|
257
|
+
reasons: [{ vocab: "ccs.cli", code, ...extra }],
|
|
258
|
+
note,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function okOutcome(body: Record<string, unknown>): Outcome {
|
|
263
|
+
return { status: "ok", reasons: [{ vocab: "ccs.cli", code: "ok" }], body };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Print an outcome and return its exit code. With --json, stdout gets exactly
|
|
268
|
+
* one JSON object and a newline, on success and on failure alike; the body is
|
|
269
|
+
* printed only on success (1.3). The note goes to stderr either way.
|
|
270
|
+
*/
|
|
271
|
+
/** One answer per process: set once emit() has run. */
|
|
272
|
+
let answered = false;
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* True once this process has given its answer. SIGINT handlers check it: tsx
|
|
276
|
+
* passes a SIGINT on to the module's process, which then gets it twice (from
|
|
277
|
+
* the terminal's process group and from tsx), and the second one must not
|
|
278
|
+
* print a second JSON object (1.8: exactly one).
|
|
279
|
+
*/
|
|
280
|
+
export function hasAnswered(): boolean {
|
|
281
|
+
return answered;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function emit(command: string, outcome: Outcome, json: boolean): number {
|
|
285
|
+
if (answered) return EXIT_CODE[outcome.status];
|
|
286
|
+
answered = true;
|
|
287
|
+
if (json) {
|
|
288
|
+
const body = outcome.status === "ok" ? outcome.body : undefined;
|
|
289
|
+
process.stdout.write(
|
|
290
|
+
JSON.stringify(envelope(command, outcome.status, outcome.reasons, body)) + "\n",
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
if (outcome.note) process.stderr.write(`[ccs] ${outcome.note}\n`);
|
|
294
|
+
else if (outcome.status !== "ok") {
|
|
295
|
+
const words = outcome.reasons
|
|
296
|
+
.map((r) => (r.detail ? `${r.code} (${r.detail})` : r.code))
|
|
297
|
+
.join(", ");
|
|
298
|
+
process.stderr.write(`[ccs] ${command}: ${outcome.status}: ${words}\n`);
|
|
299
|
+
}
|
|
300
|
+
return EXIT_CODE[outcome.status];
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* The reason for a repos.yml that could not be used (1.5): no file →
|
|
305
|
+
* config_missing (4), no permission → config_not_readable (3), anything the
|
|
306
|
+
* checks refuse → config_invalid (4) with the check's word in `detail`.
|
|
307
|
+
*/
|
|
308
|
+
export function configFailure(err: unknown): Outcome {
|
|
309
|
+
if (err instanceof ConfigError) {
|
|
310
|
+
if (err.code === "config_not_readable") {
|
|
311
|
+
return cliOutcome("config_not_readable", {}, "cannot read repos.yml (config_not_readable)");
|
|
312
|
+
}
|
|
313
|
+
const extra: Partial<Reason> = { detail: err.code };
|
|
314
|
+
if (err.entryIndex !== undefined) extra.entry_index = err.entryIndex;
|
|
315
|
+
return cliOutcome(
|
|
316
|
+
"config_invalid",
|
|
317
|
+
extra,
|
|
318
|
+
`repos.yml fails ccs's checks (config_invalid: ${err.code}` +
|
|
319
|
+
(err.entryIndex !== undefined ? `, repos[${err.entryIndex}]` : "") +
|
|
320
|
+
"); run `ccs doctor` or open the list to see the message",
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
return cliOutcome("internal_error");
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ---------------------------------------------------------------------------
|
|
327
|
+
// Flags
|
|
328
|
+
// ---------------------------------------------------------------------------
|
|
329
|
+
|
|
330
|
+
export interface FlagSpec {
|
|
331
|
+
/** Flags without a value (`--json`). */
|
|
332
|
+
booleans: readonly string[];
|
|
333
|
+
/** Flags taking a value: `--limit 5` or `--limit=5`. */
|
|
334
|
+
values?: readonly string[];
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export type ParsedFlags =
|
|
338
|
+
| {
|
|
339
|
+
ok: true;
|
|
340
|
+
booleans: Set<string>;
|
|
341
|
+
values: Map<string, string>;
|
|
342
|
+
positionals: string[];
|
|
343
|
+
/** Everything after a bare `--`, untouched. */
|
|
344
|
+
rest: string[];
|
|
345
|
+
}
|
|
346
|
+
| { ok: false; code: "unknown_option" | "missing_argument" };
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Parse a command's arguments against its flags. Unknown flags are an error
|
|
350
|
+
* (unknown_option) — the machine interface never passes flags on, except
|
|
351
|
+
* after a bare `--`.
|
|
352
|
+
*/
|
|
353
|
+
export function parseFlags(argv: readonly string[], spec: FlagSpec): ParsedFlags {
|
|
354
|
+
const booleans = new Set<string>();
|
|
355
|
+
const values = new Map<string, string>();
|
|
356
|
+
const positionals: string[] = [];
|
|
357
|
+
const valueFlags = new Set(spec.values ?? []);
|
|
358
|
+
for (let i = 0; i < argv.length; i++) {
|
|
359
|
+
const a = argv[i];
|
|
360
|
+
if (a === "--") {
|
|
361
|
+
return { ok: true, booleans, values, positionals, rest: argv.slice(i + 1) };
|
|
362
|
+
}
|
|
363
|
+
if (!a.startsWith("-") || a === "-") {
|
|
364
|
+
positionals.push(a);
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
const eq = a.indexOf("=");
|
|
368
|
+
const name = eq === -1 ? a : a.slice(0, eq);
|
|
369
|
+
if (spec.booleans.includes(name) && eq === -1) {
|
|
370
|
+
booleans.add(name);
|
|
371
|
+
} else if (valueFlags.has(name)) {
|
|
372
|
+
if (eq !== -1) {
|
|
373
|
+
values.set(name, a.slice(eq + 1));
|
|
374
|
+
} else if (i + 1 < argv.length) {
|
|
375
|
+
values.set(name, argv[++i]);
|
|
376
|
+
} else {
|
|
377
|
+
return { ok: false, code: "missing_argument" };
|
|
378
|
+
}
|
|
379
|
+
} else {
|
|
380
|
+
return { ok: false, code: "unknown_option" };
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return { ok: true, booleans, values, positionals, rest: [] };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** True when `--json` is among the arguments (before a bare `--`). */
|
|
387
|
+
export function wantsJson(argv: readonly string[]): boolean {
|
|
388
|
+
for (const a of argv) {
|
|
389
|
+
if (a === "--") return false;
|
|
390
|
+
if (a === "--json") return true;
|
|
391
|
+
}
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// ---------------------------------------------------------------------------
|
|
396
|
+
// SIGINT
|
|
397
|
+
// ---------------------------------------------------------------------------
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* On SIGINT: run `cleanup` (undo a half-done write, drop a lock), print the
|
|
401
|
+
* interrupted outcome and exit 130 (1.4). The read-only commands have nothing
|
|
402
|
+
* to undo, but still answer with the outer object.
|
|
403
|
+
*/
|
|
404
|
+
export function onInterrupt(command: string, json: boolean, cleanup?: () => void): void {
|
|
405
|
+
process.on("SIGINT", () => {
|
|
406
|
+
if (answered) return;
|
|
407
|
+
try {
|
|
408
|
+
cleanup?.();
|
|
409
|
+
} finally {
|
|
410
|
+
process.exitCode = emit(command, cliOutcome("interrupted"), json);
|
|
411
|
+
process.exit();
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// ---------------------------------------------------------------------------
|
|
417
|
+
// Dispatch
|
|
418
|
+
// ---------------------------------------------------------------------------
|
|
419
|
+
|
|
420
|
+
/** First-argument words bin/ccs hands to the machine interface. */
|
|
421
|
+
export const MACHINE_WORDS = ["doctor", "list", "sessions", "repo", "config", "open"] as const;
|
|
422
|
+
|
|
423
|
+
async function main(argv: string[]): Promise<number> {
|
|
424
|
+
const [word, ...args] = argv;
|
|
425
|
+
const json = wantsJson(args);
|
|
426
|
+
switch (word) {
|
|
427
|
+
case "list": {
|
|
428
|
+
const { runList } = await import("./ccs-list-json.ts");
|
|
429
|
+
return runList(args);
|
|
430
|
+
}
|
|
431
|
+
case "sessions": {
|
|
432
|
+
const { sessionsOutcome } = await import("./ccs-sessions.ts");
|
|
433
|
+
onInterrupt("sessions", json);
|
|
434
|
+
return emit("sessions", await sessionsOutcome(args), json);
|
|
435
|
+
}
|
|
436
|
+
case "repo":
|
|
437
|
+
case "config": {
|
|
438
|
+
const { runRepoOrConfig } = await import("./ccs-repo.ts");
|
|
439
|
+
return runRepoOrConfig(word, args);
|
|
440
|
+
}
|
|
441
|
+
case "open": {
|
|
442
|
+
const { runOpen } = await import("./ccs-open.ts");
|
|
443
|
+
return runOpen(args);
|
|
444
|
+
}
|
|
445
|
+
default:
|
|
446
|
+
return emit(
|
|
447
|
+
String(word ?? ""),
|
|
448
|
+
cliOutcome("internal_error", {}, "unknown machine command"),
|
|
449
|
+
json,
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// pathToFileURL, not `file://${argv[1]}`: a folder with a space (or any
|
|
455
|
+
// character a URL escapes) in its name would never match.
|
|
456
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
|
457
|
+
const argv = process.argv.slice(2);
|
|
458
|
+
const command = argv[0] ?? "";
|
|
459
|
+
const json = wantsJson(argv.slice(1));
|
|
460
|
+
main(argv)
|
|
461
|
+
.then((code) => {
|
|
462
|
+
process.exitCode = code;
|
|
463
|
+
})
|
|
464
|
+
.catch(() => {
|
|
465
|
+
process.exitCode = emit(command, cliOutcome("internal_error"), json);
|
|
466
|
+
});
|
|
467
|
+
}
|