@craftspace/cli 0.5.0 → 0.6.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
@@ -18788,6 +18788,18 @@ var AuditActor;
18788
18788
  AuditActor2["System"] = "system";
18789
18789
  AuditActor2["Anonymous"] = "anonymous";
18790
18790
  })(AuditActor || (AuditActor = {}));
18791
+ var AuditResource;
18792
+ (function(AuditResource2) {
18793
+ AuditResource2["Members"] = "members";
18794
+ AuditResource2["Connections"] = "connections";
18795
+ AuditResource2["Pages"] = "pages";
18796
+ AuditResource2["Spaces"] = "spaces";
18797
+ AuditResource2["Tables"] = "tables";
18798
+ AuditResource2["Agents"] = "agents";
18799
+ AuditResource2["Automation"] = "automation";
18800
+ AuditResource2["Organization"] = "organization";
18801
+ AuditResource2["Tools"] = "tools";
18802
+ })(AuditResource || (AuditResource = {}));
18791
18803
  var AuditTier;
18792
18804
  (function(AuditTier2) {
18793
18805
  AuditTier2["Critical"] = "critical";
@@ -18900,7 +18912,8 @@ var AUDIT_RETENTION_DAYS = {
18900
18912
  };
18901
18913
  var AUDIT_GROUPS = [
18902
18914
  {
18903
- label: "Identity and access",
18915
+ resource: AuditResource.Members,
18916
+ label: "Members and tokens",
18904
18917
  actions: [
18905
18918
  AuditAction.AccessDenied,
18906
18919
  AuditAction.MemberSignedIn,
@@ -18914,7 +18927,8 @@ var AUDIT_GROUPS = [
18914
18927
  ]
18915
18928
  },
18916
18929
  {
18917
- label: "Connections",
18930
+ resource: AuditResource.Connections,
18931
+ label: "Connected apps",
18918
18932
  actions: [
18919
18933
  AuditAction.ConnectionCreated,
18920
18934
  AuditAction.ConnectionReconnected,
@@ -18926,23 +18940,34 @@ var AUDIT_GROUPS = [
18926
18940
  ]
18927
18941
  },
18928
18942
  {
18929
- label: "Knowledge",
18943
+ resource: AuditResource.Pages,
18944
+ label: "Pages",
18930
18945
  actions: [
18931
18946
  AuditAction.PageCreated,
18932
18947
  AuditAction.PageUpdated,
18933
18948
  AuditAction.PageDeleted,
18934
18949
  AuditAction.PageMoved,
18935
18950
  AuditAction.PageShared,
18936
- AuditAction.PageUnshared,
18951
+ AuditAction.PageUnshared
18952
+ ]
18953
+ },
18954
+ {
18955
+ resource: AuditResource.Spaces,
18956
+ label: "Spaces",
18957
+ actions: [
18937
18958
  AuditAction.SpaceCreated,
18938
18959
  AuditAction.SpaceRenamed,
18939
18960
  AuditAction.SpaceDeleted,
18940
- AuditAction.SpaceAccessChanged,
18941
- AuditAction.TableCreated,
18942
- AuditAction.TableDeleted
18961
+ AuditAction.SpaceAccessChanged
18943
18962
  ]
18944
18963
  },
18945
18964
  {
18965
+ resource: AuditResource.Tables,
18966
+ label: "Tables",
18967
+ actions: [AuditAction.TableCreated, AuditAction.TableDeleted]
18968
+ },
18969
+ {
18970
+ resource: AuditResource.Agents,
18946
18971
  label: "Agents and channels",
18947
18972
  actions: [
18948
18973
  AuditAction.AgentCreated,
@@ -18953,7 +18978,8 @@ var AUDIT_GROUPS = [
18953
18978
  ]
18954
18979
  },
18955
18980
  {
18956
- label: "Automation",
18981
+ resource: AuditResource.Automation,
18982
+ label: "Workflows and kits",
18957
18983
  actions: [
18958
18984
  AuditAction.KitInstalled,
18959
18985
  AuditAction.KitRemoved,
@@ -18964,10 +18990,7 @@ var AUDIT_GROUPS = [
18964
18990
  ]
18965
18991
  },
18966
18992
  {
18967
- label: "MCP calls",
18968
- actions: [AuditAction.McpToolCall, AuditAction.McpRequest]
18969
- },
18970
- {
18993
+ resource: AuditResource.Organization,
18971
18994
  label: "Organization",
18972
18995
  actions: [
18973
18996
  AuditAction.OrganizationRenamed,
@@ -18975,10 +18998,17 @@ var AUDIT_GROUPS = [
18975
18998
  AuditAction.OrganizationModelChanged,
18976
18999
  AuditAction.OrganizationDeleted
18977
19000
  ]
19001
+ },
19002
+ {
19003
+ resource: AuditResource.Tools,
19004
+ label: "Tool and MCP calls",
19005
+ actions: [AuditAction.McpToolCall, AuditAction.McpRequest]
18978
19006
  }
18979
19007
  ];
19008
+ var AUDIT_RESOURCES = Object.fromEntries(AUDIT_GROUPS.flatMap((group) => group.actions.map((action) => [action, group.resource])));
18980
19009
  var AUDIT_LISTED_ACTIONS = AUDIT_GROUPS.flatMap((group) => group.actions);
18981
- var AUDIT_DEFAULT_ACTIONS = AUDIT_LISTED_ACTIONS.filter((action) => AUDIT_TIERS[action] !== AuditTier.Volume);
19010
+ var AUDIT_QUIET_ACTIONS = [AuditAction.McpRequest, AuditAction.PageRead, AuditAction.PageWrite];
19011
+ var AUDIT_DEFAULT_ACTIONS = AUDIT_LISTED_ACTIONS.filter((action) => !AUDIT_QUIET_ACTIONS.includes(action));
18982
19012
  var AUDIT_LABELS = {
18983
19013
  [AuditAction.AccessDenied]: "Denied request",
18984
19014
  [AuditAction.MemberSignedIn]: "Signed in",
@@ -19028,6 +19058,11 @@ var AUDIT_LABELS = {
19028
19058
  [AuditAction.PageRead]: "Read page",
19029
19059
  [AuditAction.PageWrite]: "Wrote page"
19030
19060
  };
19061
+ var AuditAppSchema = external_exports.object({
19062
+ kind: external_exports.string(),
19063
+ id: external_exports.string().optional(),
19064
+ name: external_exports.string().optional()
19065
+ });
19031
19066
  var AuditTargetSchema = external_exports.object({
19032
19067
  type: external_exports.string(),
19033
19068
  id: external_exports.string().optional(),
@@ -19045,6 +19080,8 @@ var AuditEntrySchema = external_exports.object({
19045
19080
  image: external_exports.string().optional()
19046
19081
  }),
19047
19082
  target: AuditTargetSchema.optional(),
19083
+ app: AuditAppSchema.optional(),
19084
+ outcome: external_exports.enum(["ok", "error"]).optional(),
19048
19085
  data: external_exports.record(external_exports.string(), external_exports.unknown()),
19049
19086
  ip: external_exports.string().optional(),
19050
19087
  userAgent: external_exports.string().optional(),
@@ -19059,15 +19096,38 @@ var AuditSummarySchema = external_exports.object({
19059
19096
  var AuditQuerySchema = external_exports.object({
19060
19097
  days: external_exports.coerce.number().int().refine((value) => AUDIT_RANGES.includes(value)).default(AUDIT_DEFAULT_DAYS),
19061
19098
  page: external_exports.coerce.number().int().min(1).default(1),
19062
- action: external_exports.union([external_exports.enum(AuditAction), external_exports.array(external_exports.enum(AuditAction))]).optional().transform((value) => value === void 0 ? void 0 : Array.isArray(value) ? value : [value]),
19099
+ action: manyOf(external_exports.enum(AuditAction)),
19063
19100
  actor: external_exports.enum(AuditActor).optional(),
19064
- accountId: external_exports.string().optional()
19101
+ account: manyOf(external_exports.string()),
19102
+ app: manyOf(external_exports.string())
19103
+ });
19104
+ var AuditFacetsSchema = external_exports.object({
19105
+ actions: external_exports.record(external_exports.string(), external_exports.number()),
19106
+ accounts: external_exports.array(external_exports.object({
19107
+ accountId: external_exports.string(),
19108
+ name: external_exports.string(),
19109
+ email: external_exports.string().optional(),
19110
+ image: external_exports.string().optional(),
19111
+ count: external_exports.number()
19112
+ })),
19113
+ apps: external_exports.array(external_exports.object({ kind: external_exports.string(), name: external_exports.string().optional(), count: external_exports.number() }))
19065
19114
  });
19066
19115
  var AuditResponseSchema = external_exports.object({
19067
19116
  summary: AuditSummarySchema,
19068
19117
  entries: external_exports.array(AuditEntrySchema),
19118
+ facets: AuditFacetsSchema,
19069
19119
  total: external_exports.number()
19070
19120
  });
19121
+ var VERB_OVERRIDES = {
19122
+ [AuditAction.McpToolCall]: "called",
19123
+ [AuditAction.McpRequest]: "called",
19124
+ [AuditAction.PageRead]: "read",
19125
+ [AuditAction.PageWrite]: "written",
19126
+ [AuditAction.AdminEnteredOrg]: "opened as support"
19127
+ };
19128
+ function manyOf(schema) {
19129
+ return external_exports.union([schema, external_exports.array(schema)]).optional().transform((value) => value === void 0 ? void 0 : Array.isArray(value) ? value : [value]);
19130
+ }
19071
19131
 
19072
19132
  // ../../shared/dist/organization/install-admin.js
19073
19133
  var InstallCredentialNameSchema = external_exports.enum([
@@ -19838,7 +19898,7 @@ var SaveWorkstationBodySchema = external_exports.object({
19838
19898
  var WorkstationResponseSchema = external_exports.object({ workstation: WorkstationSchema });
19839
19899
 
19840
19900
  // ../../shared/dist/machines/machine.js
19841
- var CLI_VERSION = "0.5.0";
19901
+ var CLI_VERSION = "0.6.0";
19842
19902
  var CLI_PACKAGE = "@craftspace/cli";
19843
19903
  var CLI_INSTALL = `npm i -g ${CLI_PACKAGE}`;
19844
19904
  var MACHINE_BEAT_INTERVAL_MS = 15e3;
@@ -19866,6 +19926,13 @@ var MachineToolSchema = external_exports.object({
19866
19926
  ok: external_exports.boolean(),
19867
19927
  detail: external_exports.string().max(200)
19868
19928
  });
19929
+ var MachineRepoSchema = external_exports.object({
19930
+ name: external_exports.string().max(200),
19931
+ ref: external_exports.string().max(200),
19932
+ path: external_exports.string().max(400),
19933
+ ok: external_exports.boolean(),
19934
+ detail: external_exports.string().max(300)
19935
+ });
19869
19936
  var MachineSchema = external_exports.object({
19870
19937
  id: external_exports.string(),
19871
19938
  tokenId: external_exports.string(),
@@ -19889,6 +19956,8 @@ var MachineSchema = external_exports.object({
19889
19956
  tools: external_exports.array(MachineToolSchema),
19890
19957
  environment: external_exports.array(WorkstationStepResultSchema),
19891
19958
  templateId: external_exports.string().nullable(),
19959
+ workDir: external_exports.string().nullable(),
19960
+ repos: external_exports.array(MachineRepoSchema),
19892
19961
  authorizedKeys: external_exports.array(MachineAuthorizedKeySchema),
19893
19962
  lastBeatAt: external_exports.coerce.date().nullable(),
19894
19963
  lastAskedAt: external_exports.coerce.date().nullable(),
@@ -19953,11 +20022,14 @@ var MachineBeatBodySchema = external_exports.object({
19953
20022
  tools: external_exports.array(MachineToolSchema).max(10).default([]),
19954
20023
  environment: external_exports.array(WorkstationStepResultSchema).max(WORKSTATION_MAX_STEPS).default([]),
19955
20024
  templateId: external_exports.string().max(64).nullable().default(null),
20025
+ workDir: external_exports.string().max(400).optional(),
20026
+ repos: external_exports.array(MachineRepoSchema).max(WORKSTATION_MAX_REPOS).default([]),
19956
20027
  sshUser: external_exports.string().min(1).max(64).optional()
19957
20028
  });
19958
20029
  var MachineEnvironmentSchema = external_exports.object({
19959
20030
  templateId: external_exports.string().nullable(),
19960
- steps: external_exports.array(WorkstationStepSchema)
20031
+ steps: external_exports.array(WorkstationStepSchema),
20032
+ repos: external_exports.array(WorkstationRepoSchema).default([])
19961
20033
  });
19962
20034
  var MachineBeatResponseSchema = external_exports.object({
19963
20035
  cli: external_exports.object({ version: external_exports.string(), url: external_exports.string(), sha256: external_exports.string() }).nullable(),
@@ -20316,7 +20388,7 @@ var MemberActivityListResponse = external_exports.object({
20316
20388
 
20317
20389
  // dist/machine.js
20318
20390
  import { execFile as execFile3, spawn as spawn2 } from "node:child_process";
20319
- import { mkdir, readFile as readFile2, readdir, rm, writeFile } from "node:fs/promises";
20391
+ import { mkdir as mkdir2, readFile as readFile2, readdir, rm, writeFile } from "node:fs/promises";
20320
20392
  import os5 from "node:os";
20321
20393
  import path4 from "node:path";
20322
20394
  import { promisify as promisify2 } from "node:util";
@@ -21289,11 +21361,11 @@ function readlineWidth() {
21289
21361
 
21290
21362
  // ../../../node_modules/@inquirer/core/dist/lib/pagination/use-pagination.js
21291
21363
  function usePointerPosition({ active, renderedItems, pageSize, loop }) {
21292
- const state = useRef({
21364
+ const state2 = useRef({
21293
21365
  lastPointer: active,
21294
21366
  lastActive: void 0
21295
21367
  });
21296
- const { lastPointer, lastActive } = state.current;
21368
+ const { lastPointer, lastActive } = state2.current;
21297
21369
  const middle = Math.floor(pageSize / 2);
21298
21370
  const renderedLength = renderedItems.reduce((acc, item) => acc + item.length, 0);
21299
21371
  const defaultPointerPosition = renderedItems.slice(0, active).reduce((acc, item) => acc + item.length, 0);
@@ -21333,8 +21405,8 @@ function usePointerPosition({ active, renderedItems, pageSize, loop }) {
21333
21405
  );
21334
21406
  }
21335
21407
  }
21336
- state.current.lastPointer = pointer;
21337
- state.current.lastActive = active;
21408
+ state2.current.lastPointer = pointer;
21409
+ state2.current.lastActive = active;
21338
21410
  return pointer;
21339
21411
  }
21340
21412
  function usePagination({ items, active, renderItem, pageSize, loop = true }) {
@@ -22237,7 +22309,7 @@ var runner = {
22237
22309
  child.on("close", (code) => resolve(code ?? 0));
22238
22310
  });
22239
22311
  },
22240
- async unattended({ work, onProgress }) {
22312
+ async unattended({ work, cwd = os2.homedir(), onProgress }) {
22241
22313
  const argv = headlessArgv(work);
22242
22314
  const raw = argv[0] === "script";
22243
22315
  let output = "";
@@ -22267,7 +22339,7 @@ var runner = {
22267
22339
  const code = await new Promise((resolve) => {
22268
22340
  const child = spawn(argv[0], argv.slice(1), {
22269
22341
  stdio: ["ignore", "pipe", "pipe"],
22270
- cwd: os2.homedir(),
22342
+ cwd,
22271
22343
  env: {
22272
22344
  ...process.env,
22273
22345
  TERM: process.env.TERM ?? "xterm-256color",
@@ -22365,6 +22437,7 @@ function parseJson(line) {
22365
22437
 
22366
22438
  // dist/setup.js
22367
22439
  import { execFile } from "node:child_process";
22440
+ import { mkdir } from "node:fs/promises";
22368
22441
  import os4 from "node:os";
22369
22442
  import path3 from "node:path";
22370
22443
 
@@ -22938,6 +23011,24 @@ var setup = {
22938
23011
  if (missing.length > 0)
22939
23012
  process.env.PATH = [...current, ...missing].join(path3.delimiter);
22940
23013
  },
23014
+ workDir() {
23015
+ return path3.join(os4.homedir(), WORK_DIR_NAME);
23016
+ },
23017
+ async cloneRepos({ repos, say }) {
23018
+ if (repos.length === 0)
23019
+ return [];
23020
+ await mkdir(setup.workDir(), { recursive: true });
23021
+ const cloned = [];
23022
+ for (const [index, repo] of repos.entries()) {
23023
+ say?.(ui.progress({ name: repo.name, done: index, total: repos.length }));
23024
+ const at = path3.join(setup.workDir(), repo.name.split("/").pop() ?? repo.name);
23025
+ const outcome = await checkout(repo, at);
23026
+ cloned.push({ name: repo.name, ref: repo.ref, path: at, ...outcome });
23027
+ say?.(`${ui.clear()}${outcome.ok ? ui.ok(`${repo.name} \xB7 ${outcome.detail}`) : ui.bad(repo.name, outcome.detail)}`);
23028
+ }
23029
+ say?.(ui.tally({ done: cloned.filter((repo) => repo.ok).length, total: cloned.length, noun: "cloned" }));
23030
+ return cloned;
23031
+ },
22941
23032
  async runSteps({ steps, say }) {
22942
23033
  const results = [];
22943
23034
  for (const [index, step] of steps.entries()) {
@@ -22953,6 +23044,43 @@ var setup = {
22953
23044
  return results;
22954
23045
  }
22955
23046
  };
23047
+ async function checkout(repo, at) {
23048
+ const opened = await git(["-C", at, "rev-parse", "--abbrev-ref", "HEAD"]);
23049
+ if (!opened.ok) {
23050
+ const url2 = `https://github.com/${repo.name}.git`;
23051
+ const made = await git(["clone", "--depth", String(CLONE_DEPTH), "--branch", repo.ref, url2, at]);
23052
+ if (!made.ok)
23053
+ return { ok: false, detail: made.detail };
23054
+ return { ok: true, detail: await state({ repo, at, branch: repo.ref, dirty: false }) };
23055
+ }
23056
+ const branch = opened.detail;
23057
+ const dirty = (await git(["-C", at, "status", "--porcelain"])).detail !== "";
23058
+ const pulled = branch === repo.ref && !dirty ? await git(["-C", at, "pull", "--ff-only"]) : null;
23059
+ return { ok: true, detail: await state({ repo, at, branch, dirty, pulled }) };
23060
+ }
23061
+ async function state({ repo, at, branch, dirty, pulled }) {
23062
+ const head = await git(["-C", at, "rev-parse", "--short", "HEAD"]);
23063
+ return [
23064
+ `${branch} @ ${head.detail}`,
23065
+ branch === repo.ref ? "" : `your team asks for ${repo.ref}`,
23066
+ dirty ? "uncommitted work here, so it was left alone" : "",
23067
+ pulled?.ok === false ? `could not pull: ${pulled.detail}` : ""
23068
+ ].filter((part) => part !== "").join(" \xB7 ");
23069
+ }
23070
+ function git(argv) {
23071
+ return new Promise((resolve) => {
23072
+ execFile("git", argv, {
23073
+ timeout: CLONE_TIMEOUT_MS,
23074
+ maxBuffer: SCRIPT_BUFFER_BYTES,
23075
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_ASKPASS: "", GIT_LFS_SKIP_SMUDGE: "1" }
23076
+ }, (error51, stdout, stderr) => {
23077
+ if (error51 === null)
23078
+ return resolve({ ok: true, detail: stdout.trim() });
23079
+ resolve({ ok: false, detail: lastLines(`${stderr}
23080
+ ${stdout}`) || error51.message.split("\n")[0] || "git failed" });
23081
+ });
23082
+ });
23083
+ }
22956
23084
  function runScript(script) {
22957
23085
  return new Promise((resolve) => {
22958
23086
  execFile("/bin/sh", ["-e", "-c", script], { timeout: STEP_TIMEOUT_MS, maxBuffer: SCRIPT_BUFFER_BYTES }, (error51, stdout, stderr) => {
@@ -22977,6 +23105,9 @@ function toolDirs() {
22977
23105
  "/usr/local/bin"
22978
23106
  ];
22979
23107
  }
23108
+ var WORK_DIR_NAME = "craftspace";
23109
+ var CLONE_DEPTH = 20;
23110
+ var CLONE_TIMEOUT_MS = 9e5;
22980
23111
  var STEP_TIMEOUT_MS = 6e5;
22981
23112
  var SCRIPT_BUFFER_BYTES = 4 * 1024 * 1024;
22982
23113
  var KEPT_LINES = 3;
@@ -23051,7 +23182,7 @@ var machine = {
23051
23182
  },
23052
23183
  schema: MachineLoginResponseSchema
23053
23184
  });
23054
- await mkdir(machine.home(), { recursive: true, mode: 448 });
23185
+ await mkdir2(machine.home(), { recursive: true, mode: 448 });
23055
23186
  await writeFile(configPath(), JSON.stringify({ url: url2, machineId: answer.machine.id }, null, 2), { mode: 384 });
23056
23187
  await writeFile(tokenPath(), token, { mode: 384 });
23057
23188
  await writeAgentConfigs({ url: url2, token, writes: answer.write });
@@ -23110,6 +23241,8 @@ ${ui.ok(`Signed in as ${signed.ownerName}. This machine is ${ui.name(signed.name
23110
23241
  tools: await checks(),
23111
23242
  environment: built?.results ?? [],
23112
23243
  templateId: built?.templateId ?? null,
23244
+ workDir: setup.workDir(),
23245
+ repos: built?.repos ?? [],
23113
23246
  sshUser: os5.userInfo().username
23114
23247
  },
23115
23248
  schema: MachineBeatResponseSchema
@@ -23124,7 +23257,7 @@ ${ui.ok(`Signed in as ${signed.ownerName}. This machine is ${ui.name(signed.name
23124
23257
  if (running.has(work.id))
23125
23258
  continue;
23126
23259
  running.add(work.id);
23127
- void runner.unattended({ work, onProgress: report }).then(report).catch((error51) => report({ id: work.id, state: "failed", output: messageOf(error51) })).finally(() => running.delete(work.id));
23260
+ 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));
23128
23261
  }
23129
23262
  return running.size > 0 || answer.work.length > 0;
23130
23263
  },
@@ -23133,7 +23266,7 @@ ${ui.ok(`Signed in as ${signed.ownerName}. This machine is ${ui.name(signed.name
23133
23266
  let busy = false;
23134
23267
  let checkedAt = 0;
23135
23268
  for (; ; ) {
23136
- if (!busy && Date.now() - checkedAt > UPDATE_EVERY_MS) {
23269
+ if (!busy && (failures > 2 || Date.now() - checkedAt > UPDATE_EVERY_MS)) {
23137
23270
  checkedAt = Date.now();
23138
23271
  const moved = await update.toLatest();
23139
23272
  if (moved !== null) {
@@ -23260,17 +23393,40 @@ async function buildEnvironment(next, say) {
23260
23393
  if (building || !installs() || next.templateId === null)
23261
23394
  return;
23262
23395
  const built = await readEnvironment();
23263
- if (!buildIsDue(built, next.templateId))
23396
+ const buildsSteps = buildIsDue(built, next.templateId);
23397
+ const clonesRepos = cloneIsDue(built, next.repos);
23398
+ if (!buildsSteps && !clonesRepos)
23264
23399
  return;
23265
23400
  building = true;
23266
23401
  try {
23267
- say?.(ui.heading(`Building the ${next.steps.length} setup scripts your team runs on every box`));
23268
- const results = await setup.runSteps({ steps: next.steps, say });
23269
- await writeEnvironment({ templateId: next.templateId, results, at: Date.now() });
23402
+ let results = built?.results ?? [];
23403
+ let repos = built?.repos ?? [];
23404
+ if (buildsSteps) {
23405
+ say?.(ui.heading(`Building the ${next.steps.length} setup scripts your team runs on every box`));
23406
+ results = await setup.runSteps({ steps: next.steps, say });
23407
+ }
23408
+ if (clonesRepos) {
23409
+ say?.(ui.heading(`Cloning the ${next.repos.length} repos your team works in, into ${setup.workDir()}`));
23410
+ repos = await setup.cloneRepos({ repos: next.repos, say });
23411
+ await acceptClaudeGates(["craftspace"], repos.filter((repo) => repo.ok).map((repo) => repo.path));
23412
+ }
23413
+ await writeEnvironment({ templateId: next.templateId, results, repos, at: Date.now() });
23270
23414
  } finally {
23271
23415
  building = false;
23272
23416
  }
23273
23417
  }
23418
+ function cloneIsDue(built, wanted) {
23419
+ const cloned = built?.repos ?? [];
23420
+ if (wanted.length === 0 && cloned.length === 0)
23421
+ return false;
23422
+ if (wanted.length !== cloned.length)
23423
+ return true;
23424
+ const missing = wanted.some((repo) => !cloned.some((seen) => seen.name === repo.name && seen.ref === repo.ref && seen.ok));
23425
+ return missing || Date.now() - (built?.at ?? 0) > REBUILD_AFTER_MS;
23426
+ }
23427
+ function startIn(built) {
23428
+ return built?.repos.find((repo) => repo.ok)?.path ?? os5.homedir();
23429
+ }
23274
23430
  function buildIsDue(built, templateId) {
23275
23431
  if (built === null || built.templateId !== templateId)
23276
23432
  return true;
@@ -23286,7 +23442,7 @@ async function readEnvironment() {
23286
23442
  return parsed.success ? parsed.data : null;
23287
23443
  }
23288
23444
  async function writeEnvironment(built) {
23289
- await mkdir(machine.home(), { recursive: true, mode: 448 });
23445
+ await mkdir2(machine.home(), { recursive: true, mode: 448 });
23290
23446
  await writeFile(environmentPath(), JSON.stringify(built, null, 2), { mode: 384 });
23291
23447
  }
23292
23448
  function installs() {
@@ -23363,7 +23519,7 @@ async function ensureKey() {
23363
23519
  const existing = await readFile2(pub, "utf8").catch(() => null);
23364
23520
  if (existing !== null)
23365
23521
  return existing.trim();
23366
- await mkdir(machine.home(), { recursive: true, mode: 448 });
23522
+ await mkdir2(machine.home(), { recursive: true, mode: 448 });
23367
23523
  await run2("ssh-keygen", ["-t", "ed25519", "-N", "", "-q", "-f", priv, "-C", `craftspace-${os5.hostname()}`]);
23368
23524
  return (await readFile2(pub, "utf8")).trim();
23369
23525
  }
@@ -23403,7 +23559,7 @@ ${KEYS_END}
23403
23559
  const next = `${trimEnd(before)}${trimEnd(before) === "" ? "" : "\n"}${block}${after.replace(/^\n/, "")}`;
23404
23560
  if (next === current)
23405
23561
  return;
23406
- await mkdir(path4.dirname(target), { recursive: true, mode: 448 });
23562
+ await mkdir2(path4.dirname(target), { recursive: true, mode: 448 });
23407
23563
  await writeFile(target, next, { mode: 384 });
23408
23564
  }
23409
23565
  function trimEnd(text) {
@@ -23414,7 +23570,7 @@ async function writeAgentConfigs({ url: url2, token, writes }) {
23414
23570
  const owned = [];
23415
23571
  for (const write of planned) {
23416
23572
  const target = expand(write.path);
23417
- await mkdir(path4.dirname(target), { recursive: true });
23573
+ await mkdir2(path4.dirname(target), { recursive: true });
23418
23574
  const current = await readFile2(target, "utf8").catch(() => null);
23419
23575
  if (current !== null)
23420
23576
  await writeFile(`${target}.craftspace-backup`, current, { flag: "wx" }).catch(() => void 0);
@@ -23429,22 +23585,29 @@ async function writeAgentConfigs({ url: url2, token, writes }) {
23429
23585
  function serversIn(writes) {
23430
23586
  return writes.flatMap((write) => isPlainObject3(write.contents.mcpServers) ? Object.keys(write.contents.mcpServers) : []);
23431
23587
  }
23432
- async function acceptClaudeGates(servers) {
23588
+ async function acceptClaudeGates(servers, dirs = [os5.homedir()]) {
23433
23589
  const target = path4.join(os5.homedir(), ".claude.json");
23434
23590
  const raw = await readFile2(target, "utf8").catch(() => null);
23435
23591
  if (raw === null)
23436
23592
  return;
23437
23593
  const parsed = JSON.parse(raw);
23438
23594
  const projects = isPlainObject3(parsed.projects) ? parsed.projects : {};
23439
- const found = projects[os5.homedir()];
23440
- const here2 = isPlainObject3(found) ? { ...found } : {};
23441
- const enabled = /* @__PURE__ */ new Set([...Array.isArray(here2.enabledMcpjsonServers) ? here2.enabledMcpjsonServers : [], ...servers]);
23442
- if (here2.hasTrustDialogAccepted === true && enabled.size === asArray(here2.enabledMcpjsonServers).length)
23595
+ const opened = { ...projects };
23596
+ let moved = false;
23597
+ for (const dir of dirs) {
23598
+ const found = opened[dir];
23599
+ const here2 = isPlainObject3(found) ? { ...found } : {};
23600
+ const enabled = /* @__PURE__ */ new Set([...asArray(here2.enabledMcpjsonServers), ...servers]);
23601
+ if (here2.hasTrustDialogAccepted === true && enabled.size === asArray(here2.enabledMcpjsonServers).length)
23602
+ continue;
23603
+ here2.hasTrustDialogAccepted = true;
23604
+ here2.enabledMcpjsonServers = [...enabled];
23605
+ opened[dir] = here2;
23606
+ moved = true;
23607
+ }
23608
+ if (!moved)
23443
23609
  return;
23444
- here2.hasTrustDialogAccepted = true;
23445
- here2.enabledMcpjsonServers = [...enabled];
23446
- const next = { ...parsed, projects: { ...projects, [os5.homedir()]: here2 } };
23447
- await writeFile(target, `${JSON.stringify(next, null, 2)}
23610
+ await writeFile(target, `${JSON.stringify({ ...parsed, projects: opened }, null, 2)}
23448
23611
  `, { mode: 384 });
23449
23612
  }
23450
23613
  function asArray(value) {
@@ -23495,16 +23658,17 @@ async function installService() {
23495
23658
  return;
23496
23659
  if (process.platform === "linux") {
23497
23660
  const dir = path4.join(os5.homedir(), ".config", "systemd", "user");
23498
- await mkdir(dir, { recursive: true });
23661
+ await mkdir2(dir, { recursive: true });
23499
23662
  await writeFile(path4.join(dir, `${SERVICE_NAME}.service`), systemdUnit());
23500
23663
  await run2("loginctl", ["enable-linger", os5.userInfo().username]).catch(() => void 0);
23501
23664
  await run2("systemctl", ["--user", "daemon-reload"]).catch(() => void 0);
23502
- await run2("systemctl", ["--user", "enable", "--now", SERVICE_NAME]).catch(() => void 0);
23665
+ await run2("systemctl", ["--user", "enable", SERVICE_NAME]).catch(() => void 0);
23666
+ await run2("systemctl", ["--user", "restart", SERVICE_NAME]).catch(() => void 0);
23503
23667
  return;
23504
23668
  }
23505
23669
  if (process.platform === "darwin") {
23506
23670
  const target = path4.join(os5.homedir(), "Library", "LaunchAgents", `${LAUNCH_LABEL}.plist`);
23507
- await mkdir(path4.dirname(target), { recursive: true });
23671
+ await mkdir2(path4.dirname(target), { recursive: true });
23508
23672
  await writeFile(target, launchdPlist());
23509
23673
  await run2("launchctl", ["unload", target]).catch(() => void 0);
23510
23674
  await run2("launchctl", ["load", target]).catch(() => void 0);
@@ -23600,6 +23764,7 @@ var ConfigSchema = external_exports.object({ url: external_exports.string(), mac
23600
23764
  var BuiltSchema = external_exports.object({
23601
23765
  templateId: external_exports.string(),
23602
23766
  results: external_exports.array(WorkstationStepResultSchema),
23767
+ repos: external_exports.array(MachineRepoSchema).default([]),
23603
23768
  at: external_exports.number()
23604
23769
  });
23605
23770
  var OwnedSchema = external_exports.object({ paths: external_exports.array(external_exports.string()) });
package/dist/machine.js CHANGED
@@ -3,7 +3,7 @@ import { mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { promisify } from 'node:util';
6
- import { CLI_VERSION, MACHINE_BEAT_BUSY_INTERVAL_MS, MACHINE_BEAT_INTERVAL_MS, MachineBeatResponseSchema, MachineLoginResponseSchema, MachineSchema, MachineSessionSchema, MachinesResponseSchema, WorkstationStepResultSchema, } from '@craftspace/shared';
6
+ import { CLI_VERSION, MACHINE_BEAT_BUSY_INTERVAL_MS, MACHINE_BEAT_INTERVAL_MS, MachineBeatResponseSchema, MachineLoginResponseSchema, MachineSchema, MachineRepoSchema, MachineSessionSchema, MachinesResponseSchema, WorkstationStepResultSchema, } from '@craftspace/shared';
7
7
  import { confirm, input, select } from '@inquirer/prompts';
8
8
  import { z } from 'zod';
9
9
  import { probe } from './probe.js';
@@ -89,6 +89,8 @@ export const machine = {
89
89
  tools: await checks(),
90
90
  environment: built?.results ?? [],
91
91
  templateId: built?.templateId ?? null,
92
+ workDir: setup.workDir(),
93
+ repos: built?.repos ?? [],
92
94
  sshUser: os.userInfo().username,
93
95
  },
94
96
  schema: MachineBeatResponseSchema,
@@ -104,7 +106,7 @@ export const machine = {
104
106
  continue;
105
107
  running.add(work.id);
106
108
  void runner
107
- .unattended({ work, onProgress: report })
109
+ .unattended({ work, cwd: startIn(built), onProgress: report })
108
110
  .then(report)
109
111
  .catch((error) => report({ id: work.id, state: 'failed', output: messageOf(error) }))
110
112
  .finally(() => running.delete(work.id));
@@ -116,7 +118,7 @@ export const machine = {
116
118
  let busy = false;
117
119
  let checkedAt = 0;
118
120
  for (;;) {
119
- if (!busy && Date.now() - checkedAt > UPDATE_EVERY_MS) {
121
+ if (!busy && (failures > 2 || Date.now() - checkedAt > UPDATE_EVERY_MS)) {
120
122
  checkedAt = Date.now();
121
123
  const moved = await update.toLatest();
122
124
  if (moved !== null) {
@@ -238,18 +240,41 @@ async function buildEnvironment(next, say) {
238
240
  if (building || !installs() || next.templateId === null)
239
241
  return;
240
242
  const built = await readEnvironment();
241
- if (!buildIsDue(built, next.templateId))
243
+ const buildsSteps = buildIsDue(built, next.templateId);
244
+ const clonesRepos = cloneIsDue(built, next.repos);
245
+ if (!buildsSteps && !clonesRepos)
242
246
  return;
243
247
  building = true;
244
248
  try {
245
- say?.(ui.heading(`Building the ${next.steps.length} setup scripts your team runs on every box`));
246
- const results = await setup.runSteps({ steps: next.steps, say });
247
- await writeEnvironment({ templateId: next.templateId, results, at: Date.now() });
249
+ let results = built?.results ?? [];
250
+ let repos = built?.repos ?? [];
251
+ if (buildsSteps) {
252
+ say?.(ui.heading(`Building the ${next.steps.length} setup scripts your team runs on every box`));
253
+ results = await setup.runSteps({ steps: next.steps, say });
254
+ }
255
+ if (clonesRepos) {
256
+ say?.(ui.heading(`Cloning the ${next.repos.length} repos your team works in, into ${setup.workDir()}`));
257
+ repos = await setup.cloneRepos({ repos: next.repos, say });
258
+ await acceptClaudeGates(['craftspace'], repos.filter((repo) => repo.ok).map((repo) => repo.path));
259
+ }
260
+ await writeEnvironment({ templateId: next.templateId, results, repos, at: Date.now() });
248
261
  }
249
262
  finally {
250
263
  building = false;
251
264
  }
252
265
  }
266
+ function cloneIsDue(built, wanted) {
267
+ const cloned = built?.repos ?? [];
268
+ if (wanted.length === 0 && cloned.length === 0)
269
+ return false;
270
+ if (wanted.length !== cloned.length)
271
+ return true;
272
+ const missing = wanted.some((repo) => !cloned.some((seen) => seen.name === repo.name && seen.ref === repo.ref && seen.ok));
273
+ return missing || Date.now() - (built?.at ?? 0) > REBUILD_AFTER_MS;
274
+ }
275
+ function startIn(built) {
276
+ return built?.repos.find((repo) => repo.ok)?.path ?? os.homedir();
277
+ }
253
278
  function buildIsDue(built, templateId) {
254
279
  if (built === null || built.templateId !== templateId)
255
280
  return true;
@@ -403,22 +428,29 @@ async function writeAgentConfigs({ url, token, writes, }) {
403
428
  function serversIn(writes) {
404
429
  return writes.flatMap((write) => isPlainObject(write.contents.mcpServers) ? Object.keys(write.contents.mcpServers) : []);
405
430
  }
406
- async function acceptClaudeGates(servers) {
431
+ async function acceptClaudeGates(servers, dirs = [os.homedir()]) {
407
432
  const target = path.join(os.homedir(), '.claude.json');
408
433
  const raw = await readFile(target, 'utf8').catch(() => null);
409
434
  if (raw === null)
410
435
  return;
411
436
  const parsed = JSON.parse(raw);
412
437
  const projects = isPlainObject(parsed.projects) ? parsed.projects : {};
413
- const found = projects[os.homedir()];
414
- const here = isPlainObject(found) ? { ...found } : {};
415
- const enabled = new Set([...(Array.isArray(here.enabledMcpjsonServers) ? here.enabledMcpjsonServers : []), ...servers]);
416
- if (here.hasTrustDialogAccepted === true && enabled.size === asArray(here.enabledMcpjsonServers).length)
438
+ const opened = { ...projects };
439
+ let moved = false;
440
+ for (const dir of dirs) {
441
+ const found = opened[dir];
442
+ const here = isPlainObject(found) ? { ...found } : {};
443
+ const enabled = new Set([...asArray(here.enabledMcpjsonServers), ...servers]);
444
+ if (here.hasTrustDialogAccepted === true && enabled.size === asArray(here.enabledMcpjsonServers).length)
445
+ continue;
446
+ here.hasTrustDialogAccepted = true;
447
+ here.enabledMcpjsonServers = [...enabled];
448
+ opened[dir] = here;
449
+ moved = true;
450
+ }
451
+ if (!moved)
417
452
  return;
418
- here.hasTrustDialogAccepted = true;
419
- here.enabledMcpjsonServers = [...enabled];
420
- const next = { ...parsed, projects: { ...projects, [os.homedir()]: here } };
421
- await writeFile(target, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
453
+ await writeFile(target, `${JSON.stringify({ ...parsed, projects: opened }, null, 2)}\n`, { mode: 0o600 });
422
454
  }
423
455
  function asArray(value) {
424
456
  return Array.isArray(value) ? value : [];
@@ -472,7 +504,8 @@ async function installService() {
472
504
  await writeFile(path.join(dir, `${SERVICE_NAME}.service`), systemdUnit());
473
505
  await run('loginctl', ['enable-linger', os.userInfo().username]).catch(() => undefined);
474
506
  await run('systemctl', ['--user', 'daemon-reload']).catch(() => undefined);
475
- await run('systemctl', ['--user', 'enable', '--now', SERVICE_NAME]).catch(() => undefined);
507
+ await run('systemctl', ['--user', 'enable', SERVICE_NAME]).catch(() => undefined);
508
+ await run('systemctl', ['--user', 'restart', SERVICE_NAME]).catch(() => undefined);
476
509
  return;
477
510
  }
478
511
  if (process.platform === 'darwin') {
@@ -573,6 +606,7 @@ const ConfigSchema = z.object({ url: z.string(), machineId: z.string() });
573
606
  const BuiltSchema = z.object({
574
607
  templateId: z.string(),
575
608
  results: z.array(WorkstationStepResultSchema),
609
+ repos: z.array(MachineRepoSchema).default([]),
576
610
  at: z.number(),
577
611
  });
578
612
  const OwnedSchema = z.object({ paths: z.array(z.string()) });
package/dist/run.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { type MachineRunReport, type MachineWork } from '@craftspace/shared';
2
2
  export declare const runner: {
3
3
  attended(argv: string[]): Promise<number>;
4
- unattended({ work, onProgress, }: {
4
+ unattended({ work, cwd, onProgress, }: {
5
5
  work: MachineWork;
6
+ cwd?: string;
6
7
  onProgress?: (report: MachineRunReport) => void;
7
8
  }): Promise<MachineRunReport>;
8
9
  };
package/dist/run.js CHANGED
@@ -9,7 +9,7 @@ export const runner = {
9
9
  child.on('close', (code) => resolve(code ?? 0));
10
10
  });
11
11
  },
12
- async unattended({ work, onProgress, }) {
12
+ async unattended({ work, cwd = os.homedir(), onProgress, }) {
13
13
  const argv = headlessArgv(work);
14
14
  const raw = argv[0] === 'script';
15
15
  let output = '';
@@ -37,7 +37,7 @@ export const runner = {
37
37
  const code = await new Promise((resolve) => {
38
38
  const child = spawn(argv[0], argv.slice(1), {
39
39
  stdio: ['ignore', 'pipe', 'pipe'],
40
- cwd: os.homedir(),
40
+ cwd,
41
41
  env: {
42
42
  ...process.env,
43
43
  TERM: process.env.TERM ?? 'xterm-256color',
package/dist/setup.d.ts CHANGED
@@ -1,6 +1,11 @@
1
- import { type WorkstationStep, type WorkstationStepResult } from '@craftspace/shared';
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, }: {
6
+ repos: WorkstationRepo[];
7
+ say?: (line: string) => void;
8
+ }): Promise<MachineRepo[]>;
4
9
  runSteps({ steps, say, }: {
5
10
  steps: WorkstationStep[];
6
11
  say?: (line: string) => void;
package/dist/setup.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { execFile } from 'node:child_process';
2
+ import { mkdir } from 'node:fs/promises';
2
3
  import os from 'node:os';
3
4
  import path from 'node:path';
4
5
  import { WORKSTATION_STEP_DETAIL_MAX_CHARS, } from '@craftspace/shared';
@@ -10,6 +11,24 @@ export const setup = {
10
11
  if (missing.length > 0)
11
12
  process.env.PATH = [...current, ...missing].join(path.delimiter);
12
13
  },
14
+ workDir() {
15
+ return path.join(os.homedir(), WORK_DIR_NAME);
16
+ },
17
+ async cloneRepos({ repos, say, }) {
18
+ if (repos.length === 0)
19
+ return [];
20
+ await mkdir(setup.workDir(), { recursive: true });
21
+ const cloned = [];
22
+ for (const [index, repo] of repos.entries()) {
23
+ say?.(ui.progress({ name: repo.name, done: index, total: repos.length }));
24
+ const at = path.join(setup.workDir(), repo.name.split('/').pop() ?? repo.name);
25
+ const outcome = await checkout(repo, at);
26
+ cloned.push({ name: repo.name, ref: repo.ref, path: at, ...outcome });
27
+ say?.(`${ui.clear()}${outcome.ok ? ui.ok(`${repo.name} · ${outcome.detail}`) : ui.bad(repo.name, outcome.detail)}`);
28
+ }
29
+ say?.(ui.tally({ done: cloned.filter((repo) => repo.ok).length, total: cloned.length, noun: 'cloned' }));
30
+ return cloned;
31
+ },
13
32
  async runSteps({ steps, say, }) {
14
33
  const results = [];
15
34
  for (const [index, step] of steps.entries()) {
@@ -25,6 +44,44 @@ export const setup = {
25
44
  return results;
26
45
  },
27
46
  };
47
+ async function checkout(repo, at) {
48
+ const opened = await git(['-C', at, 'rev-parse', '--abbrev-ref', 'HEAD']);
49
+ if (!opened.ok) {
50
+ const url = `https://github.com/${repo.name}.git`;
51
+ const made = await git(['clone', '--depth', String(CLONE_DEPTH), '--branch', repo.ref, url, at]);
52
+ if (!made.ok)
53
+ return { ok: false, detail: made.detail };
54
+ return { ok: true, detail: await state({ repo, at, branch: repo.ref, dirty: false }) };
55
+ }
56
+ const branch = opened.detail;
57
+ const dirty = (await git(['-C', at, 'status', '--porcelain'])).detail !== '';
58
+ const pulled = branch === repo.ref && !dirty ? await git(['-C', at, 'pull', '--ff-only']) : null;
59
+ return { ok: true, detail: await state({ repo, at, branch, dirty, pulled }) };
60
+ }
61
+ async function state({ repo, at, branch, dirty, pulled, }) {
62
+ const head = await git(['-C', at, 'rev-parse', '--short', 'HEAD']);
63
+ return [
64
+ `${branch} @ ${head.detail}`,
65
+ branch === repo.ref ? '' : `your team asks for ${repo.ref}`,
66
+ dirty ? 'uncommitted work here, so it was left alone' : '',
67
+ pulled?.ok === false ? `could not pull: ${pulled.detail}` : '',
68
+ ]
69
+ .filter((part) => part !== '')
70
+ .join(' · ');
71
+ }
72
+ function git(argv) {
73
+ return new Promise((resolve) => {
74
+ execFile('git', argv, {
75
+ timeout: CLONE_TIMEOUT_MS,
76
+ maxBuffer: SCRIPT_BUFFER_BYTES,
77
+ env: { ...process.env, GIT_TERMINAL_PROMPT: '0', GIT_ASKPASS: '', GIT_LFS_SKIP_SMUDGE: '1' },
78
+ }, (error, stdout, stderr) => {
79
+ if (error === null)
80
+ return resolve({ ok: true, detail: stdout.trim() });
81
+ resolve({ ok: false, detail: lastLines(`${stderr}\n${stdout}`) || error.message.split('\n')[0] || 'git failed' });
82
+ });
83
+ });
84
+ }
28
85
  function runScript(script) {
29
86
  return new Promise((resolve) => {
30
87
  execFile('/bin/sh', ['-e', '-c', script], { timeout: STEP_TIMEOUT_MS, maxBuffer: SCRIPT_BUFFER_BYTES }, (error, stdout, stderr) => {
@@ -54,6 +111,9 @@ function toolDirs() {
54
111
  '/usr/local/bin',
55
112
  ];
56
113
  }
114
+ const WORK_DIR_NAME = 'craftspace';
115
+ const CLONE_DEPTH = 20;
116
+ const CLONE_TIMEOUT_MS = 900_000;
57
117
  const STEP_TIMEOUT_MS = 600_000;
58
118
  const SCRIPT_BUFFER_BYTES = 4 * 1024 * 1024;
59
119
  const KEPT_LINES = 3;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@craftspace/cli",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "description": "Sign a Mac or Linux machine into Craftspace and keep its agent setup in step.",
6
6
  "license": "MIT",