@craftspace/cli 0.9.1 → 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.1";
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";
@@ -22344,16 +22344,17 @@ import { readFile as readFile2 } from "node:fs/promises";
22344
22344
  import os2 from "node:os";
22345
22345
  import path3 from "node:path";
22346
22346
  var probe = {
22347
- async tools({ dirs }) {
22348
- return Promise.all([brainWired(dirs), sshListening()]);
22347
+ async tools({ dirs, organization }) {
22348
+ return Promise.all([brainWired({ dirs, organization }), sshListening()]);
22349
22349
  }
22350
22350
  };
22351
- async function brainWired(dirs) {
22351
+ async function brainWired({ dirs, organization }) {
22352
22352
  if (dirs.length === 0) {
22353
22353
  return { id: "brain", ok: false, detail: "no repos cloned here yet, so there is nothing to wire it into" };
22354
22354
  }
22355
22355
  const projects = await claudeProjects();
22356
- 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])));
22357
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) };
22358
22359
  }
22359
22360
  async function claudeProjects() {
@@ -23254,9 +23255,24 @@ var INSTALL_TIMEOUT_MS = 18e4;
23254
23255
  // dist/machine.js
23255
23256
  var run2 = promisify2(execFile3);
23256
23257
  var machine = {
23257
- home() {
23258
+ root() {
23258
23259
  return process.env.CRAFTSPACE_CLI_HOME ?? path5.join(os6.homedir(), ".craftspace");
23259
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
+ },
23260
23276
  async login({ token, url: url2, install }) {
23261
23277
  const answer = await call({
23262
23278
  url: url2,
@@ -23274,26 +23290,29 @@ var machine = {
23274
23290
  },
23275
23291
  schema: MachineLoginResponseSchema
23276
23292
  });
23277
- await mkdir3(machine.home(), { recursive: true, mode: 448 });
23278
- await writeConfig({ url: url2, machineId: answer.machine.id, organization: toSpaceSlug(answer.organizationName) });
23279
- 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 });
23280
23300
  await writeAgentConfigs({ writes: answer.write });
23281
- await wireRepos({ url: url2, token, dirs: clonedDirs(await readEnvironment()) });
23301
+ await wireRepos({ url: url2, token, organization, dirs: clonedDirs(await readEnvironment(home)) });
23282
23302
  await installService();
23283
23303
  if (install)
23284
- await machine.beatOnce(ui.say);
23285
- return answer.machine;
23304
+ await machine.beatOnce({ home, say: ui.say });
23305
+ return { url: url2, token, machine: answer.machine, organization };
23286
23306
  },
23287
- async workDir() {
23288
- const organization = (await readConfig())?.organization;
23289
- return organization === void 0 ? null : setup.workDir(organization);
23307
+ async workDir(organization) {
23308
+ return setup.workDir(organization);
23290
23309
  },
23291
23310
  async controlPlane(override) {
23292
- 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;
23293
23312
  return where.replace(/\/$/, "");
23294
23313
  },
23295
23314
  async signIn({ url: url2, why, install = false }) {
23296
- const where = url2 ?? (await readConfig())?.url ?? DEFAULT_URL;
23315
+ const where = url2 ?? (await anySeat())?.config.url ?? DEFAULT_URL;
23297
23316
  if (!process.stdin.isTTY)
23298
23317
  throw new Error(`${why} Run: cs login --token <token>`);
23299
23318
  const page = `${where}/workstations`;
@@ -23315,16 +23334,34 @@ ${ui.attention(why)}
23315
23334
  throw new Error("That line carries no key.");
23316
23335
  const signed = await machine.login({ token, url: where, install });
23317
23336
  ui.say(`
23318
- ${ui.ok(`Signed in as ${signed.ownerName}. This machine is ${ui.name(signed.name)}.`)}`);
23319
- 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;
23320
23339
  },
23321
23340
  async status() {
23322
- const found = await withAuth(await signedInOrAsk(), (auth) => call({ ...auth, method: "GET", path: "/api/machines/me", schema: MachineSchema }));
23323
- 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;
23324
23361
  },
23325
- async beatOnce(say) {
23326
- const { url: url2, token, organization } = await requireSignedIn();
23327
- const built = await readEnvironment();
23362
+ async beatOnce({ home, say }) {
23363
+ const { url: url2, token, organization } = await requireSignedIn(home);
23364
+ const built = await readEnvironment(home);
23328
23365
  const answer = await call({
23329
23366
  url: url2,
23330
23367
  token,
@@ -23334,8 +23371,8 @@ ${ui.ok(`Signed in as ${signed.ownerName}. This machine is ${ui.name(signed.name
23334
23371
  cli: CLI_VERSION,
23335
23372
  ...load2(),
23336
23373
  sessions: await readSessions(),
23337
- runs: drainReports(),
23338
- tools: await checks(),
23374
+ runs: drainReports(home),
23375
+ tools: await checks(home),
23339
23376
  environment: built?.results ?? [],
23340
23377
  templateId: built?.templateId ?? null,
23341
23378
  ...organization === void 0 ? {} : { workDir: setup.workDir(organization) },
@@ -23348,10 +23385,10 @@ ${ui.ok(`Signed in as ${signed.ownerName}. This machine is ${ui.name(signed.name
23348
23385
  updateWanted = answer.cli.version;
23349
23386
  const workDir = setup.workDir(answer.organization);
23350
23387
  if (answer.organization !== organization)
23351
- await writeConfig({ organization: answer.organization });
23352
- await migrate.toLatest({ home: machine.home(), workDir }).catch(() => []);
23353
- await writeAuthorizedKeys(answer.authorizedKeys);
23354
- 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 });
23355
23392
  if (say === void 0)
23356
23393
  void building2.catch(() => void 0);
23357
23394
  else
@@ -23360,7 +23397,7 @@ ${ui.ok(`Signed in as ${signed.ownerName}. This machine is ${ui.name(signed.name
23360
23397
  if (running.has(work.id))
23361
23398
  continue;
23362
23399
  running.add(work.id);
23363
- 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));
23364
23401
  }
23365
23402
  return running.size > 0 || answer.work.length > 0;
23366
23403
  },
@@ -23381,7 +23418,7 @@ ${ui.ok(`Signed in as ${signed.ownerName}. This machine is ${ui.name(signed.name
23381
23418
  }
23382
23419
  }
23383
23420
  try {
23384
- busy = await machine.beatOnce();
23421
+ busy = await machine.beatAll();
23385
23422
  failures = 0;
23386
23423
  } catch (error51) {
23387
23424
  failures += 1;
@@ -23392,7 +23429,7 @@ ${ui.ok(`Signed in as ${signed.ownerName}. This machine is ${ui.name(signed.name
23392
23429
  }
23393
23430
  },
23394
23431
  async run() {
23395
- const first = await signedInOrAsk();
23432
+ const first = await authOfAnySeatOrAsk();
23396
23433
  const { auth, value: answer } = await withAuth(first, (live) => call({ ...live, method: "GET", path: "/api/machines", schema: MachinesResponseSchema }));
23397
23434
  if (answer.machines.length === 0)
23398
23435
  return nowhereToGo(auth);
@@ -23410,23 +23447,55 @@ ${ui.ok(`Signed in as ${signed.ownerName}. This machine is ${ui.name(signed.name
23410
23447
  return { text: BEATING, ok: true };
23411
23448
  },
23412
23449
  async logout() {
23413
- const config2 = await readConfig();
23414
- const token = await readToken();
23415
- if (config2 && token) {
23416
- await call({ url: config2.url, token, method: "POST", path: "/api/machines/logout", schema: external_exports.object({}) }).catch(() => void 0);
23417
- }
23418
- 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();
23419
23467
  await uninstallService();
23420
- await rm2(machine.home(), { recursive: true, force: true });
23468
+ await rm2(machine.root(), { recursive: true, force: true });
23421
23469
  }
23422
23470
  };
23423
- async function requireSignedIn() {
23424
- const config2 = await readConfig();
23425
- const token = await readToken();
23471
+ async function requireSignedIn(home) {
23472
+ const config2 = await readConfig(home);
23473
+ const token = await readToken(home);
23426
23474
  if (!config2 || !token)
23427
23475
  throw new Error("This machine is not signed in. Run: cs login --token <token>");
23428
23476
  return { url: config2.url, token, ...spreadIfDefined({ organization: config2.organization }) };
23429
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
+ }
23430
23499
  async function pick2(machines) {
23431
23500
  return dist_default6({
23432
23501
  message: "Which workstation?",
@@ -23485,25 +23554,27 @@ ${ui.attention("No workstations here yet.")}
23485
23554
  ui.say("\n");
23486
23555
  return 1;
23487
23556
  }
23488
- async function checks() {
23489
- if (probed !== null && Date.now() - probed.at < PROBE_EVERY_MS)
23490
- return probed.tools;
23491
- const dirs = clonedDirs(await readEnvironment());
23492
- 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 });
23493
23564
  const unwired = first.some((tool) => tool.id === "brain" && !tool.ok);
23494
- const found = unwired && installs() && dirs.length > 0 ? await rewire(dirs).then(() => probe.tools({ dirs }), () => first) : first;
23495
- 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 });
23496
23567
  return found;
23497
23568
  }
23498
- async function buildEnvironment({ next, workDir, say }) {
23499
- if (building || !installs() || next.templateId === null)
23569
+ async function buildEnvironment({ next, home, workDir, say }) {
23570
+ if (building.has(home) || !installs() || next.templateId === null)
23500
23571
  return;
23501
- const built = await readEnvironment();
23572
+ const built = await readEnvironment(home);
23502
23573
  const buildsSteps = buildIsDue(built, next.templateId);
23503
23574
  const clonesRepos = cloneIsDue({ built, wanted: next.repos, workDir });
23504
23575
  if (!buildsSteps && !clonesRepos)
23505
23576
  return;
23506
- building = true;
23577
+ building.add(home);
23507
23578
  try {
23508
23579
  let results = built?.results ?? [];
23509
23580
  let repos = built?.repos ?? [];
@@ -23514,11 +23585,11 @@ async function buildEnvironment({ next, workDir, say }) {
23514
23585
  if (clonesRepos) {
23515
23586
  say?.(ui.heading(`Cloning the ${next.repos.length} repos your team works in, into ${workDir}`));
23516
23587
  repos = await setup.cloneRepos({ repos: next.repos, workDir, say });
23517
- await rewire(repos.filter((repo) => repo.ok).map((repo) => repo.path));
23588
+ await rewire(home, repos.filter((repo) => repo.ok).map((repo) => repo.path));
23518
23589
  }
23519
- await writeEnvironment({ templateId: next.templateId, results, repos, at: Date.now() });
23590
+ await writeEnvironment(home, { templateId: next.templateId, results, repos, at: Date.now() });
23520
23591
  } finally {
23521
- building = false;
23592
+ building.delete(home);
23522
23593
  }
23523
23594
  }
23524
23595
  function cloneIsDue({ built, wanted, workDir }) {
@@ -23545,35 +23616,40 @@ function buildIsDue(built, templateId) {
23545
23616
  return false;
23546
23617
  return Date.now() - built.at > REBUILD_AFTER_MS;
23547
23618
  }
23548
- async function readEnvironment() {
23549
- const raw = await readFile3(environmentPath(), "utf8").catch(() => null);
23619
+ async function readEnvironment(home) {
23620
+ const raw = await readFile3(environmentPath(home), "utf8").catch(() => null);
23550
23621
  if (raw === null)
23551
23622
  return null;
23552
23623
  const parsed = BuiltSchema.safeParse(JSON.parse(raw));
23553
23624
  return parsed.success ? parsed.data : null;
23554
23625
  }
23555
- async function writeEnvironment(built) {
23556
- await mkdir3(machine.home(), { recursive: true, mode: 448 });
23557
- 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 });
23558
23629
  }
23559
23630
  function installs() {
23560
23631
  return process.env.CRAFTSPACE_NO_INSTALL !== "1";
23561
23632
  }
23562
- async function rewire(dirs) {
23563
- const { url: url2, token } = await requireSignedIn();
23564
- 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 });
23565
23636
  }
23566
23637
  function clonedDirs(built) {
23567
23638
  return (built?.repos ?? []).filter((repo) => repo.ok).map((repo) => repo.path);
23568
23639
  }
23569
- function report(next) {
23570
- 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);
23571
23644
  }
23572
- function drainReports() {
23573
- 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()];
23574
23650
  for (const item of pending)
23575
23651
  if (item.state !== "running")
23576
- reports.delete(item.id);
23652
+ mine.delete(item.id);
23577
23653
  return pending;
23578
23654
  }
23579
23655
  async function call({ url: url2, token, method, path: route, body, schema }) {
@@ -23597,13 +23673,6 @@ var Unauthorized = class extends Error {
23597
23673
  super("This machine no longer has a key Craftspace accepts.");
23598
23674
  }
23599
23675
  };
23600
- async function signedInOrAsk() {
23601
- const config2 = await readConfig();
23602
- const token = await readToken();
23603
- if (config2 && token)
23604
- return { url: config2.url, token };
23605
- return machine.signIn({ why: "This machine is not signed in to Craftspace yet." });
23606
- }
23607
23676
  async function withAuth(auth, work) {
23608
23677
  try {
23609
23678
  return { auth, value: await work(auth) };
@@ -23628,33 +23697,46 @@ function tokenIn(line) {
23628
23697
  return /cst_[A-Za-z0-9_-]+/.exec(line)?.[0];
23629
23698
  }
23630
23699
  async function ensureKey() {
23631
- const priv = path5.join(machine.home(), "id_ed25519");
23700
+ const priv = path5.join(machine.root(), "id_ed25519");
23632
23701
  const pub = `${priv}.pub`;
23633
23702
  const existing = await readFile3(pub, "utf8").catch(() => null);
23634
23703
  if (existing !== null)
23635
23704
  return existing.trim();
23636
- await mkdir3(machine.home(), { recursive: true, mode: 448 });
23705
+ await mkdir3(machine.root(), { recursive: true, mode: 448 });
23637
23706
  await run2("ssh-keygen", ["-t", "ed25519", "-N", "", "-q", "-f", priv, "-C", `craftspace-${os6.hostname()}`]);
23638
23707
  return (await readFile3(pub, "utf8")).trim();
23639
23708
  }
23640
- async function writeConfig(next) {
23641
- const merged = { ...await readConfig(), ...next };
23642
- await mkdir3(machine.home(), { recursive: true, mode: 448 });
23643
- 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)}
23644
23713
  `, { mode: 384 });
23645
23714
  }
23646
- async function readConfig() {
23647
- const raw = await readFile3(configPath(), "utf8").catch(() => null);
23715
+ async function readConfig(home) {
23716
+ const raw = await readFile3(configPath(home), "utf8").catch(() => null);
23648
23717
  if (raw === null)
23649
23718
  return null;
23650
- return ConfigSchema.parse(JSON.parse(raw));
23719
+ const parsed = ConfigSchema.safeParse(JSON.parse(raw));
23720
+ return parsed.success ? parsed.data : null;
23651
23721
  }
23652
- async function readToken() {
23653
- const raw = await readFile3(tokenPath(), "utf8").catch(() => null);
23722
+ async function readToken(home) {
23723
+ const raw = await readFile3(tokenPath(home), "utf8").catch(() => null);
23654
23724
  return raw === null ? null : raw.trim();
23655
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
+ }
23656
23738
  async function readSessions() {
23657
- const dir = path5.join(machine.home(), "sessions");
23739
+ const dir = path5.join(machine.root(), "sessions");
23658
23740
  const names = await readdir2(dir).catch(() => []);
23659
23741
  const sessions = [];
23660
23742
  for (const name of names.filter((entry) => entry.endsWith(".json"))) {
@@ -23667,25 +23749,37 @@ async function readSessions() {
23667
23749
  }
23668
23750
  return sessions.slice(0, MAX_REPORTED_SESSIONS);
23669
23751
  }
23670
- async function writeAuthorizedKeys(keys) {
23752
+ async function writeAuthorizedKeys({ organization, keys }) {
23671
23753
  const target = path5.join(os6.homedir(), ".ssh", "authorized_keys");
23672
23754
  const current = await readFile3(target, "utf8").catch(() => "");
23673
- const before = current.split(KEYS_BEGIN)[0] ?? "";
23674
- const after = current.includes(KEYS_END) ? current.split(KEYS_END)[1] ?? "" : "";
23675
- const block = keys.length === 0 ? "" : `${KEYS_BEGIN}
23676
- ${keys.join("\n")}
23677
- ${KEYS_END}
23755
+ const block = keys.length === 0 ? "" : `${keys.join("\n")}
23678
23756
  `;
23679
- 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
+ });
23680
23763
  if (next === current)
23681
23764
  return;
23682
23765
  await mkdir3(path5.dirname(target), { recursive: true, mode: 448 });
23683
23766
  await writeFile2(target, next, { mode: 384 });
23684
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
+ }
23685
23779
  function trimEnd(text) {
23686
23780
  return text.replace(/\s+$/, "");
23687
23781
  }
23688
- async function wireRepos({ url: url2, token, dirs }) {
23782
+ async function wireRepos({ url: url2, token, organization, dirs }) {
23689
23783
  if (dirs.length === 0)
23690
23784
  return;
23691
23785
  await migrate.stripGlobalMcp();
@@ -23694,17 +23788,24 @@ async function wireRepos({ url: url2, token, dirs }) {
23694
23788
  for (const dir of dirs) {
23695
23789
  const here2 = isPlainObject3(projects[dir]) ? { ...projects[dir] } : {};
23696
23790
  const servers = isPlainObject3(here2.mcpServers) ? { ...here2.mcpServers } : {};
23697
- 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
+ };
23698
23797
  }
23699
23798
  });
23700
23799
  }
23800
+ function serverNameFor(organization) {
23801
+ return `craftspace-${organization}`;
23802
+ }
23701
23803
  async function unwireRepos() {
23702
23804
  await editClaudeConfig((projects) => {
23703
23805
  for (const [dir, found] of Object.entries(projects)) {
23704
23806
  if (!isPlainObject3(found) || !isPlainObject3(found.mcpServers))
23705
23807
  continue;
23706
- const servers = { ...found.mcpServers };
23707
- delete servers.craftspace;
23808
+ const servers = Object.fromEntries(Object.entries(found.mcpServers).filter(([name]) => name !== "craftspace" && !name.startsWith("craftspace-")));
23708
23809
  projects[dir] = { ...found, mcpServers: servers };
23709
23810
  }
23710
23811
  });
@@ -23724,7 +23825,7 @@ async function editClaudeConfig(edit) {
23724
23825
  async function writeAgentConfigs({ writes }) {
23725
23826
  if (writes.length === 0)
23726
23827
  return;
23727
- const owned = [...await readOwned()];
23828
+ const owned = [];
23728
23829
  for (const write of writes) {
23729
23830
  const target = expand(write.path);
23730
23831
  await mkdir3(path5.dirname(target), { recursive: true });
@@ -23736,10 +23837,10 @@ async function writeAgentConfigs({ writes }) {
23736
23837
  `, { mode: 384 });
23737
23838
  owned.push(target);
23738
23839
  }
23739
- 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));
23740
23841
  }
23741
- async function readOwned() {
23742
- const raw = await readFile3(ownedPath(), "utf8").catch(() => null);
23842
+ async function readOwned(home) {
23843
+ const raw = await readFile3(ownedPath(home), "utf8").catch(() => null);
23743
23844
  if (raw === null)
23744
23845
  return [];
23745
23846
  const parsed = OwnedSchema.safeParse(JSON.parse(raw));
@@ -23756,9 +23857,8 @@ function mergeInto(current, incoming) {
23756
23857
  function isPlainObject3(value) {
23757
23858
  return typeof value === "object" && value !== null && !Array.isArray(value);
23758
23859
  }
23759
- async function removeAgentConfigs() {
23760
- await unwireRepos();
23761
- for (const target of await readOwned()) {
23860
+ async function removeAgentConfigs(home) {
23861
+ for (const target of await readOwned(home)) {
23762
23862
  const current = await readFile3(target, "utf8").catch(() => null);
23763
23863
  if (current === null)
23764
23864
  continue;
@@ -23856,17 +23956,17 @@ function entryPath() {
23856
23956
  function expand(target) {
23857
23957
  return target.startsWith("~/") ? path5.join(os6.homedir(), target.slice(2)) : target;
23858
23958
  }
23859
- function configPath() {
23860
- return path5.join(machine.home(), "config.json");
23959
+ function configPath(home) {
23960
+ return path5.join(home, "config.json");
23861
23961
  }
23862
- function tokenPath() {
23863
- return path5.join(machine.home(), "token");
23962
+ function tokenPath(home) {
23963
+ return path5.join(home, "token");
23864
23964
  }
23865
- function ownedPath() {
23866
- return path5.join(machine.home(), "owned.json");
23965
+ function ownedPath(home) {
23966
+ return path5.join(home, "owned.json");
23867
23967
  }
23868
- function environmentPath() {
23869
- return path5.join(machine.home(), "environment.json");
23968
+ function environmentPath(home) {
23969
+ return path5.join(home, "environment.json");
23870
23970
  }
23871
23971
  function load2() {
23872
23972
  const cores = Math.max(1, os6.cpus().length);
@@ -23885,7 +23985,12 @@ function nextDelayMs(failures, busy = false) {
23885
23985
  function messageOf(error51) {
23886
23986
  return error51 instanceof Error ? error51.message : String(error51);
23887
23987
  }
23888
- 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
+ });
23889
23994
  var BuiltSchema = external_exports.object({
23890
23995
  templateId: external_exports.string(),
23891
23996
  results: external_exports.array(WorkstationStepResultSchema),
@@ -23894,6 +23999,7 @@ var BuiltSchema = external_exports.object({
23894
23999
  });
23895
24000
  var OwnedSchema = external_exports.object({ paths: external_exports.array(external_exports.string()) });
23896
24001
  var CLAUDE_FILE = ".claude.json";
24002
+ var SEAT_FILES = ["config.json", "token", "environment.json", "owned.json"];
23897
24003
  var SERVICE_NAME = "craftspace";
23898
24004
  var LAUNCH_LABEL = "app.craftspace.machine";
23899
24005
  var REQUEST_TIMEOUT_MS2 = 15e3;
@@ -23924,8 +24030,8 @@ var DEFAULT_URL = process.env.CRAFTSPACE_URL ?? "https://craftspace.app";
23924
24030
  var PROBE_EVERY_MS = 3e5;
23925
24031
  var REBUILD_AFTER_MS = 216e5;
23926
24032
  var updateWanted = null;
23927
- var probed = null;
23928
- var building = false;
24033
+ var probed = /* @__PURE__ */ new Map();
24034
+ var building = /* @__PURE__ */ new Set();
23929
24035
  var reports = /* @__PURE__ */ new Map();
23930
24036
  var running = /* @__PURE__ */ new Set();
23931
24037
 
@@ -23935,12 +24041,13 @@ var program2 = new Command();
23935
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();
23936
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 }) => {
23937
24043
  const where = await machine.controlPlane(url2);
23938
- 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 });
23939
24045
  const daemon = await machine.daemon();
23940
- ui.say(ui.heading(`Signed in as ${signed.ownerName}`));
23941
- ui.say(ui.field("Machine", `${ui.name(signed.name)} ${ui.dim(machineSpecs(signed))}`));
23942
- const workDir = await machine.workDir();
23943
- 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)));
23944
24051
  ui.say(ui.field("MCP wired", "into every repo cloned there, for Claude Code"));
23945
24052
  ui.say(ui.field("Daemon", daemon.ok ? daemon.text : ui.attention(daemon.text)));
23946
24053
  ui.say(`
@@ -23951,14 +24058,17 @@ program2.command("run").description("pick a workstation and open a terminal on i
23951
24058
  process.exitCode = await machine.run();
23952
24059
  });
23953
24060
  program2.command("status").description("what this machine is and what it is doing").action(async () => {
23954
- const { machine: found } = await machine.status();
23955
- const since = found.lastBeatAt === null ? "never" : `${secondsSince(found.lastBeatAt)}s ago`;
23956
- ui.say(ui.heading(`${found.name} ${found.state}`));
23957
- ui.say(ui.field("Owner", found.ownerName));
23958
- ui.say(ui.field("Machine", ui.dim(machineSpecs(found))));
23959
- ui.say(ui.field("CLI", `${found.cli}, beat ${since}`));
23960
- for (const session of found.sessions)
23961
- 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
+ }
23962
24072
  ui.say("\n");
23963
24073
  });
23964
24074
  program2.command("logout").description("leave the team, and undo what login wrote").action(async () => {
@@ -23966,6 +24076,9 @@ program2.command("logout").description("leave the team, and undo what login wrot
23966
24076
  ui.say(ui.ok("Left the team. Nothing of ours is running here."));
23967
24077
  });
23968
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
+ }
23969
24082
  try {
23970
24083
  await program2.parseAsync(process.argv);
23971
24084
  } catch (error51) {