-
-
Notifications
You must be signed in to change notification settings - Fork 371
Expand file tree
/
Copy pathcli.ts
More file actions
700 lines (628 loc) · 22.6 KB
/
Copy pathcli.ts
File metadata and controls
700 lines (628 loc) · 22.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
#!/usr/bin/env node
import { createRequire } from "node:module";
import { stdin as input, stdout as output } from "node:process";
import { spawn } from "node:child_process";
import { mkdtempSync, writeFileSync } from "node:fs";
import { readFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import * as prompts from "@clack/prompts";
import { getShellConfig } from "@earendil-works/pi-coding-agent";
import { satisfies } from "semver";
import { loadConfig } from "./config.js";
import { runLocalAgentProvider } from "./local-agent-adapters.js";
import {
isLocalAgentProvider,
loadLocalAgentProfiles,
type LocalAgentProfile,
} from "./local-agent-profiles.js";
import {
assertLocalAgentProviderAvailable,
formatLocalAgentProviderAvailabilitySummary,
} from "./local-agent-availability.js";
import {
formatAvailableLocalAgentTargets,
parseLocalAgentRunArgs,
resolveLocalAgentTarget,
} from "./local-agent-targets.js";
import { createLocalAgentStore, type LocalAgentRecord } from "./local-agent-store.js";
import type { LocalAgentRunResult } from "./local-agent-runtime.js";
import {
ensureDevspaceDefaultSkills,
generateOwnerToken,
loadDevspaceFiles,
resolveSubagentsFlag,
writeDevspaceAuth,
writeDevspaceConfig,
type DevspaceUserConfig,
} from "./user-config.js";
import { expandHomePath } from "./roots.js";
import { shutdownHttpServer } from "./server-shutdown.js";
type Command = "serve" | "init" | "doctor" | "config" | "agents" | "help" | "version";
const require = createRequire(import.meta.url);
const SUPPORTED_NODE_RANGE = ">=20.12 <27";
async function main(argv: string[]): Promise<void> {
assertSupportedNode();
const [rawCommand, ...args] = argv;
const command = normalizeCommand(rawCommand);
switch (command) {
case "serve":
await ensureConfigured();
await serve();
return;
case "init":
await runInit({ force: args.includes("--force") });
return;
case "doctor":
await runDoctor();
return;
case "config":
runConfigCommand(args);
return;
case "agents":
await runAgentsCommand(args);
return;
case "help":
printHelp();
return;
case "version":
printVersion();
return;
}
}
function normalizeCommand(command: string | undefined): Command {
if (!command || command === "serve" || command === "start") return "serve";
if (command === "init" || command === "doctor" || command === "config" || command === "agents") return command;
if (command === "help" || command === "--help" || command === "-h") return "help";
if (command === "version" || command === "--version" || command === "-v") return "version";
throw new Error(`Unknown command: ${command}`);
}
async function ensureConfigured(): Promise<void> {
const files = loadDevspaceFiles();
if (files.configExists && files.authExists) return;
if (process.env.DEVSPACE_OAUTH_OWNER_TOKEN) return;
if (!input.isTTY || !output.isTTY) {
throw new Error(
[
"DevSpace is not configured and this terminal is non-interactive.",
"",
"Run:",
" devspace init",
"",
"Or provide DEVSPACE_OAUTH_OWNER_TOKEN and DEVSPACE_ALLOWED_ROOTS.",
].join("\n"),
);
}
await runInit({ force: false });
}
async function runInit({ force }: { force: boolean }): Promise<void> {
const files = loadDevspaceFiles();
if (!force && files.configExists && files.authExists) {
prompts.log.info(`DevSpace is already configured at ${files.dir}`);
prompts.log.info("Run `devspace init --force` to update it.");
return;
}
try {
prompts.intro("DevSpace setup");
const defaultRoots = files.config.allowedRoots?.join(", ") || process.cwd();
const rootsAnswer = await textPrompt({
message: `Where are your projects located? Press Enter to use ${defaultRoots}`,
placeholder: defaultRoots,
defaultValue: defaultRoots,
validate: (value) => value?.trim() ? undefined : "Enter at least one project root.",
});
const allowedRoots = rootsAnswer
.split(",")
.map((root) => resolve(expandHomePath(root.trim())))
.filter(Boolean);
const defaultPort = String(files.config.port ?? 7676);
const portAnswer = await textPrompt({
message: `Which local port should DevSpace use? Press Enter to use ${defaultPort}`,
placeholder: defaultPort,
defaultValue: defaultPort,
validate: validatePort,
});
const port = Number(portAnswer);
prompts.note(
[
"DevSpace needs a public base URL so ChatGPT or Claude can reach this MCP server.",
"Create a tunnel or reverse proxy with Cloudflare Tunnel, ngrok, Pinggy, Tailscale Funnel, or your own HTTPS proxy.",
"Paste the public origin here, without /mcp.",
"",
"Example: https://your-tunnel-host.example.com",
].join("\n"),
"Public URL required",
);
const publicBaseUrl = normalizePublicBaseUrl(await textPrompt({
message: files.config.publicBaseUrl
? `What is the public base URL? Press Enter to keep ${files.config.publicBaseUrl}`
: "What is the public base URL?",
placeholder: files.config.publicBaseUrl ?? "https://your-tunnel-host.example.com",
defaultValue: files.config.publicBaseUrl ?? "",
validate: validateRequiredPublicBaseUrl,
}));
const config: DevspaceUserConfig = {
host: files.config.host ?? "127.0.0.1",
port,
allowedRoots,
publicBaseUrl,
inlineOutputCharacters: files.config.inlineOutputCharacters,
subagents: resolveSubagentsFlag(files.config),
};
const auth = {
ownerToken: files.auth.ownerToken ?? generateOwnerToken(),
};
const configPath = writeDevspaceConfig(config);
const authPath = writeDevspaceAuth(auth);
const seededSkillPaths = config.subagents ? ensureDevspaceDefaultSkills() : [];
const lines = [
`Config: ${configPath}`,
`Auth: ${authPath}`,
...seededSkillPaths.map((path) => `Default skill: ${path}`),
`Local MCP URL: http://${config.host}:${config.port}/mcp`,
...(publicBaseUrl ? [`Public MCP URL: ${publicBaseUrl}/mcp`] : []),
];
prompts.note(lines.join("\n"), "DevSpace configured");
prompts.note(
[
`Owner password: ${auth.ownerToken}`,
"Use this when ChatGPT or Claude asks you to approve DevSpace access.",
`Stored at: ${authPath}`,
].join("\n"),
"Owner password",
);
prompts.outro("Run `devspace serve` to start the MCP server.");
} catch (error) {
if (error instanceof SetupCancelledError) {
prompts.cancel("Setup cancelled");
return;
}
throw error;
}
}
async function serve(): Promise<void> {
const sqliteStatus = checkSqliteNative();
if (sqliteStatus !== "ok") {
throw new Error(
[
"better-sqlite3 could not load for this Node runtime.",
sqliteStatus,
"",
"Try reinstalling or rebuilding dependencies under the active Node version:",
" npm rebuild better-sqlite3",
].join("\n"),
);
}
const { createServer } = await import("./server.js");
const config = loadConfig();
const { app, close, localAgentProviders } = createServer(config);
const httpServer = app.listen(config.port, config.host, () => {
console.log(`devspace listening on http://${config.host}:${config.port}/mcp`);
console.log(`public base url: ${config.publicBaseUrl}`);
console.log(`allowed roots: ${config.allowedRoots.join(", ")}`);
console.log(`allowed hosts: ${config.allowedHosts.join(", ")}`);
console.log(`inline output characters: ${config.inlineOutputCharacters}`);
if (config.allowedHosts.includes("*")) {
console.warn("warning: Host header allowlist is disabled because DEVSPACE_ALLOWED_HOSTS=*");
}
console.log("auth: Owner password approval required");
console.log(`logging: ${config.logging.level} ${config.logging.format}`);
if (config.subagents) {
console.log(`subagent providers: ${formatLocalAgentProviderAvailabilitySummary(localAgentProviders)}`);
}
});
let shuttingDown = false;
const shutdown = async () => {
if (shuttingDown) return;
shuttingDown = true;
await shutdownHttpServer(httpServer, close);
process.exit(0);
};
const handleShutdown = () => {
void shutdown().catch((error) => {
console.error("devspace shutdown failed", error);
process.exit(1);
});
};
process.once("SIGINT", handleShutdown);
process.once("SIGTERM", handleShutdown);
}
async function runDoctor(): Promise<void> {
const files = loadDevspaceFiles();
console.log(`Config dir: ${files.dir}`);
console.log(`Config file: ${files.configExists ? files.configPath : "missing"}`);
console.log(`Auth file: ${files.authExists ? files.authPath : "missing"}`);
console.log(`Node: ${process.version} (${nodeVersionStatus()})`);
console.log(`Node ABI: ${process.versions.modules}`);
console.log(`Platform: ${process.platform} ${process.arch}`);
console.log(`Git: ${checkGitAvailable()}`);
console.log(`Bash shell: ${checkBashShell()}`);
console.log(`SQLite native dependency: ${checkSqliteNative()}`);
try {
const config = loadConfig();
console.log(`Local MCP URL: http://${config.host}:${config.port}/mcp`);
console.log(`Public MCP URL: ${new URL("/mcp", config.publicBaseUrl).toString()}`);
console.log(`Allowed roots: ${config.allowedRoots.join(", ")}`);
console.log(`Allowed hosts: ${config.allowedHosts.join(", ")}`);
console.log(`Inline output characters: ${config.inlineOutputCharacters}`);
} catch (error) {
console.log(`Config status: ${error instanceof Error ? error.message : String(error)}`);
}
}
function runConfigCommand(args: string[]): void {
const [subcommand, key, ...rest] = args;
const files = loadDevspaceFiles();
if (!subcommand || subcommand === "get") {
console.log(JSON.stringify(files.config, null, 2));
return;
}
if (subcommand !== "set") {
throw new Error(`Unknown config command: ${subcommand}`);
}
if (key !== "publicBaseUrl") {
throw new Error("Only `devspace config set publicBaseUrl <url|null>` is supported right now.");
}
const value = rest.join(" ").trim();
if (!value) {
throw new Error("Missing publicBaseUrl value.");
}
writeDevspaceConfig({
...files.config,
publicBaseUrl: normalizeOptionalPublicBaseUrl(value),
});
console.log(`Updated ${files.configPath}`);
}
function printHelp(): void {
console.log(
[
"DevSpace",
"",
"Usage:",
" devspace Run first-time setup if needed, then start the server",
" devspace serve Start the server",
" devspace init Create or update ~/.devspace/config.json and auth.json",
" devspace doctor Show config, runtime, and native dependency status",
" devspace config get Print persisted config",
" devspace config set publicBaseUrl <url|null>",
" devspace agents ls List subagent sessions",
" devspace agents run <profile-or-provider-or-id> [--model <model>] <prompt>",
" devspace agents show <id>",
" devspace -v, --version Print the installed version",
"",
"For temporary tunnels:",
" DEVSPACE_PUBLIC_BASE_URL=https://example.trycloudflare.com devspace serve",
].join("\n"),
);
}
async function runAgentsCommand(args: string[]): Promise<void> {
const [subcommand, ...rest] = args;
switch (subcommand) {
case "ls":
case "list":
await runAgentsList();
return;
case "run":
await runAgentsRun(rest);
return;
case "show":
await runAgentsShow(rest);
return;
case "__worker":
await runAgentsWorker(rest);
return;
case undefined:
case "help":
case "--help":
case "-h":
printAgentsHelp();
return;
default:
throw new Error(`Unknown agents command: ${subcommand}`);
}
}
async function runAgentsList(): Promise<void> {
const config = loadConfig();
const store = createLocalAgentStore(config);
const agents = store.list(resolveCurrentWorkspaceScope());
if (agents.length === 0) {
console.log("No subagent sessions found for this workspace.");
return;
}
for (const agent of agents) {
console.log(formatAgentLine(agent));
}
}
async function runAgentsRun(args: string[]): Promise<void> {
const parsed = parseLocalAgentRunArgs(args);
const config = loadConfig();
const workspaceRoot = resolveCurrentWorkspaceRoot();
const store = createLocalAgentStore(config);
const existing = store.get(parsed.target);
if (existing) {
if (!isLocalAgentProvider(existing.provider)) {
throw new Error(`Unknown subagent provider for existing session: ${existing.provider}`);
}
assertLocalAgentProviderAvailable(existing.provider);
const promptFile = writeAgentPromptFile(parsed.prompt);
store.update(existing.id, {
status: "starting",
model: parsed.model ?? existing.model,
thinking: parsed.thinking ?? existing.thinking,
latestResponse: undefined,
error: undefined,
});
spawnAgentWorker(existing.id, promptFile);
console.log(formatAgentLine({
...existing,
status: "running",
model: parsed.model ?? existing.model,
thinking: parsed.thinking ?? existing.thinking,
}));
return;
}
const profiles = await loadLocalAgentProfiles(config, workspaceRoot);
const target = resolveLocalAgentTarget(parsed.target, profiles, parsed.model, parsed.thinking);
if (!target) {
throw new Error(
`Unknown subagent profile, provider, or id: ${parsed.target}. Available ${formatAvailableLocalAgentTargets(profiles)}`,
);
}
assertLocalAgentProviderAvailable(target.provider);
const promptFile = writeAgentPromptFile(parsed.prompt);
const record = store.create({
workspaceId: process.env.DEVSPACE_WORKSPACE_ID,
workspaceRoot,
profileName: target.name,
provider: target.provider,
model: target.model,
thinking: target.thinking,
});
spawnAgentWorker(record.id, promptFile);
console.log(formatAgentLine({ ...record, status: "running" }));
}
async function runAgentsShow(args: string[]): Promise<void> {
const [id] = args;
if (!id) throw new Error("Usage: devspace agents show <id>");
const config = loadConfig();
const store = createLocalAgentStore(config);
let record = store.get(id);
if (!record) throw new Error(`Unknown subagent id: ${id}`);
const deadline = Date.now() + 15_000;
while ((record.status === "starting" || record.status === "running") && Date.now() < deadline) {
await sleep(500);
record = store.get(id) ?? record;
}
console.log(formatAgentLine(record));
if (record.latestResponse) {
console.log(record.latestResponse);
return;
}
if (record.error) {
console.log(record.error);
return;
}
if (record.status === "starting" || record.status === "running") {
console.log(`No final response yet. Call \`devspace agents show ${record.id}\` again later.`);
}
}
async function runAgentsWorker(args: string[]): Promise<void> {
const [id, promptFileFlag, promptFile] = args;
if (!id || promptFileFlag !== "--prompt-file" || !promptFile) {
throw new Error("Usage: devspace agents __worker <id> --prompt-file <path>");
}
const config = loadConfig();
const store = createLocalAgentStore(config);
const record = store.get(id);
if (!record) throw new Error(`Unknown subagent id: ${id}`);
store.update(record.id, { status: "running", error: undefined });
try {
const profiles = await loadLocalAgentProfiles(config, record.workspaceRoot);
const profile = profiles.find((candidate) => candidate.name === record.profileName);
const prompt = await readFile(promptFile, "utf8");
const result = profile
? await runLocalAgentProfile(profile, record, prompt)
: await runRawLocalAgentProvider(record, prompt);
store.update(record.id, {
providerSessionId: result.providerSessionId ?? undefined,
status: "idle",
latestResponse: result.finalResponse,
error: undefined,
});
} catch (error) {
store.update(record.id, {
status: "error",
error: error instanceof Error ? error.message : String(error),
});
}
}
async function runLocalAgentProfile(
profile: LocalAgentProfile,
record: LocalAgentRecord,
prompt: string,
): Promise<LocalAgentRunResult> {
const body = profile.body.trim();
const fullPrompt = body ? `${body}\n\nTask:\n${prompt}` : prompt;
return runLocalAgentProvider(profile.provider, {
prompt: fullPrompt,
workspace: record.workspaceRoot,
providerSessionId: record.providerSessionId,
writeMode: "allowed",
model: record.model ?? profile.model,
thinking: record.thinking ?? profile.thinking,
});
}
async function runRawLocalAgentProvider(
record: LocalAgentRecord,
prompt: string,
): Promise<LocalAgentRunResult> {
if (record.profileName !== record.provider || !isLocalAgentProvider(record.provider)) {
throw new Error(`Subagent profile not found: ${record.profileName}`);
}
return runLocalAgentProvider(record.provider, {
prompt,
workspace: record.workspaceRoot,
providerSessionId: record.providerSessionId,
writeMode: "allowed",
model: record.model,
thinking: record.thinking,
});
}
function spawnAgentWorker(agentId: string, promptFile: string): void {
const child = spawn(process.execPath, [
...process.execArgv,
fileURLToPath(import.meta.url),
"agents",
"__worker",
agentId,
"--prompt-file",
promptFile,
], {
detached: true,
stdio: "ignore",
env: process.env,
});
child.unref();
}
function writeAgentPromptFile(prompt: string): string {
const directory = mkdtempSync(join(tmpdir(), "devspace-agent-prompt-"));
const filePath = join(directory, "prompt.txt");
writeFileSync(filePath, prompt, { mode: 0o600 });
return filePath;
}
function resolveCurrentWorkspaceRoot(): string {
return resolve(process.env.DEVSPACE_WORKSPACE_ROOT || process.cwd());
}
function resolveCurrentWorkspaceScope(): { workspaceId?: string; workspaceRoot: string } {
return {
workspaceId: process.env.DEVSPACE_WORKSPACE_ID,
workspaceRoot: resolveCurrentWorkspaceRoot(),
};
}
function formatAgentLine(agent: Pick<
LocalAgentRecord,
"id" | "status" | "profileName" | "provider" | "model" | "thinking"
>): string {
const model = agent.model ? ` ${agent.model}` : "";
const thinking = agent.thinking ? ` thinking=${agent.thinking}` : "";
return `${agent.id} ${agent.status} ${agent.profileName} ${agent.provider}${model}${thinking}`;
}
function sleep(ms: number): Promise<void> {
return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
}
function printAgentsHelp(): void {
console.log(
[
"DevSpace agents",
"",
"Usage:",
" devspace agents ls",
" devspace agents run <profile-or-provider-or-id> [--model <model>] [--thinking <level>] <prompt>",
" devspace agents show <id>",
].join("\n"),
);
}
function printVersion(): void {
const packageJson = require("../package.json") as { version?: unknown };
if (typeof packageJson.version !== "string") {
throw new Error("Unable to read DevSpace package version.");
}
console.log(packageJson.version);
}
function normalizeOptionalPublicBaseUrl(value: string): string | null {
const trimmed = value.trim();
if (!trimmed || trimmed === "null" || trimmed === "none") return null;
return normalizePublicBaseUrl(trimmed);
}
function normalizePublicBaseUrl(value: string): string {
const trimmed = value.trim();
const parsed = new URL(trimmed);
parsed.hash = "";
parsed.search = "";
parsed.pathname = parsed.pathname.replace(/\/+$/, "");
return parsed.toString().replace(/\/$/, "");
}
type TextPromptOptions = Omit<Parameters<typeof prompts.text>[0], "validate"> & {
defaultValue: string;
validate?: (value: string | undefined) => string | Error | undefined;
};
async function textPrompt(options: TextPromptOptions): Promise<string> {
const result = await prompts.text({
...options,
validate: (value) => options.validate?.(value?.trim() ? value : options.defaultValue),
});
if (prompts.isCancel(result)) throw new SetupCancelledError();
const value = String(result).trim();
return value || options.defaultValue;
}
function validatePort(value: string | undefined): string | undefined {
const port = Number(value);
return Number.isInteger(port) && port >= 1 && port <= 65535
? undefined
: "Enter a port between 1 and 65535.";
}
function validateRequiredPublicBaseUrl(value: string | undefined): string | undefined {
const trimmed = value?.trim() ?? "";
if (!trimmed) return "Enter the public URL from your tunnel or reverse proxy.";
if (trimmed.endsWith("/mcp")) return "Enter the base URL only, without /mcp.";
return validatePublicBaseUrl(trimmed);
}
function validatePublicBaseUrl(value: string): string | undefined {
try {
const parsed = new URL(value);
return parsed.protocol === "http:" || parsed.protocol === "https:"
? undefined
: "Use an http or https URL.";
} catch {
return "Enter a valid URL, for example https://your-tunnel-host.example.com.";
}
}
function assertSupportedNode(): void {
if (satisfies(process.versions.node, SUPPORTED_NODE_RANGE)) return;
throw new Error(
[
`DevSpace requires Node ${SUPPORTED_NODE_RANGE}.`,
`Current Node: ${process.version}`,
"",
"Install Node 22 LTS or use a version manager such as nvm, fnm, or mise.",
].join("\n"),
);
}
function nodeVersionStatus(): string {
return satisfies(process.versions.node, SUPPORTED_NODE_RANGE)
? `supported ${SUPPORTED_NODE_RANGE}`
: `unsupported, requires ${SUPPORTED_NODE_RANGE}`;
}
class SetupCancelledError extends Error {}
function checkSqliteNative(): string {
try {
const Database = require("better-sqlite3") as typeof import("better-sqlite3");
const db = new Database(":memory:");
db.close();
return "ok";
} catch (error) {
return error instanceof Error ? error.message : String(error);
}
}
function checkGitAvailable(): string {
try {
const { execFileSync } = require("node:child_process") as typeof import("node:child_process");
return execFileSync("git", ["--version"], { encoding: "utf8" }).trim();
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return `unavailable (${message})`;
}
}
function checkBashShell(): string {
try {
const { shell, args } = getShellConfig();
return `${shell} ${args.join(" ")}`;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return `unavailable (${message})`;
}
}
main(process.argv.slice(2)).catch((error) => {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
});