OXYGENHUB
# 94e8677a5439 spectra-vscode / src / cli.ts
3.2 KiB · last changed 0.1.1: find claude in ~/.local/bin when VS Code's PATH lacks it; offer an agent after registering a folder 1 day ago source blame history raw
1
// Running the spectra program: setup commands, and the bridge.
2
import * as cp from "node:child_process";
3
import * as fs from "node:fs";
4
import * as os from "node:os";
5
import * as path from "node:path";
6
import * as vscode from "vscode";
7
8
export interface Ran {
9
  code: number;
10
  stdout: string;
11
  stderr: string;
12
}
13
14
/** The spectra program: the setting, else PATH, else ~/.local/bin. */
15
export function binary(): string | undefined {
16
  const set = vscode.workspace.getConfiguration("spectra").get<string>("path")?.trim();
17
  if (set) return set.replace(/^~(?=\/)/, os.homedir());
18
  for (const dir of (process.env.PATH ?? "").split(path.delimiter)) {
19
    const p = path.join(dir, "spectra");
20
    if (dir && fs.existsSync(p)) return p;
21
  }
22
  const local = path.join(os.homedir(), ".local", "bin", "spectra");
23
  return fs.existsSync(local) ? local : undefined;
24
}
25
26
/** A program by name: on PATH, else in ~/.local/bin. VS Code from a snap
27
 * or a desktop launcher often has a PATH without ~/.local/bin. */
28
export function program(name: string): string | undefined {
29
  for (const dir of (process.env.PATH ?? "").split(path.delimiter)) {
30
    const p = path.join(dir, name);
31
    if (dir && fs.existsSync(p)) return p;
32
  }
33
  for (const dir of [path.join(os.homedir(), ".local", "bin"), "/usr/local/bin", "/usr/bin"]) {
34
    const p = path.join(dir, name);
35
    if (fs.existsSync(p)) return p;
36
  }
37
  return undefined;
38
}
39
40
export function run(args: string[], cwd?: string, input?: string): Promise<Ran> {
41
  const bin = binary();
42
  if (!bin) return Promise.resolve({ code: 127, stdout: "", stderr: "spectra is not installed (Spectra: Check for spectra)" });
43
  return new Promise((resolve) => {
44
    const child = cp.execFile(bin, args, { cwd, env: { ...process.env, NO_COLOR: "1" }, maxBuffer: 16 * 1024 * 1024 }, (err, stdout, stderr) => {
45
      const code = err && typeof (err as cp.ExecFileException).code === "number" ? ((err as cp.ExecFileException).code as number) : err ? 1 : 0;
46
      resolve({ code, stdout: String(stdout), stderr: String(stderr) });
47
    });
48
    if (input !== undefined) {
49
      child.stdin?.end(input);
50
    }
51
  });
52
}
53
54
/** Run, and parse stdout as JSON; throw with spectra's own words on failure. */
55
export async function json<T>(args: string[], cwd?: string): Promise<T> {
56
  const r = await run([...args], cwd);
57
  if (r.code !== 0) throw new Error(why(r));
58
  return JSON.parse(r.stdout) as T;
59
}
60
61
/** Run; throw with spectra's own words on failure. */
62
export async function ok(args: string[], cwd?: string, input?: string): Promise<string> {
63
  const r = await run(args, cwd, input);
64
  if (r.code !== 0) throw new Error(why(r));
65
  return r.stdout;
66
}
67
68
/** The error spectra printed, without its `error:` prefix. */
69
export function why(r: Ran): string {
70
  const text = (r.stderr.trim() || r.stdout.trim()).split("\n").filter((l) => l.trim()).pop() ?? `spectra exited ${r.code}`;
71
  return text.replace(/^error:\s*/, "");
72
}
73
74
export interface Status {
75
  version: string;
76
  home: string;
77
  identity: { name: string; key: string } | null;
78
  server: { url: string; instance: string } | null;
79
  local: { data: string; initialised: boolean; serving: boolean };
80
  reachable: boolean;
81
}
82
83
export function status(): Promise<Status> {
84
  return json<Status>(["status", "--json"]);
85
}