@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 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.9.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
- const target = path2.join(os.homedir(), ".mcp.json");
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 unwired = dirs.filter((dir) => !("craftspace" in mcpServersIn(projects[dir])));
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
- home() {
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
- await mkdir3(machine.home(), { recursive: true, mode: 448 });
23275
- await writeConfig({ url: url2, machineId: answer.machine.id, organization: toSpaceSlug(answer.organizationName) });
23276
- await writeFile2(tokenPath(), token, { mode: 384 });
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
- const organization = (await readConfig())?.organization;
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 readConfig())?.url ?? DEFAULT_URL;
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 readConfig())?.url ?? DEFAULT_URL;
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 { url: where, token, machine: signed };
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 found = await withAuth(await signedInOrAsk(), (auth) => call({ ...auth, method: "GET", path: "/api/machines/me", schema: MachineSchema }));
23320
- return { machine: found.value, url: found.auth.url };
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.home(), workDir }).catch(() => []);
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.beatOnce();
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 signedInOrAsk();
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 config2 = await readConfig();
23411
- const token = await readToken();
23412
- if (config2 && token) {
23413
- await call({ url: config2.url, token, method: "POST", path: "/api/machines/logout", schema: external_exports.object({}) }).catch(() => void 0);
23414
- }
23415
- await removeAgentConfigs();
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.home(), { recursive: true, force: true });
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
- if (probed !== null && Date.now() - probed.at < PROBE_EVERY_MS)
23487
- return probed.tools;
23488
- const dirs = clonedDirs(await readEnvironment());
23489
- const first = await probe.tools({ dirs });
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 = { at: Date.now(), tools: found };
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 = true;
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 = false;
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(machine.home(), { recursive: true, mode: 448 });
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.set(next.id, next);
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 pending = [...reports.values()];
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
- reports.delete(item.id);
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.home(), "id_ed25519");
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.home(), { recursive: true, mode: 448 });
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(machine.home(), { recursive: true, mode: 448 });
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
- return ConfigSchema.parse(JSON.parse(raw));
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.home(), "sessions");
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 before = current.split(KEYS_BEGIN)[0] ?? "";
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 = `${trimEnd(before)}${trimEnd(before) === "" ? "" : "\n"}${block}${after.replace(/^\n/, "")}`;
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
- projects[dir] = { ...here2, hasTrustDialogAccepted: true, mcpServers: { ...servers, craftspace: server } };
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 = { ...found.mcpServers };
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 = [...await readOwned()];
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 unwireRepos();
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(machine.home(), "config.json");
23959
+ function configPath(home) {
23960
+ return path5.join(home, "config.json");
23857
23961
  }
23858
- function tokenPath() {
23859
- return path5.join(machine.home(), "token");
23962
+ function tokenPath(home) {
23963
+ return path5.join(home, "token");
23860
23964
  }
23861
- function ownedPath() {
23862
- return path5.join(machine.home(), "owned.json");
23965
+ function ownedPath(home) {
23966
+ return path5.join(home, "owned.json");
23863
23967
  }
23864
- function environmentPath() {
23865
- return path5.join(machine.home(), "environment.json");
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({ url: external_exports.string(), machineId: external_exports.string(), organization: external_exports.string().optional() });
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 = null;
23924
- var building = false;
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 ? (await machine.signIn({ url: where, why: "Signing this machine in.", install })).machine : await machine.login({ token, url: where, install });
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
- ui.say(ui.heading(`Signed in as ${signed.ownerName}`));
23937
- ui.say(ui.field("Machine", `${ui.name(signed.name)} ${ui.dim(machineSpecs(signed))}`));
23938
- const workDir = await machine.workDir();
23939
- ui.say(ui.field("Work dir", workDir === null ? "set on the first beat" : workDir));
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 } = await machine.status();
23951
- const since = found.lastBeatAt === null ? "never" : `${secondsSince(found.lastBeatAt)}s ago`;
23952
- ui.say(ui.heading(`${found.name} ${found.state}`));
23953
- ui.say(ui.field("Owner", found.ownerName));
23954
- ui.say(ui.field("Machine", ui.dim(machineSpecs(found))));
23955
- ui.say(ui.field("CLI", `${found.cli}, beat ${since}`));
23956
- for (const session of found.sessions)
23957
- ui.say(ui.field("Session", `${session.state} ${session.title ?? session.id}`));
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) {