@craftspace/cli 0.8.0 → 0.9.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 +495 -375
- package/dist/machine.d.ts +1 -0
- package/dist/machine.js +94 -62
- package/dist/migrate.d.ts +6 -0
- package/dist/migrate.js +79 -0
- package/dist/probe.d.ts +2 -2
- package/dist/probe.js +21 -12
- package/dist/setup.d.ts +4 -2
- package/dist/setup.js +9 -5
- package/package.json +2 -2
package/dist/machine.d.ts
CHANGED
package/dist/machine.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { execFile, spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
2
3
|
import { mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
|
|
3
4
|
import os from 'node:os';
|
|
4
5
|
import path from 'node:path';
|
|
5
6
|
import { promisify } from 'node:util';
|
|
6
|
-
import { CLI_VERSION, MACHINE_BEAT_BUSY_INTERVAL_MS, MACHINE_BEAT_INTERVAL_MS, MachineBeatResponseSchema, MachineLoginResponseSchema, MachineSchema, MachineRepoSchema, MachineSessionSchema, MachinesResponseSchema, WorkstationStepResultSchema, } from '@craftspace/shared';
|
|
7
|
+
import { CLI_VERSION, MACHINE_BEAT_BUSY_INTERVAL_MS, MACHINE_BEAT_INTERVAL_MS, MachineBeatResponseSchema, MachineLoginResponseSchema, MachineSchema, MachineRepoSchema, MachineSessionSchema, MachinesResponseSchema, WorkstationStepResultSchema, spreadIfDefined, toSpaceSlug, } from '@craftspace/shared';
|
|
7
8
|
import { confirm, input, select } from '@inquirer/prompts';
|
|
8
9
|
import { z } from 'zod';
|
|
10
|
+
import { migrate } from './migrate.js';
|
|
9
11
|
import { probe } from './probe.js';
|
|
10
12
|
import { runner } from './run.js';
|
|
11
13
|
import { setup } from './setup.js';
|
|
@@ -34,14 +36,19 @@ export const machine = {
|
|
|
34
36
|
schema: MachineLoginResponseSchema,
|
|
35
37
|
});
|
|
36
38
|
await mkdir(machine.home(), { recursive: true, mode: 0o700 });
|
|
37
|
-
await
|
|
39
|
+
await writeConfig({ url, machineId: answer.machine.id, organization: toSpaceSlug(answer.organizationName) });
|
|
38
40
|
await writeFile(tokenPath(), token, { mode: 0o600 });
|
|
39
|
-
await writeAgentConfigs({
|
|
41
|
+
await writeAgentConfigs({ writes: answer.write });
|
|
42
|
+
await wireRepos({ url, token, dirs: clonedDirs(await readEnvironment()) });
|
|
40
43
|
await installService();
|
|
41
44
|
if (install)
|
|
42
45
|
await machine.beatOnce(ui.say);
|
|
43
46
|
return answer.machine;
|
|
44
47
|
},
|
|
48
|
+
async workDir() {
|
|
49
|
+
const organization = (await readConfig())?.organization;
|
|
50
|
+
return organization === undefined ? null : setup.workDir(organization);
|
|
51
|
+
},
|
|
45
52
|
async controlPlane(override) {
|
|
46
53
|
const where = override ?? process.env.CRAFTSPACE_URL ?? (await readConfig())?.url ?? DEFAULT_URL;
|
|
47
54
|
return where.replace(/\/$/, '');
|
|
@@ -74,7 +81,7 @@ export const machine = {
|
|
|
74
81
|
return { machine: found.value, url: found.auth.url };
|
|
75
82
|
},
|
|
76
83
|
async beatOnce(say) {
|
|
77
|
-
const { url, token } = await requireSignedIn();
|
|
84
|
+
const { url, token, organization } = await requireSignedIn();
|
|
78
85
|
const built = await readEnvironment();
|
|
79
86
|
const answer = await call({
|
|
80
87
|
url,
|
|
@@ -89,7 +96,7 @@ export const machine = {
|
|
|
89
96
|
tools: await checks(),
|
|
90
97
|
environment: built?.results ?? [],
|
|
91
98
|
templateId: built?.templateId ?? null,
|
|
92
|
-
workDir: setup.workDir(),
|
|
99
|
+
...(organization === undefined ? {} : { workDir: setup.workDir(organization) }),
|
|
93
100
|
repos: built?.repos ?? [],
|
|
94
101
|
sshUser: os.userInfo().username,
|
|
95
102
|
},
|
|
@@ -97,8 +104,12 @@ export const machine = {
|
|
|
97
104
|
});
|
|
98
105
|
if (answer.cli !== null && isNewer(answer.cli.version))
|
|
99
106
|
updateWanted = answer.cli.version;
|
|
107
|
+
const workDir = setup.workDir(answer.organization);
|
|
108
|
+
if (answer.organization !== organization)
|
|
109
|
+
await writeConfig({ organization: answer.organization });
|
|
110
|
+
await migrate.toLatest({ home: machine.home(), workDir }).catch(() => []);
|
|
100
111
|
await writeAuthorizedKeys(answer.authorizedKeys);
|
|
101
|
-
const building = buildEnvironment(answer.environment, say);
|
|
112
|
+
const building = buildEnvironment({ next: answer.environment, workDir, say });
|
|
102
113
|
if (say === undefined)
|
|
103
114
|
void building.catch(() => undefined);
|
|
104
115
|
else
|
|
@@ -175,7 +186,7 @@ async function requireSignedIn() {
|
|
|
175
186
|
const token = await readToken();
|
|
176
187
|
if (!config || !token)
|
|
177
188
|
throw new Error('This machine is not signed in. Run: cs login --token <token>');
|
|
178
|
-
return { url: config.url, token };
|
|
189
|
+
return { url: config.url, token, ...spreadIfDefined({ organization: config.organization }) };
|
|
179
190
|
}
|
|
180
191
|
async function pick(machines) {
|
|
181
192
|
return select({
|
|
@@ -234,18 +245,21 @@ function nowhereToGo({ url }) {
|
|
|
234
245
|
async function checks() {
|
|
235
246
|
if (probed !== null && Date.now() - probed.at < PROBE_EVERY_MS)
|
|
236
247
|
return probed.tools;
|
|
237
|
-
const
|
|
248
|
+
const dirs = clonedDirs(await readEnvironment());
|
|
249
|
+
const first = await probe.tools({ dirs });
|
|
238
250
|
const unwired = first.some((tool) => tool.id === 'brain' && !tool.ok);
|
|
239
|
-
const found = unwired && installs()
|
|
251
|
+
const found = unwired && installs() && dirs.length > 0
|
|
252
|
+
? await rewire(dirs).then(() => probe.tools({ dirs }), () => first)
|
|
253
|
+
: first;
|
|
240
254
|
probed = { at: Date.now(), tools: found };
|
|
241
255
|
return found;
|
|
242
256
|
}
|
|
243
|
-
async function buildEnvironment(next, say) {
|
|
257
|
+
async function buildEnvironment({ next, workDir, say, }) {
|
|
244
258
|
if (building || !installs() || next.templateId === null)
|
|
245
259
|
return;
|
|
246
260
|
const built = await readEnvironment();
|
|
247
261
|
const buildsSteps = buildIsDue(built, next.templateId);
|
|
248
|
-
const clonesRepos = cloneIsDue(built, next.repos);
|
|
262
|
+
const clonesRepos = cloneIsDue({ built, wanted: next.repos, workDir });
|
|
249
263
|
if (!buildsSteps && !clonesRepos)
|
|
250
264
|
return;
|
|
251
265
|
building = true;
|
|
@@ -257,9 +271,9 @@ async function buildEnvironment(next, say) {
|
|
|
257
271
|
results = await setup.runSteps({ steps: next.steps, say });
|
|
258
272
|
}
|
|
259
273
|
if (clonesRepos) {
|
|
260
|
-
say?.(ui.heading(`Cloning the ${next.repos.length} repos your team works in, into ${
|
|
261
|
-
repos = await setup.cloneRepos({ repos: next.repos, say });
|
|
262
|
-
await
|
|
274
|
+
say?.(ui.heading(`Cloning the ${next.repos.length} repos your team works in, into ${workDir}`));
|
|
275
|
+
repos = await setup.cloneRepos({ repos: next.repos, workDir, say });
|
|
276
|
+
await rewire(repos.filter((repo) => repo.ok).map((repo) => repo.path));
|
|
263
277
|
}
|
|
264
278
|
await writeEnvironment({ templateId: next.templateId, results, repos, at: Date.now() });
|
|
265
279
|
}
|
|
@@ -267,15 +281,20 @@ async function buildEnvironment(next, say) {
|
|
|
267
281
|
building = false;
|
|
268
282
|
}
|
|
269
283
|
}
|
|
270
|
-
function cloneIsDue(built, wanted) {
|
|
284
|
+
function cloneIsDue({ built, wanted, workDir, }) {
|
|
271
285
|
const cloned = built?.repos ?? [];
|
|
272
286
|
if (wanted.length === 0 && cloned.length === 0)
|
|
273
287
|
return false;
|
|
274
288
|
if (wanted.length !== cloned.length)
|
|
275
289
|
return true;
|
|
276
|
-
const missing = wanted.some((repo) => !cloned.some((seen) =>
|
|
290
|
+
const missing = wanted.some((repo) => !cloned.some((seen) => isCloned({ seen, repo, workDir })));
|
|
277
291
|
return missing || Date.now() - (built?.at ?? 0) > REBUILD_AFTER_MS;
|
|
278
292
|
}
|
|
293
|
+
function isCloned({ seen, repo, workDir, }) {
|
|
294
|
+
if (seen.name !== repo.name || seen.ref !== repo.ref || !seen.ok)
|
|
295
|
+
return false;
|
|
296
|
+
return path.dirname(seen.path) === workDir && existsSync(seen.path);
|
|
297
|
+
}
|
|
279
298
|
function startIn(built) {
|
|
280
299
|
return built?.repos.find((repo) => repo.ok)?.path ?? os.homedir();
|
|
281
300
|
}
|
|
@@ -300,9 +319,12 @@ async function writeEnvironment(built) {
|
|
|
300
319
|
function installs() {
|
|
301
320
|
return process.env.CRAFTSPACE_NO_INSTALL !== '1';
|
|
302
321
|
}
|
|
303
|
-
async function rewire() {
|
|
322
|
+
async function rewire(dirs) {
|
|
304
323
|
const { url, token } = await requireSignedIn();
|
|
305
|
-
await
|
|
324
|
+
await wireRepos({ url, token, dirs });
|
|
325
|
+
}
|
|
326
|
+
function clonedDirs(built) {
|
|
327
|
+
return (built?.repos ?? []).filter((repo) => repo.ok).map((repo) => repo.path);
|
|
306
328
|
}
|
|
307
329
|
function report(next) {
|
|
308
330
|
reports.set(next.id, next);
|
|
@@ -374,6 +396,11 @@ async function ensureKey() {
|
|
|
374
396
|
await run('ssh-keygen', ['-t', 'ed25519', '-N', '', '-q', '-f', priv, '-C', `craftspace-${os.hostname()}`]);
|
|
375
397
|
return (await readFile(pub, 'utf8')).trim();
|
|
376
398
|
}
|
|
399
|
+
async function writeConfig(next) {
|
|
400
|
+
const merged = { ...(await readConfig()), ...next };
|
|
401
|
+
await mkdir(machine.home(), { recursive: true, mode: 0o700 });
|
|
402
|
+
await writeFile(configPath(), `${JSON.stringify(merged, null, 2)}\n`, { mode: 0o600 });
|
|
403
|
+
}
|
|
377
404
|
async function readConfig() {
|
|
378
405
|
const raw = await readFile(configPath(), 'utf8').catch(() => null);
|
|
379
406
|
if (raw === null)
|
|
@@ -413,10 +440,45 @@ async function writeAuthorizedKeys(keys) {
|
|
|
413
440
|
function trimEnd(text) {
|
|
414
441
|
return text.replace(/\s+$/, '');
|
|
415
442
|
}
|
|
416
|
-
async function
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
443
|
+
async function wireRepos({ url, token, dirs }) {
|
|
444
|
+
if (dirs.length === 0)
|
|
445
|
+
return;
|
|
446
|
+
const server = { type: 'http', url: `${url}/mcp`, headers: { Authorization: `Bearer ${token}` } };
|
|
447
|
+
await editClaudeConfig((projects) => {
|
|
448
|
+
for (const dir of dirs) {
|
|
449
|
+
const here = isPlainObject(projects[dir]) ? { ...projects[dir] } : {};
|
|
450
|
+
const servers = isPlainObject(here.mcpServers) ? { ...here.mcpServers } : {};
|
|
451
|
+
projects[dir] = { ...here, hasTrustDialogAccepted: true, mcpServers: { ...servers, craftspace: server } };
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
async function unwireRepos() {
|
|
456
|
+
await editClaudeConfig((projects) => {
|
|
457
|
+
for (const [dir, found] of Object.entries(projects)) {
|
|
458
|
+
if (!isPlainObject(found) || !isPlainObject(found.mcpServers))
|
|
459
|
+
continue;
|
|
460
|
+
const servers = { ...found.mcpServers };
|
|
461
|
+
delete servers.craftspace;
|
|
462
|
+
projects[dir] = { ...found, mcpServers: servers };
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
async function editClaudeConfig(edit) {
|
|
467
|
+
const target = path.join(os.homedir(), CLAUDE_FILE);
|
|
468
|
+
const raw = await readFile(target, 'utf8').catch(() => null);
|
|
469
|
+
const parsed = raw === null ? {} : JSON.parse(raw);
|
|
470
|
+
const projects = isPlainObject(parsed.projects) ? { ...parsed.projects } : {};
|
|
471
|
+
const before = JSON.stringify(projects);
|
|
472
|
+
edit(projects);
|
|
473
|
+
if (JSON.stringify(projects) === before)
|
|
474
|
+
return;
|
|
475
|
+
await writeFile(target, `${JSON.stringify({ ...parsed, projects }, null, 2)}\n`, { mode: 0o600 });
|
|
476
|
+
}
|
|
477
|
+
async function writeAgentConfigs({ writes, }) {
|
|
478
|
+
if (writes.length === 0)
|
|
479
|
+
return;
|
|
480
|
+
const owned = [...(await readOwned())];
|
|
481
|
+
for (const write of writes) {
|
|
420
482
|
const target = expand(write.path);
|
|
421
483
|
await mkdir(path.dirname(target), { recursive: true });
|
|
422
484
|
const current = await readFile(target, 'utf8').catch(() => null);
|
|
@@ -426,38 +488,14 @@ async function writeAgentConfigs({ url, token, writes, }) {
|
|
|
426
488
|
await writeFile(target, `${JSON.stringify(merged, null, 2)}\n`, { mode: 0o600 });
|
|
427
489
|
owned.push(target);
|
|
428
490
|
}
|
|
429
|
-
await writeFile(ownedPath(), JSON.stringify({ paths: owned }, null, 2));
|
|
430
|
-
await acceptClaudeGates(serversIn(planned));
|
|
431
|
-
}
|
|
432
|
-
function serversIn(writes) {
|
|
433
|
-
return writes.flatMap((write) => isPlainObject(write.contents.mcpServers) ? Object.keys(write.contents.mcpServers) : []);
|
|
491
|
+
await writeFile(ownedPath(), JSON.stringify({ paths: [...new Set(owned)] }, null, 2));
|
|
434
492
|
}
|
|
435
|
-
async function
|
|
436
|
-
const
|
|
437
|
-
const raw = await readFile(target, 'utf8').catch(() => null);
|
|
493
|
+
async function readOwned() {
|
|
494
|
+
const raw = await readFile(ownedPath(), 'utf8').catch(() => null);
|
|
438
495
|
if (raw === null)
|
|
439
|
-
return;
|
|
440
|
-
const parsed = JSON.parse(raw);
|
|
441
|
-
|
|
442
|
-
const opened = { ...projects };
|
|
443
|
-
let moved = false;
|
|
444
|
-
for (const dir of dirs) {
|
|
445
|
-
const found = opened[dir];
|
|
446
|
-
const here = isPlainObject(found) ? { ...found } : {};
|
|
447
|
-
const enabled = new Set([...asArray(here.enabledMcpjsonServers), ...servers]);
|
|
448
|
-
if (here.hasTrustDialogAccepted === true && enabled.size === asArray(here.enabledMcpjsonServers).length)
|
|
449
|
-
continue;
|
|
450
|
-
here.hasTrustDialogAccepted = true;
|
|
451
|
-
here.enabledMcpjsonServers = [...enabled];
|
|
452
|
-
opened[dir] = here;
|
|
453
|
-
moved = true;
|
|
454
|
-
}
|
|
455
|
-
if (!moved)
|
|
456
|
-
return;
|
|
457
|
-
await writeFile(target, `${JSON.stringify({ ...parsed, projects: opened }, null, 2)}\n`, { mode: 0o600 });
|
|
458
|
-
}
|
|
459
|
-
function asArray(value) {
|
|
460
|
-
return Array.isArray(value) ? value : [];
|
|
496
|
+
return [];
|
|
497
|
+
const parsed = OwnedSchema.safeParse(JSON.parse(raw));
|
|
498
|
+
return parsed.success ? parsed.data.paths : [];
|
|
461
499
|
}
|
|
462
500
|
function mergeInto(current, incoming) {
|
|
463
501
|
const merged = { ...current };
|
|
@@ -471,16 +509,9 @@ function mergeInto(current, incoming) {
|
|
|
471
509
|
function isPlainObject(value) {
|
|
472
510
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
473
511
|
}
|
|
474
|
-
function defaultWrites({ url, token }) {
|
|
475
|
-
const server = { craftspace: { type: 'http', url: `${url}/mcp`, headers: { Authorization: `Bearer ${token}` } } };
|
|
476
|
-
return [{ path: '~/.mcp.json', contents: { mcpServers: server } }];
|
|
477
|
-
}
|
|
478
512
|
async function removeAgentConfigs() {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
return;
|
|
482
|
-
const owned = OwnedSchema.parse(JSON.parse(raw));
|
|
483
|
-
for (const target of owned.paths) {
|
|
513
|
+
await unwireRepos();
|
|
514
|
+
for (const target of await readOwned()) {
|
|
484
515
|
const current = await readFile(target, 'utf8').catch(() => null);
|
|
485
516
|
if (current === null)
|
|
486
517
|
continue;
|
|
@@ -606,7 +637,7 @@ export function nextDelayMs(failures, busy = false) {
|
|
|
606
637
|
function messageOf(error) {
|
|
607
638
|
return error instanceof Error ? error.message : String(error);
|
|
608
639
|
}
|
|
609
|
-
const ConfigSchema = z.object({ url: z.string(), machineId: z.string() });
|
|
640
|
+
const ConfigSchema = z.object({ url: z.string(), machineId: z.string(), organization: z.string().optional() });
|
|
610
641
|
const BuiltSchema = z.object({
|
|
611
642
|
templateId: z.string(),
|
|
612
643
|
results: z.array(WorkstationStepResultSchema),
|
|
@@ -614,6 +645,7 @@ const BuiltSchema = z.object({
|
|
|
614
645
|
at: z.number(),
|
|
615
646
|
});
|
|
616
647
|
const OwnedSchema = z.object({ paths: z.array(z.string()) });
|
|
648
|
+
const CLAUDE_FILE = '.claude.json';
|
|
617
649
|
const SERVICE_NAME = 'craftspace';
|
|
618
650
|
const LAUNCH_LABEL = 'app.craftspace.machine';
|
|
619
651
|
const REQUEST_TIMEOUT_MS = 15_000;
|
package/dist/migrate.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { mkdir, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
export const migrate = {
|
|
7
|
+
async toLatest({ home, workDir }) {
|
|
8
|
+
const done = new Set(await alreadyRan(home));
|
|
9
|
+
const ran = [];
|
|
10
|
+
for (const step of STEPS) {
|
|
11
|
+
if (done.has(step.id))
|
|
12
|
+
continue;
|
|
13
|
+
if (!(await step.run({ workDir }).catch(() => false)))
|
|
14
|
+
continue;
|
|
15
|
+
done.add(step.id);
|
|
16
|
+
ran.push(step.id);
|
|
17
|
+
}
|
|
18
|
+
if (ran.length === 0)
|
|
19
|
+
return ran;
|
|
20
|
+
await mkdir(home, { recursive: true, mode: 0o700 });
|
|
21
|
+
await writeFile(path.join(home, RAN_FILE), `${JSON.stringify({ ran: [...done] }, null, 2)}\n`, { mode: 0o600 });
|
|
22
|
+
return ran;
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
const STEPS = [
|
|
26
|
+
{
|
|
27
|
+
id: '0001-work-dir-under-org',
|
|
28
|
+
async run({ workDir }) {
|
|
29
|
+
if (workDir === null)
|
|
30
|
+
return false;
|
|
31
|
+
const flat = path.dirname(workDir);
|
|
32
|
+
const org = path.basename(workDir);
|
|
33
|
+
const staged = `${org}${MOVING}`;
|
|
34
|
+
if (existsSync(path.join(workDir, '.git')))
|
|
35
|
+
await rename(workDir, path.join(flat, staged));
|
|
36
|
+
const entries = await readdir(flat, { withFileTypes: true }).catch(() => []);
|
|
37
|
+
const moving = entries.filter((entry) => entry.isDirectory() && path.join(flat, entry.name) !== workDir);
|
|
38
|
+
if (moving.length === 0)
|
|
39
|
+
return true;
|
|
40
|
+
await mkdir(workDir, { recursive: true });
|
|
41
|
+
for (const entry of moving) {
|
|
42
|
+
const to = path.join(workDir, entry.name === staged ? org : entry.name);
|
|
43
|
+
await rename(path.join(flat, entry.name), to).catch(() => undefined);
|
|
44
|
+
}
|
|
45
|
+
return true;
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: '0002-mcp-out-of-home',
|
|
50
|
+
async run() {
|
|
51
|
+
const target = path.join(os.homedir(), '.mcp.json');
|
|
52
|
+
const raw = await readFile(target, 'utf8').catch(() => null);
|
|
53
|
+
if (raw === null)
|
|
54
|
+
return true;
|
|
55
|
+
const parsed = JSON.parse(raw);
|
|
56
|
+
const servers = parsed.mcpServers;
|
|
57
|
+
if (typeof servers !== 'object' || servers === null || !('craftspace' in servers))
|
|
58
|
+
return true;
|
|
59
|
+
delete servers.craftspace;
|
|
60
|
+
const emptied = Object.keys(servers).length === 0 && Object.keys(parsed).length === 1;
|
|
61
|
+
const ours = (await readFile(`${target}.craftspace-backup`, 'utf8').catch(() => null)) === null;
|
|
62
|
+
if (emptied && ours)
|
|
63
|
+
await rm(target, { force: true });
|
|
64
|
+
else
|
|
65
|
+
await writeFile(target, `${JSON.stringify(parsed, null, 2)}\n`, { mode: 0o600 });
|
|
66
|
+
return true;
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
async function alreadyRan(home) {
|
|
71
|
+
const raw = await readFile(path.join(home, RAN_FILE), 'utf8').catch(() => null);
|
|
72
|
+
if (raw === null)
|
|
73
|
+
return [];
|
|
74
|
+
const parsed = RanSchema.safeParse(JSON.parse(raw));
|
|
75
|
+
return parsed.success ? parsed.data.ran : [];
|
|
76
|
+
}
|
|
77
|
+
const RanSchema = z.object({ ran: z.array(z.string()) });
|
|
78
|
+
const RAN_FILE = 'migrations.json';
|
|
79
|
+
const MOVING = '.craftspace-moving';
|
package/dist/probe.d.ts
CHANGED
package/dist/probe.js
CHANGED
|
@@ -3,22 +3,30 @@ import { readFile } from 'node:fs/promises';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
export const probe = {
|
|
6
|
-
async tools({
|
|
7
|
-
return Promise.all([brainWired(
|
|
6
|
+
async tools({ dirs }) {
|
|
7
|
+
return Promise.all([brainWired(dirs), sshListening()]);
|
|
8
8
|
},
|
|
9
9
|
};
|
|
10
|
-
async function brainWired(
|
|
11
|
-
|
|
10
|
+
async function brainWired(dirs) {
|
|
11
|
+
if (dirs.length === 0) {
|
|
12
|
+
return { id: 'brain', ok: false, detail: 'no repos cloned here yet, so there is nothing to wire it into' };
|
|
13
|
+
}
|
|
14
|
+
const projects = await claudeProjects();
|
|
15
|
+
const unwired = dirs.filter((dir) => !('craftspace' in mcpServersIn(projects[dir])));
|
|
16
|
+
return unwired.length === 0
|
|
17
|
+
? { id: 'brain', ok: true, detail: `wired into ${dirs.length} repo${dirs.length === 1 ? '' : 's'}` }
|
|
18
|
+
: { id: 'brain', ok: false, detail: `not wired into ${unwired.map(short).join(', ')}`.slice(0, DETAIL_MAX) };
|
|
19
|
+
}
|
|
20
|
+
async function claudeProjects() {
|
|
21
|
+
const raw = await readFile(path.join(os.homedir(), '.claude.json'), 'utf8').catch(() => null);
|
|
12
22
|
if (raw === null)
|
|
13
|
-
return {
|
|
23
|
+
return {};
|
|
14
24
|
const parsed = JSON.parse(raw);
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
detail: wired ? short(mcpPath) : `${short(mcpPath)} has no craftspace entry`,
|
|
21
|
-
};
|
|
25
|
+
return parsed.projects ?? {};
|
|
26
|
+
}
|
|
27
|
+
function mcpServersIn(project) {
|
|
28
|
+
const servers = project?.mcpServers;
|
|
29
|
+
return servers ?? {};
|
|
22
30
|
}
|
|
23
31
|
function sshListening() {
|
|
24
32
|
return new Promise((resolve) => {
|
|
@@ -37,3 +45,4 @@ function short(target) {
|
|
|
37
45
|
return target.startsWith(os.homedir()) ? `~${target.slice(os.homedir().length)}` : target;
|
|
38
46
|
}
|
|
39
47
|
const SSH_PROBE_TIMEOUT_MS = 1_500;
|
|
48
|
+
const DETAIL_MAX = 200;
|
package/dist/setup.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { type MachineRepo, type WorkstationRepo, type WorkstationStep, type WorkstationStepResult } from '@craftspace/shared';
|
|
2
2
|
export declare const setup: {
|
|
3
3
|
widenPath(): void;
|
|
4
|
-
workDir(): string;
|
|
5
|
-
|
|
4
|
+
workDir(organization: string): string;
|
|
5
|
+
workRoot(): string;
|
|
6
|
+
cloneRepos({ repos, workDir, say, }: {
|
|
6
7
|
repos: WorkstationRepo[];
|
|
8
|
+
workDir: string;
|
|
7
9
|
say?: (line: string) => void;
|
|
8
10
|
}): Promise<MachineRepo[]>;
|
|
9
11
|
runSteps({ steps, say, }: {
|
package/dist/setup.js
CHANGED
|
@@ -2,7 +2,7 @@ import { execFile } from 'node:child_process';
|
|
|
2
2
|
import { mkdir } from 'node:fs/promises';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
-
import { WORKSTATION_STEP_DETAIL_MAX_CHARS, } from '@craftspace/shared';
|
|
5
|
+
import { WORKSTATION_STEP_DETAIL_MAX_CHARS, toSpaceSlug, } from '@craftspace/shared';
|
|
6
6
|
import { ui } from './ui.js';
|
|
7
7
|
export const setup = {
|
|
8
8
|
widenPath() {
|
|
@@ -11,17 +11,20 @@ export const setup = {
|
|
|
11
11
|
if (missing.length > 0)
|
|
12
12
|
process.env.PATH = [...current, ...missing].join(path.delimiter);
|
|
13
13
|
},
|
|
14
|
-
workDir() {
|
|
14
|
+
workDir(organization) {
|
|
15
|
+
return path.join(setup.workRoot(), toSpaceSlug(organization) || FALLBACK_ORG);
|
|
16
|
+
},
|
|
17
|
+
workRoot() {
|
|
15
18
|
return path.join(os.homedir(), WORK_DIR_NAME);
|
|
16
19
|
},
|
|
17
|
-
async cloneRepos({ repos, say, }) {
|
|
20
|
+
async cloneRepos({ repos, workDir, say, }) {
|
|
18
21
|
if (repos.length === 0)
|
|
19
22
|
return [];
|
|
20
|
-
await mkdir(
|
|
23
|
+
await mkdir(workDir, { recursive: true });
|
|
21
24
|
const cloned = [];
|
|
22
25
|
for (const [index, repo] of repos.entries()) {
|
|
23
26
|
say?.(ui.progress({ name: repo.name, done: index, total: repos.length }));
|
|
24
|
-
const at = path.join(
|
|
27
|
+
const at = path.join(workDir, repo.name.split('/').pop() ?? repo.name);
|
|
25
28
|
const outcome = await checkout(repo, at);
|
|
26
29
|
cloned.push({ name: repo.name, ref: repo.ref, path: at, ...outcome });
|
|
27
30
|
say?.(`${ui.clear()}${outcome.ok ? ui.ok(`${repo.name} · ${outcome.detail}`) : ui.bad(repo.name, outcome.detail)}`);
|
|
@@ -112,6 +115,7 @@ function toolDirs() {
|
|
|
112
115
|
];
|
|
113
116
|
}
|
|
114
117
|
const WORK_DIR_NAME = 'craftspace';
|
|
118
|
+
const FALLBACK_ORG = 'team';
|
|
115
119
|
const CLONE_DEPTH = 20;
|
|
116
120
|
const CLONE_TIMEOUT_MS = 900_000;
|
|
117
121
|
const STEP_TIMEOUT_MS = 600_000;
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@craftspace/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Sign a Mac or Linux machine into Craftspace and keep its agent setup in step.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"homepage": "https://craftspace.app",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "git+https://github.com/
|
|
10
|
+
"url": "git+https://github.com/craftspacehq/craftspace.git",
|
|
11
11
|
"directory": "packages/app/cli"
|
|
12
12
|
},
|
|
13
13
|
"engines": {
|