@craftspace/cli 0.8.0 → 0.9.1

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