@craftspace/cli 0.4.2 → 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/dist/index.js +278 -254
- package/dist/machine.d.ts +2 -1
- package/dist/machine.js +88 -11
- package/dist/probe.d.ts +0 -1
- package/dist/probe.js +3 -66
- package/dist/setup.d.ts +4 -5
- package/dist/setup.js +32 -81
- package/package.json +1 -1
package/dist/machine.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export declare const machine: {
|
|
|
16
16
|
machine: Machine;
|
|
17
17
|
url: string;
|
|
18
18
|
}>;
|
|
19
|
-
beatOnce(): Promise<boolean>;
|
|
19
|
+
beatOnce(say?: (line: string) => void): Promise<boolean>;
|
|
20
20
|
beatForever(): Promise<void>;
|
|
21
21
|
run(): Promise<number>;
|
|
22
22
|
daemon(): Promise<{
|
|
@@ -25,6 +25,7 @@ export declare const machine: {
|
|
|
25
25
|
}>;
|
|
26
26
|
logout(): Promise<void>;
|
|
27
27
|
};
|
|
28
|
+
export declare function portableTerm(local: string | undefined): string;
|
|
28
29
|
export declare function tokenIn(line: string): string | undefined;
|
|
29
30
|
export declare function nextDelayMs(failures: number, busy?: boolean): number;
|
|
30
31
|
interface Auth {
|
package/dist/machine.js
CHANGED
|
@@ -3,9 +3,10 @@ import { mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
|
-
import { CLI_VERSION, MACHINE_BEAT_BUSY_INTERVAL_MS, MACHINE_BEAT_INTERVAL_MS, MachineBeatResponseSchema, MachineLoginResponseSchema, MachineSchema, MachineSessionSchema, MachinesResponseSchema, } from '@craftspace/shared';
|
|
6
|
+
import { CLI_VERSION, MACHINE_BEAT_BUSY_INTERVAL_MS, MACHINE_BEAT_INTERVAL_MS, MachineBeatResponseSchema, MachineLoginResponseSchema, MachineSchema, MachineSessionSchema, MachinesResponseSchema, WorkstationStepResultSchema, } from '@craftspace/shared';
|
|
7
7
|
import { confirm, input, select } from '@inquirer/prompts';
|
|
8
8
|
import { z } from 'zod';
|
|
9
|
+
import { probe } from './probe.js';
|
|
9
10
|
import { runner } from './run.js';
|
|
10
11
|
import { setup } from './setup.js';
|
|
11
12
|
import { ui } from './ui.js';
|
|
@@ -37,10 +38,8 @@ export const machine = {
|
|
|
37
38
|
await writeFile(tokenPath(), token, { mode: 0o600 });
|
|
38
39
|
await writeAgentConfigs({ url, token, writes: answer.write });
|
|
39
40
|
await installService();
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
tools: await setup.run({ install, rewire, say: ui.say }),
|
|
43
|
-
};
|
|
41
|
+
if (install)
|
|
42
|
+
await machine.beatOnce(ui.say);
|
|
44
43
|
return answer.machine;
|
|
45
44
|
},
|
|
46
45
|
async controlPlane(override) {
|
|
@@ -74,8 +73,9 @@ export const machine = {
|
|
|
74
73
|
const found = await withAuth(await signedInOrAsk(), (auth) => call({ ...auth, method: 'GET', path: '/api/machines/me', schema: MachineSchema }));
|
|
75
74
|
return { machine: found.value, url: found.auth.url };
|
|
76
75
|
},
|
|
77
|
-
async beatOnce() {
|
|
76
|
+
async beatOnce(say) {
|
|
78
77
|
const { url, token } = await requireSignedIn();
|
|
78
|
+
const built = await readEnvironment();
|
|
79
79
|
const answer = await call({
|
|
80
80
|
url,
|
|
81
81
|
token,
|
|
@@ -86,12 +86,19 @@ export const machine = {
|
|
|
86
86
|
...load(),
|
|
87
87
|
sessions: await readSessions(),
|
|
88
88
|
runs: drainReports(),
|
|
89
|
-
tools: await
|
|
89
|
+
tools: await checks(),
|
|
90
|
+
environment: built?.results ?? [],
|
|
91
|
+
templateId: built?.templateId ?? null,
|
|
90
92
|
sshUser: os.userInfo().username,
|
|
91
93
|
},
|
|
92
94
|
schema: MachineBeatResponseSchema,
|
|
93
95
|
});
|
|
94
96
|
await writeAuthorizedKeys(answer.authorizedKeys);
|
|
97
|
+
const building = buildEnvironment(answer.environment, say);
|
|
98
|
+
if (say === undefined)
|
|
99
|
+
void building.catch(() => undefined);
|
|
100
|
+
else
|
|
101
|
+
await building;
|
|
95
102
|
for (const work of answer.work) {
|
|
96
103
|
if (running.has(work.id))
|
|
97
104
|
continue;
|
|
@@ -170,7 +177,7 @@ async function pick(machines) {
|
|
|
170
177
|
choices: machines.map((candidate) => ({
|
|
171
178
|
name: `${candidate.name.padEnd(24)} ${candidate.state.padEnd(12)} ${specs(candidate)} ${beat(candidate)}`,
|
|
172
179
|
value: candidate,
|
|
173
|
-
|
|
180
|
+
disabled: candidate.sshAddress === null ? '(no address yet, so nothing to open)' : false,
|
|
174
181
|
})),
|
|
175
182
|
pageSize: 12,
|
|
176
183
|
});
|
|
@@ -197,7 +204,10 @@ async function openTerminal(target) {
|
|
|
197
204
|
const where = `${target.sshUser ?? 'root'}@${target.sshAddress}`;
|
|
198
205
|
ui.say(`\n${ui.dim('\u2192')} ${ui.name(target.name)} ${ui.dim(`\u00b7 ${where}`)}\n\n`);
|
|
199
206
|
return new Promise((resolve) => {
|
|
200
|
-
const session = spawn('ssh', ['-t', where], {
|
|
207
|
+
const session = spawn('ssh', ['-t', where], {
|
|
208
|
+
stdio: 'inherit',
|
|
209
|
+
env: { ...process.env, TERM: portableTerm(process.env.TERM) },
|
|
210
|
+
});
|
|
201
211
|
session.on('error', (error) => {
|
|
202
212
|
ui.say(ui.bad(`could not start ssh`, error.message));
|
|
203
213
|
resolve(1);
|
|
@@ -205,6 +215,9 @@ async function openTerminal(target) {
|
|
|
205
215
|
session.on('exit', (code) => resolve(code ?? 0));
|
|
206
216
|
});
|
|
207
217
|
}
|
|
218
|
+
export function portableTerm(local) {
|
|
219
|
+
return local !== undefined && PORTABLE_TERMS.has(local) ? local : 'xterm-256color';
|
|
220
|
+
}
|
|
208
221
|
function nowhereToGo({ url }) {
|
|
209
222
|
ui.say(`\n${ui.attention('No workstations here yet.')}\n\n`);
|
|
210
223
|
ui.say(ui.field('Add one', ui.link(`${url}/workstations`)));
|
|
@@ -212,13 +225,52 @@ function nowhereToGo({ url }) {
|
|
|
212
225
|
ui.say('\n');
|
|
213
226
|
return 1;
|
|
214
227
|
}
|
|
215
|
-
async function
|
|
228
|
+
async function checks() {
|
|
216
229
|
if (probed !== null && Date.now() - probed.at < PROBE_EVERY_MS)
|
|
217
230
|
return probed.tools;
|
|
218
|
-
const
|
|
231
|
+
const first = await probe.tools();
|
|
232
|
+
const unwired = first.some((tool) => tool.id === 'brain' && !tool.ok);
|
|
233
|
+
const found = unwired && installs() ? await rewire().then(() => probe.tools(), () => first) : first;
|
|
219
234
|
probed = { at: Date.now(), tools: found };
|
|
220
235
|
return found;
|
|
221
236
|
}
|
|
237
|
+
async function buildEnvironment(next, say) {
|
|
238
|
+
if (building || !installs() || next.templateId === null)
|
|
239
|
+
return;
|
|
240
|
+
const built = await readEnvironment();
|
|
241
|
+
if (!buildIsDue(built, next.templateId))
|
|
242
|
+
return;
|
|
243
|
+
building = true;
|
|
244
|
+
try {
|
|
245
|
+
say?.(ui.heading(`Building the ${next.steps.length} setup scripts your team runs on every box`));
|
|
246
|
+
const results = await setup.runSteps({ steps: next.steps, say });
|
|
247
|
+
await writeEnvironment({ templateId: next.templateId, results, at: Date.now() });
|
|
248
|
+
}
|
|
249
|
+
finally {
|
|
250
|
+
building = false;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function buildIsDue(built, templateId) {
|
|
254
|
+
if (built === null || built.templateId !== templateId)
|
|
255
|
+
return true;
|
|
256
|
+
if (built.results.every((result) => result.ok))
|
|
257
|
+
return false;
|
|
258
|
+
return Date.now() - built.at > REBUILD_AFTER_MS;
|
|
259
|
+
}
|
|
260
|
+
async function readEnvironment() {
|
|
261
|
+
const raw = await readFile(environmentPath(), 'utf8').catch(() => null);
|
|
262
|
+
if (raw === null)
|
|
263
|
+
return null;
|
|
264
|
+
const parsed = BuiltSchema.safeParse(JSON.parse(raw));
|
|
265
|
+
return parsed.success ? parsed.data : null;
|
|
266
|
+
}
|
|
267
|
+
async function writeEnvironment(built) {
|
|
268
|
+
await mkdir(machine.home(), { recursive: true, mode: 0o700 });
|
|
269
|
+
await writeFile(environmentPath(), JSON.stringify(built, null, 2), { mode: 0o600 });
|
|
270
|
+
}
|
|
271
|
+
function installs() {
|
|
272
|
+
return process.env.CRAFTSPACE_NO_INSTALL !== '1';
|
|
273
|
+
}
|
|
222
274
|
async function rewire() {
|
|
223
275
|
const { url, token } = await requireSignedIn();
|
|
224
276
|
await writeAgentConfigs({ url, token, writes: [] });
|
|
@@ -497,6 +549,9 @@ function tokenPath() {
|
|
|
497
549
|
function ownedPath() {
|
|
498
550
|
return path.join(machine.home(), 'owned.json');
|
|
499
551
|
}
|
|
552
|
+
function environmentPath() {
|
|
553
|
+
return path.join(machine.home(), 'environment.json');
|
|
554
|
+
}
|
|
500
555
|
function load() {
|
|
501
556
|
const cores = Math.max(1, os.cpus().length);
|
|
502
557
|
const busy = Math.min(100, ((os.loadavg()[0] ?? 0) / cores) * 100);
|
|
@@ -515,6 +570,11 @@ function messageOf(error) {
|
|
|
515
570
|
return error instanceof Error ? error.message : String(error);
|
|
516
571
|
}
|
|
517
572
|
const ConfigSchema = z.object({ url: z.string(), machineId: z.string() });
|
|
573
|
+
const BuiltSchema = z.object({
|
|
574
|
+
templateId: z.string(),
|
|
575
|
+
results: z.array(WorkstationStepResultSchema),
|
|
576
|
+
at: z.number(),
|
|
577
|
+
});
|
|
518
578
|
const OwnedSchema = z.object({ paths: z.array(z.string()) });
|
|
519
579
|
const SERVICE_NAME = 'craftspace';
|
|
520
580
|
const LAUNCH_LABEL = 'app.craftspace.machine';
|
|
@@ -525,9 +585,26 @@ const KEYS_BEGIN = '# craftspace begin';
|
|
|
525
585
|
const KEYS_END = '# craftspace end';
|
|
526
586
|
const UPDATE_EVERY_MS = 6 * 60 * 60 * 1_000;
|
|
527
587
|
const BEATING = 'beating every 15s, outbound only, and keeps this set up';
|
|
588
|
+
const PORTABLE_TERMS = new Set([
|
|
589
|
+
'ansi',
|
|
590
|
+
'dumb',
|
|
591
|
+
'linux',
|
|
592
|
+
'rxvt',
|
|
593
|
+
'rxvt-unicode',
|
|
594
|
+
'rxvt-unicode-256color',
|
|
595
|
+
'screen',
|
|
596
|
+
'screen-256color',
|
|
597
|
+
'vt100',
|
|
598
|
+
'vt220',
|
|
599
|
+
'xterm',
|
|
600
|
+
'xterm-color',
|
|
601
|
+
'xterm-256color',
|
|
602
|
+
]);
|
|
528
603
|
const LINGER_LINE = `sudo loginctl enable-linger ${os.userInfo().username}`;
|
|
529
604
|
const DEFAULT_URL = process.env.CRAFTSPACE_URL ?? 'https://craftspace.app';
|
|
530
605
|
const PROBE_EVERY_MS = 300_000;
|
|
606
|
+
const REBUILD_AFTER_MS = 21_600_000;
|
|
531
607
|
let probed = null;
|
|
608
|
+
let building = false;
|
|
532
609
|
const reports = new Map();
|
|
533
610
|
const running = new Set();
|
package/dist/probe.d.ts
CHANGED
package/dist/probe.js
CHANGED
|
@@ -1,21 +1,10 @@
|
|
|
1
|
-
import { execFile } from 'node:child_process';
|
|
2
1
|
import net from 'node:net';
|
|
3
|
-
import {
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
4
3
|
import os from 'node:os';
|
|
5
4
|
import path from 'node:path';
|
|
6
|
-
import { promisify } from 'node:util';
|
|
7
|
-
const run = promisify(execFile);
|
|
8
5
|
export const probe = {
|
|
9
6
|
async tools({ mcpPath = path.join(os.homedir(), '.mcp.json') } = {}) {
|
|
10
|
-
|
|
11
|
-
brainWired(mcpPath),
|
|
12
|
-
version({ id: 'claude', command: 'claude' }),
|
|
13
|
-
version({ id: 'codex', command: 'codex' }),
|
|
14
|
-
gitAndGithub(),
|
|
15
|
-
chromeInstalled(),
|
|
16
|
-
sshListening(),
|
|
17
|
-
]);
|
|
18
|
-
return [brain, claude, codex, git, chrome, ssh];
|
|
7
|
+
return Promise.all([brainWired(mcpPath), sshListening()]);
|
|
19
8
|
},
|
|
20
9
|
};
|
|
21
10
|
async function brainWired(mcpPath) {
|
|
@@ -31,43 +20,6 @@ async function brainWired(mcpPath) {
|
|
|
31
20
|
detail: wired ? short(mcpPath) : `${short(mcpPath)} has no craftspace entry`,
|
|
32
21
|
};
|
|
33
22
|
}
|
|
34
|
-
async function version({ id, command }) {
|
|
35
|
-
const found = await firstLine(command, ['--version']);
|
|
36
|
-
if (found.ran)
|
|
37
|
-
return { id, ok: true, detail: found.line };
|
|
38
|
-
return { id, ok: false, detail: found.why === 'absent' ? 'not installed' : 'installed, but it will not report a version' };
|
|
39
|
-
}
|
|
40
|
-
async function gitAndGithub() {
|
|
41
|
-
const git = await firstLine('git', ['--version']);
|
|
42
|
-
if (!git.ran)
|
|
43
|
-
return { id: 'git', ok: false, detail: 'git is not installed' };
|
|
44
|
-
const gh = await firstLine('gh', ['--version']);
|
|
45
|
-
if (!gh.ran)
|
|
46
|
-
return { id: 'git', ok: false, detail: `${git.line} · the GitHub CLI is not installed yet` };
|
|
47
|
-
const account = await githubAccount();
|
|
48
|
-
return {
|
|
49
|
-
id: 'git',
|
|
50
|
-
ok: account !== null,
|
|
51
|
-
detail: account === null
|
|
52
|
-
? `${git.line} · ${gh.line}, run gh auth login on this machine`
|
|
53
|
-
: `${git.line} · ${gh.line}, signed in as ${account}`,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
async function githubAccount() {
|
|
57
|
-
const raw = await readFile(path.join(os.homedir(), '.config', 'gh', 'hosts.yml'), 'utf8').catch(() => null);
|
|
58
|
-
return raw === null ? null : githubUser(raw);
|
|
59
|
-
}
|
|
60
|
-
export function githubUser(hostsYml) {
|
|
61
|
-
return /^ {4}user: *(\S+) *$/m.exec(hostsYml)?.[1] ?? null;
|
|
62
|
-
}
|
|
63
|
-
async function chromeInstalled() {
|
|
64
|
-
for (const command of [...MAC_CHROME, ...LINUX_CHROME]) {
|
|
65
|
-
const found = await firstLine(command, ['--version']);
|
|
66
|
-
if (found.ran)
|
|
67
|
-
return { id: 'chrome', ok: true, detail: found.line };
|
|
68
|
-
}
|
|
69
|
-
return { id: 'chrome', ok: false, detail: 'not installed yet, so browser runs will fail' };
|
|
70
|
-
}
|
|
71
23
|
function sshListening() {
|
|
72
24
|
return new Promise((resolve) => {
|
|
73
25
|
const socket = net.connect({ host: '127.0.0.1', port: 22 });
|
|
@@ -78,25 +30,10 @@ function sshListening() {
|
|
|
78
30
|
socket.setTimeout(SSH_PROBE_TIMEOUT_MS);
|
|
79
31
|
socket.on('connect', () => answer(true, 'sshd is listening on port 22'));
|
|
80
32
|
socket.on('timeout', () => answer(false, 'nothing answered on port 22'));
|
|
81
|
-
socket.on('error', () => answer(false, 'sshd is not running, so nobody can
|
|
33
|
+
socket.on('error', () => answer(false, 'sshd is not running, so nobody can open a terminal here'));
|
|
82
34
|
});
|
|
83
35
|
}
|
|
84
|
-
async function firstLine(command, args) {
|
|
85
|
-
if (command.includes('/')) {
|
|
86
|
-
const reachable = await access(command).then(() => true, () => false);
|
|
87
|
-
if (!reachable)
|
|
88
|
-
return { ran: false, why: 'absent' };
|
|
89
|
-
}
|
|
90
|
-
const answer = await run(command, args, { timeout: PROBE_TIMEOUT_MS }).catch((thrown) => thrown);
|
|
91
|
-
if (answer instanceof Error)
|
|
92
|
-
return { ran: false, why: answer.code === 'ENOENT' ? 'absent' : 'broken' };
|
|
93
|
-
const line = answer.stdout.split('\n')[0]?.trim() ?? '';
|
|
94
|
-
return line === '' ? { ran: false, why: 'broken' } : { ran: true, line: line.slice(0, 200) };
|
|
95
|
-
}
|
|
96
36
|
function short(target) {
|
|
97
37
|
return target.startsWith(os.homedir()) ? `~${target.slice(os.homedir().length)}` : target;
|
|
98
38
|
}
|
|
99
|
-
const MAC_CHROME = ['/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'];
|
|
100
|
-
const LINUX_CHROME = ['google-chrome', 'google-chrome-stable', 'chromium'];
|
|
101
|
-
const PROBE_TIMEOUT_MS = 5_000;
|
|
102
39
|
const SSH_PROBE_TIMEOUT_MS = 1_500;
|
package/dist/setup.d.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type WorkstationStep, type WorkstationStepResult } from '@craftspace/shared';
|
|
2
2
|
export declare const setup: {
|
|
3
3
|
widenPath(): void;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
rewire?: () => Promise<void>;
|
|
4
|
+
runSteps({ steps, say, }: {
|
|
5
|
+
steps: WorkstationStep[];
|
|
7
6
|
say?: (line: string) => void;
|
|
8
|
-
}): Promise<
|
|
7
|
+
}): Promise<WorkstationStepResult[]>;
|
|
9
8
|
};
|
package/dist/setup.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import {
|
|
5
|
-
import { probe } from './probe.js';
|
|
4
|
+
import { WORKSTATION_STEP_DETAIL_MAX_CHARS, } from '@craftspace/shared';
|
|
6
5
|
import { ui } from './ui.js';
|
|
7
|
-
const run = promisify(execFile);
|
|
8
6
|
export const setup = {
|
|
9
7
|
widenPath() {
|
|
10
8
|
const current = (process.env.PATH ?? '').split(path.delimiter).filter((entry) => entry !== '');
|
|
@@ -12,44 +10,39 @@ export const setup = {
|
|
|
12
10
|
if (missing.length > 0)
|
|
13
11
|
process.env.PATH = [...current, ...missing].join(path.delimiter);
|
|
14
12
|
},
|
|
15
|
-
async
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
let ready = 0;
|
|
24
|
-
for (const [index, tool] of broken.entries()) {
|
|
25
|
-
const repair = repairs[tool.id];
|
|
26
|
-
if (repair === undefined)
|
|
27
|
-
continue;
|
|
28
|
-
say?.(ui.progress({ name: repair.name, done: index, total: broken.length }));
|
|
29
|
-
tried.set(tool.id, Date.now());
|
|
30
|
-
const failed = await repair.fix().then(() => null, (error) => firstLine(error.message));
|
|
31
|
-
if (failed === null)
|
|
32
|
-
reasons.delete(tool.id);
|
|
33
|
-
else
|
|
34
|
-
reasons.set(tool.id, failed);
|
|
35
|
-
if (failed === null)
|
|
36
|
-
ready += 1;
|
|
37
|
-
say?.(`${ui.clear()}${failed === null ? ui.ok(repair.name) : ui.bad(repair.name, failed)}`);
|
|
13
|
+
async runSteps({ steps, say, }) {
|
|
14
|
+
const results = [];
|
|
15
|
+
for (const [index, step] of steps.entries()) {
|
|
16
|
+
say?.(ui.progress({ name: step.name, done: index, total: steps.length }));
|
|
17
|
+
const startedAt = Date.now();
|
|
18
|
+
const outcome = await runScript(step.script);
|
|
19
|
+
results.push({ ...outcome, id: step.id, name: step.name, ms: Date.now() - startedAt });
|
|
20
|
+
say?.(`${ui.clear()}${outcome.ok ? ui.ok(step.name) : ui.bad(step.name, outcome.detail)}`);
|
|
38
21
|
}
|
|
39
|
-
|
|
40
|
-
|
|
22
|
+
if (results.length > 0) {
|
|
23
|
+
say?.(ui.tally({ done: results.filter((result) => result.ok).length, total: results.length, noun: 'ready' }));
|
|
24
|
+
}
|
|
25
|
+
return results;
|
|
41
26
|
},
|
|
42
27
|
};
|
|
43
|
-
function
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return reason === undefined ? tool : { ...tool, detail: `${tool.detail} · ${reason}`.slice(0, 200) };
|
|
28
|
+
function runScript(script) {
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
execFile('/bin/sh', ['-e', '-c', script], { timeout: STEP_TIMEOUT_MS, maxBuffer: SCRIPT_BUFFER_BYTES }, (error, stdout, stderr) => {
|
|
31
|
+
if (error === null)
|
|
32
|
+
return resolve({ ok: true, detail: lastLines(stdout) });
|
|
33
|
+
resolve({ ok: false, detail: lastLines(`${stderr}\n${stdout}`) || error.message.split('\n')[0] || 'it failed' });
|
|
34
|
+
});
|
|
51
35
|
});
|
|
52
36
|
}
|
|
37
|
+
function lastLines(output) {
|
|
38
|
+
return output
|
|
39
|
+
.split('\n')
|
|
40
|
+
.map((line) => line.trim())
|
|
41
|
+
.filter((line) => line !== '')
|
|
42
|
+
.slice(-KEPT_LINES)
|
|
43
|
+
.join(' · ')
|
|
44
|
+
.slice(0, WORKSTATION_STEP_DETAIL_MAX_CHARS);
|
|
45
|
+
}
|
|
53
46
|
function toolDirs() {
|
|
54
47
|
const prefix = process.env.npm_config_prefix;
|
|
55
48
|
return [
|
|
@@ -61,48 +54,6 @@ function toolDirs() {
|
|
|
61
54
|
'/usr/local/bin',
|
|
62
55
|
];
|
|
63
56
|
}
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
git: {
|
|
68
|
-
name: 'the GitHub CLI',
|
|
69
|
-
fix: async () => {
|
|
70
|
-
if ((await firstAvailable(['gh'])) !== null)
|
|
71
|
-
return;
|
|
72
|
-
await installPackage({ brew: ['gh'], apt: ['gh'], dnf: ['gh'] });
|
|
73
|
-
},
|
|
74
|
-
},
|
|
75
|
-
chrome: {
|
|
76
|
-
name: 'Google Chrome',
|
|
77
|
-
fix: () => installPackage({ brew: ['--cask', 'google-chrome'], apt: ['chromium'], dnf: ['chromium'] }),
|
|
78
|
-
},
|
|
79
|
-
};
|
|
80
|
-
async function npmGlobal(name) {
|
|
81
|
-
await run('npm', ['install', '-g', name], { timeout: INSTALL_TIMEOUT_MS });
|
|
82
|
-
}
|
|
83
|
-
async function installPackage({ brew, apt, dnf }) {
|
|
84
|
-
if (process.platform === 'darwin') {
|
|
85
|
-
await run('brew', ['install', ...brew], { timeout: INSTALL_TIMEOUT_MS });
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
const manager = await firstAvailable(['apt-get', 'dnf']);
|
|
89
|
-
if (manager === null)
|
|
90
|
-
throw new Error('no package manager this CLI knows how to drive');
|
|
91
|
-
const packages = manager === 'apt-get' ? apt : dnf;
|
|
92
|
-
await run('sudo', ['-n', manager, 'install', '-y', ...packages], { timeout: INSTALL_TIMEOUT_MS });
|
|
93
|
-
}
|
|
94
|
-
async function firstAvailable(commands) {
|
|
95
|
-
for (const command of commands) {
|
|
96
|
-
const there = await run('/bin/sh', ['-c', `command -v ${command}`]).then(() => true, () => false);
|
|
97
|
-
if (there)
|
|
98
|
-
return command;
|
|
99
|
-
}
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
function firstLine(message) {
|
|
103
|
-
return message.split('\n')[0] ?? 'it failed';
|
|
104
|
-
}
|
|
105
|
-
const tried = new Map();
|
|
106
|
-
const reasons = new Map();
|
|
107
|
-
const INSTALL_TIMEOUT_MS = 180_000;
|
|
108
|
-
const RETRY_AFTER_MS = 21_600_000;
|
|
57
|
+
const STEP_TIMEOUT_MS = 600_000;
|
|
58
|
+
const SCRIPT_BUFFER_BYTES = 4 * 1024 * 1024;
|
|
59
|
+
const KEPT_LINES = 3;
|