@craftspace/cli 0.9.0 → 0.10.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 +265 -148
- package/dist/machine.d.ts +30 -5
- package/dist/machine.js +231 -113
- package/dist/migrate.d.ts +1 -0
- package/dist/migrate.js +18 -15
- package/dist/probe.d.ts +2 -1
- package/dist/probe.js +5 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -19892,7 +19892,7 @@ var SaveWorkstationBodySchema = external_exports.object({
|
|
|
19892
19892
|
var WorkstationResponseSchema = external_exports.object({ workstation: WorkstationSchema });
|
|
19893
19893
|
|
|
19894
19894
|
// ../../shared/dist/machines/machine.js
|
|
19895
|
-
var CLI_VERSION = "0.
|
|
19895
|
+
var CLI_VERSION = "0.10.0";
|
|
19896
19896
|
var CLI_PACKAGE = "@craftspace/cli";
|
|
19897
19897
|
var CLI_INSTALL = `npm i -g ${CLI_PACKAGE}`;
|
|
19898
19898
|
var MACHINE_BEAT_INTERVAL_MS = 15e3;
|
|
@@ -20384,7 +20384,7 @@ var MemberActivityListResponse = external_exports.object({
|
|
|
20384
20384
|
// dist/machine.js
|
|
20385
20385
|
import { execFile as execFile3, spawn as spawn2 } from "node:child_process";
|
|
20386
20386
|
import { existsSync as existsSync2 } from "node:fs";
|
|
20387
|
-
import { mkdir as mkdir3, readFile as readFile3, readdir as readdir2, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
|
|
20387
|
+
import { mkdir as mkdir3, readFile as readFile3, readdir as readdir2, rename as rename2, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
|
|
20388
20388
|
import os6 from "node:os";
|
|
20389
20389
|
import path5 from "node:path";
|
|
20390
20390
|
import { promisify as promisify2 } from "node:util";
|
|
@@ -22259,6 +22259,24 @@ import { mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promise
|
|
|
22259
22259
|
import os from "node:os";
|
|
22260
22260
|
import path2 from "node:path";
|
|
22261
22261
|
var migrate = {
|
|
22262
|
+
async stripGlobalMcp() {
|
|
22263
|
+
const target = path2.join(os.homedir(), ".mcp.json");
|
|
22264
|
+
const raw = await readFile(target, "utf8").catch(() => null);
|
|
22265
|
+
if (raw === null)
|
|
22266
|
+
return;
|
|
22267
|
+
const parsed = JSON.parse(raw);
|
|
22268
|
+
const servers = parsed.mcpServers;
|
|
22269
|
+
if (typeof servers !== "object" || servers === null || !("craftspace" in servers))
|
|
22270
|
+
return;
|
|
22271
|
+
delete servers.craftspace;
|
|
22272
|
+
const emptied = Object.keys(servers).length === 0 && Object.keys(parsed).length === 1;
|
|
22273
|
+
const ours = await readFile(`${target}.craftspace-backup`, "utf8").catch(() => null) === null;
|
|
22274
|
+
if (emptied && ours)
|
|
22275
|
+
await rm(target, { force: true });
|
|
22276
|
+
else
|
|
22277
|
+
await writeFile(target, `${JSON.stringify(parsed, null, 2)}
|
|
22278
|
+
`, { mode: 384 });
|
|
22279
|
+
},
|
|
22262
22280
|
async toLatest({ home, workDir }) {
|
|
22263
22281
|
const done = new Set(await alreadyRan(home));
|
|
22264
22282
|
const ran = [];
|
|
@@ -22304,22 +22322,7 @@ var STEPS = [
|
|
|
22304
22322
|
{
|
|
22305
22323
|
id: "0002-mcp-out-of-home",
|
|
22306
22324
|
async run() {
|
|
22307
|
-
|
|
22308
|
-
const raw = await readFile(target, "utf8").catch(() => null);
|
|
22309
|
-
if (raw === null)
|
|
22310
|
-
return true;
|
|
22311
|
-
const parsed = JSON.parse(raw);
|
|
22312
|
-
const servers = parsed.mcpServers;
|
|
22313
|
-
if (typeof servers !== "object" || servers === null || !("craftspace" in servers))
|
|
22314
|
-
return true;
|
|
22315
|
-
delete servers.craftspace;
|
|
22316
|
-
const emptied = Object.keys(servers).length === 0 && Object.keys(parsed).length === 1;
|
|
22317
|
-
const ours = await readFile(`${target}.craftspace-backup`, "utf8").catch(() => null) === null;
|
|
22318
|
-
if (emptied && ours)
|
|
22319
|
-
await rm(target, { force: true });
|
|
22320
|
-
else
|
|
22321
|
-
await writeFile(target, `${JSON.stringify(parsed, null, 2)}
|
|
22322
|
-
`, { mode: 384 });
|
|
22325
|
+
await migrate.stripGlobalMcp();
|
|
22323
22326
|
return true;
|
|
22324
22327
|
}
|
|
22325
22328
|
}
|
|
@@ -22341,16 +22344,17 @@ import { readFile as readFile2 } from "node:fs/promises";
|
|
|
22341
22344
|
import os2 from "node:os";
|
|
22342
22345
|
import path3 from "node:path";
|
|
22343
22346
|
var probe = {
|
|
22344
|
-
async tools({ dirs }) {
|
|
22345
|
-
return Promise.all([brainWired(dirs), sshListening()]);
|
|
22347
|
+
async tools({ dirs, organization }) {
|
|
22348
|
+
return Promise.all([brainWired({ dirs, organization }), sshListening()]);
|
|
22346
22349
|
}
|
|
22347
22350
|
};
|
|
22348
|
-
async function brainWired(dirs) {
|
|
22351
|
+
async function brainWired({ dirs, organization }) {
|
|
22349
22352
|
if (dirs.length === 0) {
|
|
22350
22353
|
return { id: "brain", ok: false, detail: "no repos cloned here yet, so there is nothing to wire it into" };
|
|
22351
22354
|
}
|
|
22352
22355
|
const projects = await claudeProjects();
|
|
22353
|
-
const
|
|
22356
|
+
const wanted = `craftspace-${organization}`;
|
|
22357
|
+
const unwired = dirs.filter((dir) => !(wanted in mcpServersIn(projects[dir])));
|
|
22354
22358
|
return unwired.length === 0 ? { id: "brain", ok: true, detail: `wired into ${dirs.length} repo${dirs.length === 1 ? "" : "s"}` } : { id: "brain", ok: false, detail: `not wired into ${unwired.map(short).join(", ")}`.slice(0, DETAIL_MAX) };
|
|
22355
22359
|
}
|
|
22356
22360
|
async function claudeProjects() {
|
|
@@ -23251,9 +23255,24 @@ var INSTALL_TIMEOUT_MS = 18e4;
|
|
|
23251
23255
|
// dist/machine.js
|
|
23252
23256
|
var run2 = promisify2(execFile3);
|
|
23253
23257
|
var machine = {
|
|
23254
|
-
|
|
23258
|
+
root() {
|
|
23255
23259
|
return process.env.CRAFTSPACE_CLI_HOME ?? path5.join(os6.homedir(), ".craftspace");
|
|
23256
23260
|
},
|
|
23261
|
+
async seats() {
|
|
23262
|
+
await settleLayout();
|
|
23263
|
+
const entries = await readdir2(machine.root(), { withFileTypes: true }).catch(() => []);
|
|
23264
|
+
const seats = [];
|
|
23265
|
+
for (const entry of entries.filter((found) => found.isDirectory())) {
|
|
23266
|
+
const home = path5.join(machine.root(), entry.name);
|
|
23267
|
+
const config2 = await readConfig(home);
|
|
23268
|
+
if (config2 !== null)
|
|
23269
|
+
seats.push({ home, config: config2 });
|
|
23270
|
+
}
|
|
23271
|
+
const legacy = await readConfig(machine.root());
|
|
23272
|
+
if (legacy !== null)
|
|
23273
|
+
seats.push({ home: machine.root(), config: legacy });
|
|
23274
|
+
return seats;
|
|
23275
|
+
},
|
|
23257
23276
|
async login({ token, url: url2, install }) {
|
|
23258
23277
|
const answer = await call({
|
|
23259
23278
|
url: url2,
|
|
@@ -23271,26 +23290,29 @@ var machine = {
|
|
|
23271
23290
|
},
|
|
23272
23291
|
schema: MachineLoginResponseSchema
|
|
23273
23292
|
});
|
|
23274
|
-
|
|
23275
|
-
|
|
23276
|
-
await
|
|
23293
|
+
const organization = toSpaceSlug(answer.organizationName) || "team";
|
|
23294
|
+
const account = answer.machine.ownerAccountId;
|
|
23295
|
+
await refuseSecondAccount({ account, organization });
|
|
23296
|
+
const home = path5.join(machine.root(), organization);
|
|
23297
|
+
await mkdir3(home, { recursive: true, mode: 448 });
|
|
23298
|
+
await writeConfig(home, { url: url2, machineId: answer.machine.id, organization, account });
|
|
23299
|
+
await writeFile2(tokenPath(home), token, { mode: 384 });
|
|
23277
23300
|
await writeAgentConfigs({ writes: answer.write });
|
|
23278
|
-
await wireRepos({ url: url2, token, dirs: clonedDirs(await readEnvironment()) });
|
|
23301
|
+
await wireRepos({ url: url2, token, organization, dirs: clonedDirs(await readEnvironment(home)) });
|
|
23279
23302
|
await installService();
|
|
23280
23303
|
if (install)
|
|
23281
|
-
await machine.beatOnce(ui.say);
|
|
23282
|
-
return answer.machine;
|
|
23304
|
+
await machine.beatOnce({ home, say: ui.say });
|
|
23305
|
+
return { url: url2, token, machine: answer.machine, organization };
|
|
23283
23306
|
},
|
|
23284
|
-
async workDir() {
|
|
23285
|
-
|
|
23286
|
-
return organization === void 0 ? null : setup.workDir(organization);
|
|
23307
|
+
async workDir(organization) {
|
|
23308
|
+
return setup.workDir(organization);
|
|
23287
23309
|
},
|
|
23288
23310
|
async controlPlane(override) {
|
|
23289
|
-
const where = override ?? process.env.CRAFTSPACE_URL ?? (await
|
|
23311
|
+
const where = override ?? process.env.CRAFTSPACE_URL ?? (await anySeat())?.config.url ?? DEFAULT_URL;
|
|
23290
23312
|
return where.replace(/\/$/, "");
|
|
23291
23313
|
},
|
|
23292
23314
|
async signIn({ url: url2, why, install = false }) {
|
|
23293
|
-
const where = url2 ?? (await
|
|
23315
|
+
const where = url2 ?? (await anySeat())?.config.url ?? DEFAULT_URL;
|
|
23294
23316
|
if (!process.stdin.isTTY)
|
|
23295
23317
|
throw new Error(`${why} Run: cs login --token <token>`);
|
|
23296
23318
|
const page = `${where}/workstations`;
|
|
@@ -23312,16 +23334,34 @@ ${ui.attention(why)}
|
|
|
23312
23334
|
throw new Error("That line carries no key.");
|
|
23313
23335
|
const signed = await machine.login({ token, url: where, install });
|
|
23314
23336
|
ui.say(`
|
|
23315
|
-
${ui.ok(`Signed in as ${signed.ownerName}. This machine is ${ui.name(signed.name)}.`)}`);
|
|
23316
|
-
return
|
|
23337
|
+
${ui.ok(`Signed in as ${signed.machine.ownerName}. This machine is ${ui.name(signed.machine.name)}.`)}`);
|
|
23338
|
+
return signed;
|
|
23317
23339
|
},
|
|
23318
23340
|
async status() {
|
|
23319
|
-
const
|
|
23320
|
-
|
|
23341
|
+
const seats = await machine.seats();
|
|
23342
|
+
if (seats.length === 0) {
|
|
23343
|
+
const signed = await machine.signIn({ why: "This machine is not signed in to Craftspace yet." });
|
|
23344
|
+
return [{ machine: signed.machine, url: signed.url, organization: signed.organization }];
|
|
23345
|
+
}
|
|
23346
|
+
const found = [];
|
|
23347
|
+
for (const seat of seats) {
|
|
23348
|
+
const live = await withAuth(await authOf(seat), (auth) => call({ ...auth, method: "GET", path: "/api/machines/me", schema: MachineSchema }));
|
|
23349
|
+
found.push({ machine: live.value, url: live.auth.url, organization: seat.config.organization ?? "?" });
|
|
23350
|
+
}
|
|
23351
|
+
return found;
|
|
23352
|
+
},
|
|
23353
|
+
async beatAll(say) {
|
|
23354
|
+
const seats = await machine.seats();
|
|
23355
|
+
if (seats.length === 0)
|
|
23356
|
+
throw new Error("This machine is not signed in. Run: cs login --token <token>");
|
|
23357
|
+
let busy = false;
|
|
23358
|
+
for (const seat of seats)
|
|
23359
|
+
busy = await machine.beatOnce({ home: seat.home, say }) || busy;
|
|
23360
|
+
return busy;
|
|
23321
23361
|
},
|
|
23322
|
-
async beatOnce(say) {
|
|
23323
|
-
const { url: url2, token, organization } = await requireSignedIn();
|
|
23324
|
-
const built = await readEnvironment();
|
|
23362
|
+
async beatOnce({ home, say }) {
|
|
23363
|
+
const { url: url2, token, organization } = await requireSignedIn(home);
|
|
23364
|
+
const built = await readEnvironment(home);
|
|
23325
23365
|
const answer = await call({
|
|
23326
23366
|
url: url2,
|
|
23327
23367
|
token,
|
|
@@ -23331,8 +23371,8 @@ ${ui.ok(`Signed in as ${signed.ownerName}. This machine is ${ui.name(signed.name
|
|
|
23331
23371
|
cli: CLI_VERSION,
|
|
23332
23372
|
...load2(),
|
|
23333
23373
|
sessions: await readSessions(),
|
|
23334
|
-
runs: drainReports(),
|
|
23335
|
-
tools: await checks(),
|
|
23374
|
+
runs: drainReports(home),
|
|
23375
|
+
tools: await checks(home),
|
|
23336
23376
|
environment: built?.results ?? [],
|
|
23337
23377
|
templateId: built?.templateId ?? null,
|
|
23338
23378
|
...organization === void 0 ? {} : { workDir: setup.workDir(organization) },
|
|
@@ -23345,10 +23385,10 @@ ${ui.ok(`Signed in as ${signed.ownerName}. This machine is ${ui.name(signed.name
|
|
|
23345
23385
|
updateWanted = answer.cli.version;
|
|
23346
23386
|
const workDir = setup.workDir(answer.organization);
|
|
23347
23387
|
if (answer.organization !== organization)
|
|
23348
|
-
await writeConfig({ organization: answer.organization });
|
|
23349
|
-
await migrate.toLatest({ home: machine.
|
|
23350
|
-
await writeAuthorizedKeys(answer.authorizedKeys);
|
|
23351
|
-
const building2 = buildEnvironment({ next: answer.environment, workDir, say });
|
|
23388
|
+
await writeConfig(home, { organization: answer.organization });
|
|
23389
|
+
await migrate.toLatest({ home: machine.root(), workDir }).catch(() => []);
|
|
23390
|
+
await writeAuthorizedKeys({ organization: answer.organization, keys: answer.authorizedKeys });
|
|
23391
|
+
const building2 = buildEnvironment({ next: answer.environment, home, workDir, say });
|
|
23352
23392
|
if (say === void 0)
|
|
23353
23393
|
void building2.catch(() => void 0);
|
|
23354
23394
|
else
|
|
@@ -23357,7 +23397,7 @@ ${ui.ok(`Signed in as ${signed.ownerName}. This machine is ${ui.name(signed.name
|
|
|
23357
23397
|
if (running.has(work.id))
|
|
23358
23398
|
continue;
|
|
23359
23399
|
running.add(work.id);
|
|
23360
|
-
void runner.unattended({ work, cwd: startIn(built), onProgress: report }).then(report).catch((error51) => report({ id: work.id, state: "failed", output: messageOf(error51) })).finally(() => running.delete(work.id));
|
|
23400
|
+
void runner.unattended({ work, cwd: startIn(built), onProgress: (next) => report(home, next) }).then((next) => report(home, next)).catch((error51) => report(home, { id: work.id, state: "failed", output: messageOf(error51) })).finally(() => running.delete(work.id));
|
|
23361
23401
|
}
|
|
23362
23402
|
return running.size > 0 || answer.work.length > 0;
|
|
23363
23403
|
},
|
|
@@ -23378,7 +23418,7 @@ ${ui.ok(`Signed in as ${signed.ownerName}. This machine is ${ui.name(signed.name
|
|
|
23378
23418
|
}
|
|
23379
23419
|
}
|
|
23380
23420
|
try {
|
|
23381
|
-
busy = await machine.
|
|
23421
|
+
busy = await machine.beatAll();
|
|
23382
23422
|
failures = 0;
|
|
23383
23423
|
} catch (error51) {
|
|
23384
23424
|
failures += 1;
|
|
@@ -23389,7 +23429,7 @@ ${ui.ok(`Signed in as ${signed.ownerName}. This machine is ${ui.name(signed.name
|
|
|
23389
23429
|
}
|
|
23390
23430
|
},
|
|
23391
23431
|
async run() {
|
|
23392
|
-
const first = await
|
|
23432
|
+
const first = await authOfAnySeatOrAsk();
|
|
23393
23433
|
const { auth, value: answer } = await withAuth(first, (live) => call({ ...live, method: "GET", path: "/api/machines", schema: MachinesResponseSchema }));
|
|
23394
23434
|
if (answer.machines.length === 0)
|
|
23395
23435
|
return nowhereToGo(auth);
|
|
@@ -23407,23 +23447,55 @@ ${ui.ok(`Signed in as ${signed.ownerName}. This machine is ${ui.name(signed.name
|
|
|
23407
23447
|
return { text: BEATING, ok: true };
|
|
23408
23448
|
},
|
|
23409
23449
|
async logout() {
|
|
23410
|
-
const
|
|
23411
|
-
|
|
23412
|
-
|
|
23413
|
-
|
|
23414
|
-
|
|
23415
|
-
|
|
23450
|
+
for (const seat of await machine.seats()) {
|
|
23451
|
+
const token = await readToken(seat.home);
|
|
23452
|
+
if (token === null)
|
|
23453
|
+
continue;
|
|
23454
|
+
await call({
|
|
23455
|
+
url: seat.config.url,
|
|
23456
|
+
token,
|
|
23457
|
+
method: "POST",
|
|
23458
|
+
path: "/api/machines/logout",
|
|
23459
|
+
schema: external_exports.object({})
|
|
23460
|
+
}).catch(() => void 0);
|
|
23461
|
+
await removeAgentConfigs(seat.home);
|
|
23462
|
+
if (seat.config.organization !== void 0) {
|
|
23463
|
+
await writeAuthorizedKeys({ organization: seat.config.organization, keys: [] });
|
|
23464
|
+
}
|
|
23465
|
+
}
|
|
23466
|
+
await unwireRepos();
|
|
23416
23467
|
await uninstallService();
|
|
23417
|
-
await rm2(machine.
|
|
23468
|
+
await rm2(machine.root(), { recursive: true, force: true });
|
|
23418
23469
|
}
|
|
23419
23470
|
};
|
|
23420
|
-
async function requireSignedIn() {
|
|
23421
|
-
const config2 = await readConfig();
|
|
23422
|
-
const token = await readToken();
|
|
23471
|
+
async function requireSignedIn(home) {
|
|
23472
|
+
const config2 = await readConfig(home);
|
|
23473
|
+
const token = await readToken(home);
|
|
23423
23474
|
if (!config2 || !token)
|
|
23424
23475
|
throw new Error("This machine is not signed in. Run: cs login --token <token>");
|
|
23425
23476
|
return { url: config2.url, token, ...spreadIfDefined({ organization: config2.organization }) };
|
|
23426
23477
|
}
|
|
23478
|
+
async function refuseSecondAccount({ account, organization }) {
|
|
23479
|
+
const taken = (await machine.seats()).find((seat) => seat.config.account !== void 0 && seat.config.account !== account);
|
|
23480
|
+
if (taken === void 0)
|
|
23481
|
+
return;
|
|
23482
|
+
throw new Error(`This box is already signed in as someone else, under ${taken.config.organization ?? "another org"}. One box holds one account, any number of its orgs. Run: cs logout, then sign in for ${organization}.`);
|
|
23483
|
+
}
|
|
23484
|
+
async function anySeat() {
|
|
23485
|
+
return (await machine.seats())[0];
|
|
23486
|
+
}
|
|
23487
|
+
async function authOf(seat) {
|
|
23488
|
+
const token = await readToken(seat.home);
|
|
23489
|
+
if (token === null)
|
|
23490
|
+
throw new Error("This machine is not signed in. Run: cs login --token <token>");
|
|
23491
|
+
return { url: seat.config.url, token };
|
|
23492
|
+
}
|
|
23493
|
+
async function authOfAnySeatOrAsk() {
|
|
23494
|
+
const seat = await anySeat();
|
|
23495
|
+
if (seat === void 0)
|
|
23496
|
+
return machine.signIn({ why: "This machine is not signed in to Craftspace yet." });
|
|
23497
|
+
return authOf(seat);
|
|
23498
|
+
}
|
|
23427
23499
|
async function pick2(machines) {
|
|
23428
23500
|
return dist_default6({
|
|
23429
23501
|
message: "Which workstation?",
|
|
@@ -23482,25 +23554,27 @@ ${ui.attention("No workstations here yet.")}
|
|
|
23482
23554
|
ui.say("\n");
|
|
23483
23555
|
return 1;
|
|
23484
23556
|
}
|
|
23485
|
-
async function checks() {
|
|
23486
|
-
|
|
23487
|
-
|
|
23488
|
-
|
|
23489
|
-
const
|
|
23557
|
+
async function checks(home) {
|
|
23558
|
+
const seen = probed.get(home);
|
|
23559
|
+
if (seen !== void 0 && Date.now() - seen.at < PROBE_EVERY_MS)
|
|
23560
|
+
return seen.tools;
|
|
23561
|
+
const organization = (await readConfig(home))?.organization ?? "team";
|
|
23562
|
+
const dirs = clonedDirs(await readEnvironment(home));
|
|
23563
|
+
const first = await probe.tools({ dirs, organization });
|
|
23490
23564
|
const unwired = first.some((tool) => tool.id === "brain" && !tool.ok);
|
|
23491
|
-
const found = unwired && installs() && dirs.length > 0 ? await rewire(dirs).then(() => probe.tools({ dirs }), () => first) : first;
|
|
23492
|
-
probed
|
|
23565
|
+
const found = unwired && installs() && dirs.length > 0 ? await rewire(home, dirs).then(() => probe.tools({ dirs, organization }), () => first) : first;
|
|
23566
|
+
probed.set(home, { at: Date.now(), tools: found });
|
|
23493
23567
|
return found;
|
|
23494
23568
|
}
|
|
23495
|
-
async function buildEnvironment({ next, workDir, say }) {
|
|
23496
|
-
if (building || !installs() || next.templateId === null)
|
|
23569
|
+
async function buildEnvironment({ next, home, workDir, say }) {
|
|
23570
|
+
if (building.has(home) || !installs() || next.templateId === null)
|
|
23497
23571
|
return;
|
|
23498
|
-
const built = await readEnvironment();
|
|
23572
|
+
const built = await readEnvironment(home);
|
|
23499
23573
|
const buildsSteps = buildIsDue(built, next.templateId);
|
|
23500
23574
|
const clonesRepos = cloneIsDue({ built, wanted: next.repos, workDir });
|
|
23501
23575
|
if (!buildsSteps && !clonesRepos)
|
|
23502
23576
|
return;
|
|
23503
|
-
building
|
|
23577
|
+
building.add(home);
|
|
23504
23578
|
try {
|
|
23505
23579
|
let results = built?.results ?? [];
|
|
23506
23580
|
let repos = built?.repos ?? [];
|
|
@@ -23511,11 +23585,11 @@ async function buildEnvironment({ next, workDir, say }) {
|
|
|
23511
23585
|
if (clonesRepos) {
|
|
23512
23586
|
say?.(ui.heading(`Cloning the ${next.repos.length} repos your team works in, into ${workDir}`));
|
|
23513
23587
|
repos = await setup.cloneRepos({ repos: next.repos, workDir, say });
|
|
23514
|
-
await rewire(repos.filter((repo) => repo.ok).map((repo) => repo.path));
|
|
23588
|
+
await rewire(home, repos.filter((repo) => repo.ok).map((repo) => repo.path));
|
|
23515
23589
|
}
|
|
23516
|
-
await writeEnvironment({ templateId: next.templateId, results, repos, at: Date.now() });
|
|
23590
|
+
await writeEnvironment(home, { templateId: next.templateId, results, repos, at: Date.now() });
|
|
23517
23591
|
} finally {
|
|
23518
|
-
building
|
|
23592
|
+
building.delete(home);
|
|
23519
23593
|
}
|
|
23520
23594
|
}
|
|
23521
23595
|
function cloneIsDue({ built, wanted, workDir }) {
|
|
@@ -23542,35 +23616,40 @@ function buildIsDue(built, templateId) {
|
|
|
23542
23616
|
return false;
|
|
23543
23617
|
return Date.now() - built.at > REBUILD_AFTER_MS;
|
|
23544
23618
|
}
|
|
23545
|
-
async function readEnvironment() {
|
|
23546
|
-
const raw = await readFile3(environmentPath(), "utf8").catch(() => null);
|
|
23619
|
+
async function readEnvironment(home) {
|
|
23620
|
+
const raw = await readFile3(environmentPath(home), "utf8").catch(() => null);
|
|
23547
23621
|
if (raw === null)
|
|
23548
23622
|
return null;
|
|
23549
23623
|
const parsed = BuiltSchema.safeParse(JSON.parse(raw));
|
|
23550
23624
|
return parsed.success ? parsed.data : null;
|
|
23551
23625
|
}
|
|
23552
|
-
async function writeEnvironment(built) {
|
|
23553
|
-
await mkdir3(
|
|
23554
|
-
await writeFile2(environmentPath(), JSON.stringify(built, null, 2), { mode: 384 });
|
|
23626
|
+
async function writeEnvironment(home, built) {
|
|
23627
|
+
await mkdir3(home, { recursive: true, mode: 448 });
|
|
23628
|
+
await writeFile2(environmentPath(home), JSON.stringify(built, null, 2), { mode: 384 });
|
|
23555
23629
|
}
|
|
23556
23630
|
function installs() {
|
|
23557
23631
|
return process.env.CRAFTSPACE_NO_INSTALL !== "1";
|
|
23558
23632
|
}
|
|
23559
|
-
async function rewire(dirs) {
|
|
23560
|
-
const { url: url2, token } = await requireSignedIn();
|
|
23561
|
-
await wireRepos({ url: url2, token, dirs });
|
|
23633
|
+
async function rewire(home, dirs) {
|
|
23634
|
+
const { url: url2, token, organization } = await requireSignedIn(home);
|
|
23635
|
+
await wireRepos({ url: url2, token, organization: organization ?? "team", dirs });
|
|
23562
23636
|
}
|
|
23563
23637
|
function clonedDirs(built) {
|
|
23564
23638
|
return (built?.repos ?? []).filter((repo) => repo.ok).map((repo) => repo.path);
|
|
23565
23639
|
}
|
|
23566
|
-
function report(next) {
|
|
23567
|
-
reports.
|
|
23640
|
+
function report(home, next) {
|
|
23641
|
+
const mine = reports.get(home) ?? /* @__PURE__ */ new Map();
|
|
23642
|
+
mine.set(next.id, next);
|
|
23643
|
+
reports.set(home, mine);
|
|
23568
23644
|
}
|
|
23569
|
-
function drainReports() {
|
|
23570
|
-
const
|
|
23645
|
+
function drainReports(home) {
|
|
23646
|
+
const mine = reports.get(home);
|
|
23647
|
+
if (mine === void 0)
|
|
23648
|
+
return [];
|
|
23649
|
+
const pending = [...mine.values()];
|
|
23571
23650
|
for (const item of pending)
|
|
23572
23651
|
if (item.state !== "running")
|
|
23573
|
-
|
|
23652
|
+
mine.delete(item.id);
|
|
23574
23653
|
return pending;
|
|
23575
23654
|
}
|
|
23576
23655
|
async function call({ url: url2, token, method, path: route, body, schema }) {
|
|
@@ -23594,13 +23673,6 @@ var Unauthorized = class extends Error {
|
|
|
23594
23673
|
super("This machine no longer has a key Craftspace accepts.");
|
|
23595
23674
|
}
|
|
23596
23675
|
};
|
|
23597
|
-
async function signedInOrAsk() {
|
|
23598
|
-
const config2 = await readConfig();
|
|
23599
|
-
const token = await readToken();
|
|
23600
|
-
if (config2 && token)
|
|
23601
|
-
return { url: config2.url, token };
|
|
23602
|
-
return machine.signIn({ why: "This machine is not signed in to Craftspace yet." });
|
|
23603
|
-
}
|
|
23604
23676
|
async function withAuth(auth, work) {
|
|
23605
23677
|
try {
|
|
23606
23678
|
return { auth, value: await work(auth) };
|
|
@@ -23625,33 +23697,46 @@ function tokenIn(line) {
|
|
|
23625
23697
|
return /cst_[A-Za-z0-9_-]+/.exec(line)?.[0];
|
|
23626
23698
|
}
|
|
23627
23699
|
async function ensureKey() {
|
|
23628
|
-
const priv = path5.join(machine.
|
|
23700
|
+
const priv = path5.join(machine.root(), "id_ed25519");
|
|
23629
23701
|
const pub = `${priv}.pub`;
|
|
23630
23702
|
const existing = await readFile3(pub, "utf8").catch(() => null);
|
|
23631
23703
|
if (existing !== null)
|
|
23632
23704
|
return existing.trim();
|
|
23633
|
-
await mkdir3(machine.
|
|
23705
|
+
await mkdir3(machine.root(), { recursive: true, mode: 448 });
|
|
23634
23706
|
await run2("ssh-keygen", ["-t", "ed25519", "-N", "", "-q", "-f", priv, "-C", `craftspace-${os6.hostname()}`]);
|
|
23635
23707
|
return (await readFile3(pub, "utf8")).trim();
|
|
23636
23708
|
}
|
|
23637
|
-
async function writeConfig(next) {
|
|
23638
|
-
const merged = { ...await readConfig(), ...next };
|
|
23639
|
-
await mkdir3(
|
|
23640
|
-
await writeFile2(configPath(), `${JSON.stringify(merged, null, 2)}
|
|
23709
|
+
async function writeConfig(home, next) {
|
|
23710
|
+
const merged = { ...await readConfig(home), ...next };
|
|
23711
|
+
await mkdir3(home, { recursive: true, mode: 448 });
|
|
23712
|
+
await writeFile2(configPath(home), `${JSON.stringify(merged, null, 2)}
|
|
23641
23713
|
`, { mode: 384 });
|
|
23642
23714
|
}
|
|
23643
|
-
async function readConfig() {
|
|
23644
|
-
const raw = await readFile3(configPath(), "utf8").catch(() => null);
|
|
23715
|
+
async function readConfig(home) {
|
|
23716
|
+
const raw = await readFile3(configPath(home), "utf8").catch(() => null);
|
|
23645
23717
|
if (raw === null)
|
|
23646
23718
|
return null;
|
|
23647
|
-
|
|
23719
|
+
const parsed = ConfigSchema.safeParse(JSON.parse(raw));
|
|
23720
|
+
return parsed.success ? parsed.data : null;
|
|
23648
23721
|
}
|
|
23649
|
-
async function readToken() {
|
|
23650
|
-
const raw = await readFile3(tokenPath(), "utf8").catch(() => null);
|
|
23722
|
+
async function readToken(home) {
|
|
23723
|
+
const raw = await readFile3(tokenPath(home), "utf8").catch(() => null);
|
|
23651
23724
|
return raw === null ? null : raw.trim();
|
|
23652
23725
|
}
|
|
23726
|
+
async function settleLayout() {
|
|
23727
|
+
const root = machine.root();
|
|
23728
|
+
const config2 = await readConfig(root);
|
|
23729
|
+
const organization = config2?.organization;
|
|
23730
|
+
if (config2 === null || organization === void 0)
|
|
23731
|
+
return;
|
|
23732
|
+
const home = path5.join(root, organization);
|
|
23733
|
+
await mkdir3(home, { recursive: true, mode: 448 });
|
|
23734
|
+
for (const name of SEAT_FILES) {
|
|
23735
|
+
await rename2(path5.join(root, name), path5.join(home, name)).catch(() => void 0);
|
|
23736
|
+
}
|
|
23737
|
+
}
|
|
23653
23738
|
async function readSessions() {
|
|
23654
|
-
const dir = path5.join(machine.
|
|
23739
|
+
const dir = path5.join(machine.root(), "sessions");
|
|
23655
23740
|
const names = await readdir2(dir).catch(() => []);
|
|
23656
23741
|
const sessions = [];
|
|
23657
23742
|
for (const name of names.filter((entry) => entry.endsWith(".json"))) {
|
|
@@ -23664,43 +23749,63 @@ async function readSessions() {
|
|
|
23664
23749
|
}
|
|
23665
23750
|
return sessions.slice(0, MAX_REPORTED_SESSIONS);
|
|
23666
23751
|
}
|
|
23667
|
-
async function writeAuthorizedKeys(keys) {
|
|
23752
|
+
async function writeAuthorizedKeys({ organization, keys }) {
|
|
23668
23753
|
const target = path5.join(os6.homedir(), ".ssh", "authorized_keys");
|
|
23669
23754
|
const current = await readFile3(target, "utf8").catch(() => "");
|
|
23670
|
-
const
|
|
23671
|
-
const after = current.includes(KEYS_END) ? current.split(KEYS_END)[1] ?? "" : "";
|
|
23672
|
-
const block = keys.length === 0 ? "" : `${KEYS_BEGIN}
|
|
23673
|
-
${keys.join("\n")}
|
|
23674
|
-
${KEYS_END}
|
|
23755
|
+
const block = keys.length === 0 ? "" : `${keys.join("\n")}
|
|
23675
23756
|
`;
|
|
23676
|
-
const next =
|
|
23757
|
+
const next = replaceBlock({
|
|
23758
|
+
text: replaceBlock({ text: current, begin: KEYS_BEGIN, end: KEYS_END, block: "" }),
|
|
23759
|
+
begin: `${KEYS_BEGIN} ${organization}`,
|
|
23760
|
+
end: `${KEYS_END} ${organization}`,
|
|
23761
|
+
block
|
|
23762
|
+
});
|
|
23677
23763
|
if (next === current)
|
|
23678
23764
|
return;
|
|
23679
23765
|
await mkdir3(path5.dirname(target), { recursive: true, mode: 448 });
|
|
23680
23766
|
await writeFile2(target, next, { mode: 384 });
|
|
23681
23767
|
}
|
|
23768
|
+
function replaceBlock({ text, begin, end, block }) {
|
|
23769
|
+
const before = text.split(begin)[0] ?? "";
|
|
23770
|
+
const after = text.includes(end) ? text.split(end)[1] ?? "" : text.includes(begin) ? "" : null;
|
|
23771
|
+
if (after === null && block === "")
|
|
23772
|
+
return text;
|
|
23773
|
+
const kept = after ?? "";
|
|
23774
|
+
const wrapped = block === "" ? "" : `${begin}
|
|
23775
|
+
${block}${end}
|
|
23776
|
+
`;
|
|
23777
|
+
return `${trimEnd(before)}${trimEnd(before) === "" ? "" : "\n"}${wrapped}${kept.replace(/^\n/, "")}`;
|
|
23778
|
+
}
|
|
23682
23779
|
function trimEnd(text) {
|
|
23683
23780
|
return text.replace(/\s+$/, "");
|
|
23684
23781
|
}
|
|
23685
|
-
async function wireRepos({ url: url2, token, dirs }) {
|
|
23782
|
+
async function wireRepos({ url: url2, token, organization, dirs }) {
|
|
23686
23783
|
if (dirs.length === 0)
|
|
23687
23784
|
return;
|
|
23785
|
+
await migrate.stripGlobalMcp();
|
|
23688
23786
|
const server = { type: "http", url: `${url2}/mcp`, headers: { Authorization: `Bearer ${token}` } };
|
|
23689
23787
|
await editClaudeConfig((projects) => {
|
|
23690
23788
|
for (const dir of dirs) {
|
|
23691
23789
|
const here2 = isPlainObject3(projects[dir]) ? { ...projects[dir] } : {};
|
|
23692
23790
|
const servers = isPlainObject3(here2.mcpServers) ? { ...here2.mcpServers } : {};
|
|
23693
|
-
|
|
23791
|
+
delete servers.craftspace;
|
|
23792
|
+
projects[dir] = {
|
|
23793
|
+
...here2,
|
|
23794
|
+
hasTrustDialogAccepted: true,
|
|
23795
|
+
mcpServers: { ...servers, [serverNameFor(organization)]: server }
|
|
23796
|
+
};
|
|
23694
23797
|
}
|
|
23695
23798
|
});
|
|
23696
23799
|
}
|
|
23800
|
+
function serverNameFor(organization) {
|
|
23801
|
+
return `craftspace-${organization}`;
|
|
23802
|
+
}
|
|
23697
23803
|
async function unwireRepos() {
|
|
23698
23804
|
await editClaudeConfig((projects) => {
|
|
23699
23805
|
for (const [dir, found] of Object.entries(projects)) {
|
|
23700
23806
|
if (!isPlainObject3(found) || !isPlainObject3(found.mcpServers))
|
|
23701
23807
|
continue;
|
|
23702
|
-
const servers =
|
|
23703
|
-
delete servers.craftspace;
|
|
23808
|
+
const servers = Object.fromEntries(Object.entries(found.mcpServers).filter(([name]) => name !== "craftspace" && !name.startsWith("craftspace-")));
|
|
23704
23809
|
projects[dir] = { ...found, mcpServers: servers };
|
|
23705
23810
|
}
|
|
23706
23811
|
});
|
|
@@ -23720,7 +23825,7 @@ async function editClaudeConfig(edit) {
|
|
|
23720
23825
|
async function writeAgentConfigs({ writes }) {
|
|
23721
23826
|
if (writes.length === 0)
|
|
23722
23827
|
return;
|
|
23723
|
-
const owned = [
|
|
23828
|
+
const owned = [];
|
|
23724
23829
|
for (const write of writes) {
|
|
23725
23830
|
const target = expand(write.path);
|
|
23726
23831
|
await mkdir3(path5.dirname(target), { recursive: true });
|
|
@@ -23732,10 +23837,10 @@ async function writeAgentConfigs({ writes }) {
|
|
|
23732
23837
|
`, { mode: 384 });
|
|
23733
23838
|
owned.push(target);
|
|
23734
23839
|
}
|
|
23735
|
-
await writeFile2(ownedPath(), JSON.stringify({ paths: [...new Set(owned)] }, null, 2));
|
|
23840
|
+
await writeFile2(ownedPath(machine.root()), JSON.stringify({ paths: [...new Set(owned)] }, null, 2));
|
|
23736
23841
|
}
|
|
23737
|
-
async function readOwned() {
|
|
23738
|
-
const raw = await readFile3(ownedPath(), "utf8").catch(() => null);
|
|
23842
|
+
async function readOwned(home) {
|
|
23843
|
+
const raw = await readFile3(ownedPath(home), "utf8").catch(() => null);
|
|
23739
23844
|
if (raw === null)
|
|
23740
23845
|
return [];
|
|
23741
23846
|
const parsed = OwnedSchema.safeParse(JSON.parse(raw));
|
|
@@ -23752,9 +23857,8 @@ function mergeInto(current, incoming) {
|
|
|
23752
23857
|
function isPlainObject3(value) {
|
|
23753
23858
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
23754
23859
|
}
|
|
23755
|
-
async function removeAgentConfigs() {
|
|
23756
|
-
await
|
|
23757
|
-
for (const target of await readOwned()) {
|
|
23860
|
+
async function removeAgentConfigs(home) {
|
|
23861
|
+
for (const target of await readOwned(home)) {
|
|
23758
23862
|
const current = await readFile3(target, "utf8").catch(() => null);
|
|
23759
23863
|
if (current === null)
|
|
23760
23864
|
continue;
|
|
@@ -23852,17 +23956,17 @@ function entryPath() {
|
|
|
23852
23956
|
function expand(target) {
|
|
23853
23957
|
return target.startsWith("~/") ? path5.join(os6.homedir(), target.slice(2)) : target;
|
|
23854
23958
|
}
|
|
23855
|
-
function configPath() {
|
|
23856
|
-
return path5.join(
|
|
23959
|
+
function configPath(home) {
|
|
23960
|
+
return path5.join(home, "config.json");
|
|
23857
23961
|
}
|
|
23858
|
-
function tokenPath() {
|
|
23859
|
-
return path5.join(
|
|
23962
|
+
function tokenPath(home) {
|
|
23963
|
+
return path5.join(home, "token");
|
|
23860
23964
|
}
|
|
23861
|
-
function ownedPath() {
|
|
23862
|
-
return path5.join(
|
|
23965
|
+
function ownedPath(home) {
|
|
23966
|
+
return path5.join(home, "owned.json");
|
|
23863
23967
|
}
|
|
23864
|
-
function environmentPath() {
|
|
23865
|
-
return path5.join(
|
|
23968
|
+
function environmentPath(home) {
|
|
23969
|
+
return path5.join(home, "environment.json");
|
|
23866
23970
|
}
|
|
23867
23971
|
function load2() {
|
|
23868
23972
|
const cores = Math.max(1, os6.cpus().length);
|
|
@@ -23881,7 +23985,12 @@ function nextDelayMs(failures, busy = false) {
|
|
|
23881
23985
|
function messageOf(error51) {
|
|
23882
23986
|
return error51 instanceof Error ? error51.message : String(error51);
|
|
23883
23987
|
}
|
|
23884
|
-
var ConfigSchema = external_exports.object({
|
|
23988
|
+
var ConfigSchema = external_exports.object({
|
|
23989
|
+
url: external_exports.string(),
|
|
23990
|
+
machineId: external_exports.string(),
|
|
23991
|
+
organization: external_exports.string().optional(),
|
|
23992
|
+
account: external_exports.string().optional()
|
|
23993
|
+
});
|
|
23885
23994
|
var BuiltSchema = external_exports.object({
|
|
23886
23995
|
templateId: external_exports.string(),
|
|
23887
23996
|
results: external_exports.array(WorkstationStepResultSchema),
|
|
@@ -23890,6 +23999,7 @@ var BuiltSchema = external_exports.object({
|
|
|
23890
23999
|
});
|
|
23891
24000
|
var OwnedSchema = external_exports.object({ paths: external_exports.array(external_exports.string()) });
|
|
23892
24001
|
var CLAUDE_FILE = ".claude.json";
|
|
24002
|
+
var SEAT_FILES = ["config.json", "token", "environment.json", "owned.json"];
|
|
23893
24003
|
var SERVICE_NAME = "craftspace";
|
|
23894
24004
|
var LAUNCH_LABEL = "app.craftspace.machine";
|
|
23895
24005
|
var REQUEST_TIMEOUT_MS2 = 15e3;
|
|
@@ -23920,8 +24030,8 @@ var DEFAULT_URL = process.env.CRAFTSPACE_URL ?? "https://craftspace.app";
|
|
|
23920
24030
|
var PROBE_EVERY_MS = 3e5;
|
|
23921
24031
|
var REBUILD_AFTER_MS = 216e5;
|
|
23922
24032
|
var updateWanted = null;
|
|
23923
|
-
var probed =
|
|
23924
|
-
var building =
|
|
24033
|
+
var probed = /* @__PURE__ */ new Map();
|
|
24034
|
+
var building = /* @__PURE__ */ new Set();
|
|
23925
24035
|
var reports = /* @__PURE__ */ new Map();
|
|
23926
24036
|
var running = /* @__PURE__ */ new Set();
|
|
23927
24037
|
|
|
@@ -23931,12 +24041,13 @@ var program2 = new Command();
|
|
|
23931
24041
|
program2.enablePositionalOptions().name("cs").description("Sign this machine in to your team, so every agent on it reads the same brain.").version(CLI_VERSION).showHelpAfterError();
|
|
23932
24042
|
program2.command("login").description("sign this machine in").option("--token <token>", "the login token from your team", process.env.CRAFTSPACE_TOKEN).option("--url <url>", "your control plane, remembered after the first login").option("--no-install", "do not install anything this machine is missing").action(async ({ token, url: url2, install }) => {
|
|
23933
24043
|
const where = await machine.controlPlane(url2);
|
|
23934
|
-
const signed = token === void 0 ?
|
|
24044
|
+
const signed = token === void 0 ? await machine.signIn({ url: where, why: "Signing this machine in.", install }) : await machine.login({ token, url: where, install });
|
|
23935
24045
|
const daemon = await machine.daemon();
|
|
23936
|
-
|
|
23937
|
-
ui.say(ui.
|
|
23938
|
-
|
|
23939
|
-
ui.say(ui.field("
|
|
24046
|
+
const seats = await machine.seats();
|
|
24047
|
+
ui.say(ui.heading(`Signed in as ${signed.machine.ownerName}`));
|
|
24048
|
+
ui.say(ui.field("Machine", `${ui.name(signed.machine.name)} ${ui.dim(machineSpecs(signed.machine))}`));
|
|
24049
|
+
ui.say(ui.field("Org", `${ui.name(signed.organization)}${orgTally(seats.length)}`));
|
|
24050
|
+
ui.say(ui.field("Work dir", await machine.workDir(signed.organization)));
|
|
23940
24051
|
ui.say(ui.field("MCP wired", "into every repo cloned there, for Claude Code"));
|
|
23941
24052
|
ui.say(ui.field("Daemon", daemon.ok ? daemon.text : ui.attention(daemon.text)));
|
|
23942
24053
|
ui.say(`
|
|
@@ -23947,14 +24058,17 @@ program2.command("run").description("pick a workstation and open a terminal on i
|
|
|
23947
24058
|
process.exitCode = await machine.run();
|
|
23948
24059
|
});
|
|
23949
24060
|
program2.command("status").description("what this machine is and what it is doing").action(async () => {
|
|
23950
|
-
const { machine: found }
|
|
23951
|
-
|
|
23952
|
-
|
|
23953
|
-
|
|
23954
|
-
|
|
23955
|
-
|
|
23956
|
-
|
|
23957
|
-
|
|
24061
|
+
for (const { machine: found, organization } of await machine.status()) {
|
|
24062
|
+
const since = found.lastBeatAt === null ? "never" : `${secondsSince(found.lastBeatAt)}s ago`;
|
|
24063
|
+
ui.say(ui.heading(`${found.name} ${found.state} ${ui.dim(organization)}`));
|
|
24064
|
+
ui.say(ui.field("Owner", found.ownerName));
|
|
24065
|
+
ui.say(ui.field("Machine", ui.dim(machineSpecs(found))));
|
|
24066
|
+
ui.say(ui.field("CLI", `${found.cli}, beat ${since}`));
|
|
24067
|
+
ui.say(ui.field("Work dir", found.workDir ?? "set on the first beat"));
|
|
24068
|
+
for (const session of found.sessions) {
|
|
24069
|
+
ui.say(ui.field("Session", `${session.state} ${session.title ?? session.id}`));
|
|
24070
|
+
}
|
|
24071
|
+
}
|
|
23958
24072
|
ui.say("\n");
|
|
23959
24073
|
});
|
|
23960
24074
|
program2.command("logout").description("leave the team, and undo what login wrote").action(async () => {
|
|
@@ -23962,6 +24076,9 @@ program2.command("logout").description("leave the team, and undo what login wrot
|
|
|
23962
24076
|
ui.say(ui.ok("Left the team. Nothing of ours is running here."));
|
|
23963
24077
|
});
|
|
23964
24078
|
program2.command("daemon", { hidden: true }).action(async () => machine.beatForever());
|
|
24079
|
+
function orgTally(seats) {
|
|
24080
|
+
return seats < 2 ? "" : ui.dim(` \xB7 ${seats} orgs on this box`);
|
|
24081
|
+
}
|
|
23965
24082
|
try {
|
|
23966
24083
|
await program2.parseAsync(process.argv);
|
|
23967
24084
|
} catch (error51) {
|