@craftspace/cli 0.1.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/dist/run.js ADDED
@@ -0,0 +1,117 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { MACHINE_RUN_OUTPUT_MAX, spreadIfDefined } from '@craftspace/shared';
3
+ export const runner = {
4
+ attended(argv) {
5
+ return new Promise((resolve) => {
6
+ const child = spawn(argv[0], argv.slice(1), { stdio: 'inherit' });
7
+ child.on('error', () => resolve(127));
8
+ child.on('close', (code) => resolve(code ?? 0));
9
+ });
10
+ },
11
+ async unattended({ work, onProgress, }) {
12
+ const argv = headlessArgv(work);
13
+ let output = '';
14
+ let agentSessionId = work.agentSessionId ?? undefined;
15
+ let lastText = '';
16
+ let pending = '';
17
+ const append = (text) => {
18
+ output = `${output}${text}`.slice(-MACHINE_RUN_OUTPUT_MAX);
19
+ onProgress?.({ id: work.id, state: 'running', output, ...spreadIfDefined({ agentSessionId }) });
20
+ };
21
+ const readLine = (line) => {
22
+ const event = parseJson(line);
23
+ if (event === null) {
24
+ append(`${line}\n`);
25
+ return;
26
+ }
27
+ const seen = readEvent(event);
28
+ if (seen.sessionId !== undefined)
29
+ agentSessionId = seen.sessionId;
30
+ if (seen.text !== undefined) {
31
+ lastText = seen.text;
32
+ append(`${seen.text}\n`);
33
+ }
34
+ };
35
+ const code = await new Promise((resolve) => {
36
+ const child = spawn(argv[0], argv.slice(1), { stdio: ['ignore', 'pipe', 'pipe'] });
37
+ child.stdout.setEncoding('utf8');
38
+ child.stderr.setEncoding('utf8');
39
+ child.stdout.on('data', (chunk) => {
40
+ pending += chunk;
41
+ const lines = pending.split('\n');
42
+ pending = lines.pop() ?? '';
43
+ for (const line of lines)
44
+ if (line.trim() !== '')
45
+ readLine(line);
46
+ });
47
+ child.stderr.on('data', (chunk) => append(chunk));
48
+ child.on('error', (error) => {
49
+ append(`${error.message}\n`);
50
+ resolve(127);
51
+ });
52
+ child.on('close', (exit) => {
53
+ if (pending.trim() !== '')
54
+ readLine(pending);
55
+ resolve(exit ?? 0);
56
+ });
57
+ });
58
+ if (code === 0) {
59
+ return { id: work.id, state: 'done', output, exitCode: 0, ...spreadIfDefined({ agentSessionId }) };
60
+ }
61
+ if (agentSessionId !== undefined) {
62
+ return {
63
+ id: work.id,
64
+ state: 'needs_input',
65
+ output,
66
+ question: (lastText === '' ? 'It stopped and needs a decision from you.' : lastText).slice(0, 4000),
67
+ agentSessionId,
68
+ };
69
+ }
70
+ return { id: work.id, state: 'failed', output, exitCode: code, ...spreadIfDefined({ agentSessionId }) };
71
+ },
72
+ };
73
+ export function headlessArgv(work) {
74
+ const argv = work.argv;
75
+ if (!isAgent(argv[0]))
76
+ return argv;
77
+ // A Remote Control session is interactive and long-lived, and its whole point is that a phone drives it.
78
+ // Wrapping it in -p would turn it into a one-shot print run with nothing to attach to.
79
+ if (argv.includes('--remote-control'))
80
+ return argv;
81
+ const streaming = ['--output-format', 'stream-json', '--verbose'];
82
+ if (work.answer !== null && work.agentSessionId !== null) {
83
+ return [argv[0], '--resume', work.agentSessionId, '-p', work.answer, ...streaming];
84
+ }
85
+ if (argv.includes('-p') || argv.includes('--print'))
86
+ return [...argv, ...streaming];
87
+ return [argv[0], '-p', argv.slice(1).join(' '), ...streaming];
88
+ }
89
+ function isAgent(command) {
90
+ return AGENTS.has((command ?? '').split('/').pop() ?? '');
91
+ }
92
+ const AGENTS = new Set(['claude', 'codex', 'opencode']);
93
+ function readEvent(event) {
94
+ const sessionId = typeof event.session_id === 'string' ? event.session_id : undefined;
95
+ if (event.type === 'assistant') {
96
+ const message = event.message;
97
+ const text = (message?.content ?? [])
98
+ .filter((part) => part.type === 'text' && typeof part.text === 'string')
99
+ .map((part) => part.text)
100
+ .join('');
101
+ return { ...spreadIfDefined({ sessionId }), ...spreadIfDefined({ text: text === '' ? undefined : text }) };
102
+ }
103
+ if (event.type === 'result') {
104
+ const result = typeof event.result === 'string' ? event.result : undefined;
105
+ return { ...spreadIfDefined({ sessionId }), ...spreadIfDefined({ text: result }) };
106
+ }
107
+ return spreadIfDefined({ sessionId });
108
+ }
109
+ function parseJson(line) {
110
+ try {
111
+ const parsed = JSON.parse(line);
112
+ return typeof parsed === 'object' && parsed !== null ? parsed : null;
113
+ }
114
+ catch {
115
+ return null;
116
+ }
117
+ }
@@ -0,0 +1,8 @@
1
+ import type { MachineTool } from '@craftspace/shared';
2
+ export declare const setup: {
3
+ run({ install, rewire, say, }: {
4
+ install: boolean;
5
+ rewire?: () => Promise<void>;
6
+ say?: (line: string) => void;
7
+ }): Promise<MachineTool[]>;
8
+ };
package/dist/setup.js ADDED
@@ -0,0 +1,59 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ import { probe } from './probe.js';
4
+ const run = promisify(execFile);
5
+ export const setup = {
6
+ async run({ install, rewire, say, }) {
7
+ const found = await probe.tools();
8
+ if (!install)
9
+ return found;
10
+ const repairs = { ...REPAIRS, ...(rewire === undefined ? {} : { brain: { name: 'the brain', fix: rewire } }) };
11
+ const broken = found.filter((tool) => !tool.ok && repairs[tool.id] !== undefined);
12
+ if (broken.length === 0)
13
+ return found;
14
+ for (const tool of broken) {
15
+ const repair = repairs[tool.id];
16
+ if (repair === undefined)
17
+ continue;
18
+ say?.(`Setting up ${repair.name}`);
19
+ const failed = await repair.fix().then(() => null, (error) => firstLine(error.message));
20
+ say?.(failed === null ? ` ${repair.name} is ready` : ` could not set up ${repair.name}: ${failed}`);
21
+ }
22
+ return probe.tools();
23
+ },
24
+ };
25
+ const REPAIRS = {
26
+ claude: { name: 'Claude Code', fix: () => npmGlobal('@anthropic-ai/claude-code') },
27
+ codex: { name: 'Codex', fix: () => npmGlobal('@openai/codex') },
28
+ git: { name: 'the GitHub CLI', fix: () => installPackage({ brew: ['gh'], apt: ['gh'], dnf: ['gh'] }) },
29
+ chrome: {
30
+ name: 'Google Chrome',
31
+ fix: () => installPackage({ brew: ['--cask', 'google-chrome'], apt: ['chromium'], dnf: ['chromium'] }),
32
+ },
33
+ };
34
+ async function npmGlobal(name) {
35
+ await run('npm', ['install', '-g', name], { timeout: INSTALL_TIMEOUT_MS });
36
+ }
37
+ async function installPackage({ brew, apt, dnf }) {
38
+ if (process.platform === 'darwin') {
39
+ await run('brew', ['install', ...brew], { timeout: INSTALL_TIMEOUT_MS });
40
+ return;
41
+ }
42
+ const manager = await firstAvailable(['apt-get', 'dnf']);
43
+ if (manager === null)
44
+ throw new Error('no package manager this CLI knows how to drive');
45
+ const packages = manager === 'apt-get' ? apt : dnf;
46
+ await run('sudo', ['-n', manager, 'install', '-y', ...packages], { timeout: INSTALL_TIMEOUT_MS });
47
+ }
48
+ async function firstAvailable(commands) {
49
+ for (const command of commands) {
50
+ const there = await run('/bin/sh', ['-c', `command -v ${command}`]).then(() => true, () => false);
51
+ if (there)
52
+ return command;
53
+ }
54
+ return null;
55
+ }
56
+ function firstLine(message) {
57
+ return message.split('\n')[0] ?? 'it failed';
58
+ }
59
+ const INSTALL_TIMEOUT_MS = 180_000;
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@craftspace/cli",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Sign a Mac or Linux machine into Craftspace and keep its agent setup in step.",
6
+ "license": "MIT",
7
+ "homepage": "https://craftspace.app",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/abuaboud/gus.git",
11
+ "directory": "packages/app/cli"
12
+ },
13
+ "engines": {
14
+ "node": ">=20"
15
+ },
16
+ "bin": {
17
+ "craftspace": "dist/index.js"
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "scripts": {
26
+ "build": "tsc",
27
+ "dev": "tsc --watch",
28
+ "test": "tsc && node --test test/*.test.mjs",
29
+ "prepack": "npm run build && esbuild dist/index.js --bundle --platform=node --format=esm --outfile=dist/index.js --allow-overwrite --banner:js=\"import{createRequire as __cr}from'node:module';const require=__cr(import.meta.url);\""
30
+ },
31
+ "devDependencies": {
32
+ "@craftspace/shared": "*",
33
+ "@types/node": "^22.0.0",
34
+ "commander": "^14.0.0",
35
+ "esbuild": "^0.28.0",
36
+ "typescript": "^5.6.0"
37
+ }
38
+ }