@craftspace/cli 0.5.0 → 0.7.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.7.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,10 +23241,14 @@ ${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
23116
23249
  });
23250
+ if (answer.cli !== null && isNewer(answer.cli.version))
23251
+ updateWanted = answer.cli.version;
23117
23252
  await writeAuthorizedKeys(answer.authorizedKeys);
23118
23253
  const building2 = buildEnvironment(answer.environment, say);
23119
23254
  if (say === void 0)
@@ -23124,7 +23259,7 @@ ${ui.ok(`Signed in as ${signed.ownerName}. This machine is ${ui.name(signed.name
23124
23259
  if (running.has(work.id))
23125
23260
  continue;
23126
23261
  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));
23262
+ 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
23263
  }
23129
23264
  return running.size > 0 || answer.work.length > 0;
23130
23265
  },
@@ -23133,8 +23268,10 @@ ${ui.ok(`Signed in as ${signed.ownerName}. This machine is ${ui.name(signed.name
23133
23268
  let busy = false;
23134
23269
  let checkedAt = 0;
23135
23270
  for (; ; ) {
23136
- if (!busy && Date.now() - checkedAt > UPDATE_EVERY_MS) {
23271
+ const asked = updateWanted !== null && Date.now() - checkedAt > UPDATE_AFTER_BEAT_MS;
23272
+ if (!busy && (asked || failures > 2 || Date.now() - checkedAt > UPDATE_EVERY_MS)) {
23137
23273
  checkedAt = Date.now();
23274
+ updateWanted = null;
23138
23275
  const moved = await update.toLatest();
23139
23276
  if (moved !== null) {
23140
23277
  process.stderr.write(`craftspace ${CLI_VERSION} -> ${moved}, restarting
@@ -23260,17 +23397,40 @@ async function buildEnvironment(next, say) {
23260
23397
  if (building || !installs() || next.templateId === null)
23261
23398
  return;
23262
23399
  const built = await readEnvironment();
23263
- if (!buildIsDue(built, next.templateId))
23400
+ const buildsSteps = buildIsDue(built, next.templateId);
23401
+ const clonesRepos = cloneIsDue(built, next.repos);
23402
+ if (!buildsSteps && !clonesRepos)
23264
23403
  return;
23265
23404
  building = true;
23266
23405
  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() });
23406
+ let results = built?.results ?? [];
23407
+ let repos = built?.repos ?? [];
23408
+ if (buildsSteps) {
23409
+ say?.(ui.heading(`Building the ${next.steps.length} setup scripts your team runs on every box`));
23410
+ results = await setup.runSteps({ steps: next.steps, say });
23411
+ }
23412
+ if (clonesRepos) {
23413
+ say?.(ui.heading(`Cloning the ${next.repos.length} repos your team works in, into ${setup.workDir()}`));
23414
+ repos = await setup.cloneRepos({ repos: next.repos, say });
23415
+ await acceptClaudeGates(["craftspace"], repos.filter((repo) => repo.ok).map((repo) => repo.path));
23416
+ }
23417
+ await writeEnvironment({ templateId: next.templateId, results, repos, at: Date.now() });
23270
23418
  } finally {
23271
23419
  building = false;
23272
23420
  }
23273
23421
  }
23422
+ function cloneIsDue(built, wanted) {
23423
+ const cloned = built?.repos ?? [];
23424
+ if (wanted.length === 0 && cloned.length === 0)
23425
+ return false;
23426
+ if (wanted.length !== cloned.length)
23427
+ return true;
23428
+ const missing = wanted.some((repo) => !cloned.some((seen) => seen.name === repo.name && seen.ref === repo.ref && seen.ok));
23429
+ return missing || Date.now() - (built?.at ?? 0) > REBUILD_AFTER_MS;
23430
+ }
23431
+ function startIn(built) {
23432
+ return built?.repos.find((repo) => repo.ok)?.path ?? os5.homedir();
23433
+ }
23274
23434
  function buildIsDue(built, templateId) {
23275
23435
  if (built === null || built.templateId !== templateId)
23276
23436
  return true;
@@ -23286,7 +23446,7 @@ async function readEnvironment() {
23286
23446
  return parsed.success ? parsed.data : null;
23287
23447
  }
23288
23448
  async function writeEnvironment(built) {
23289
- await mkdir(machine.home(), { recursive: true, mode: 448 });
23449
+ await mkdir2(machine.home(), { recursive: true, mode: 448 });
23290
23450
  await writeFile(environmentPath(), JSON.stringify(built, null, 2), { mode: 384 });
23291
23451
  }
23292
23452
  function installs() {
@@ -23363,7 +23523,7 @@ async function ensureKey() {
23363
23523
  const existing = await readFile2(pub, "utf8").catch(() => null);
23364
23524
  if (existing !== null)
23365
23525
  return existing.trim();
23366
- await mkdir(machine.home(), { recursive: true, mode: 448 });
23526
+ await mkdir2(machine.home(), { recursive: true, mode: 448 });
23367
23527
  await run2("ssh-keygen", ["-t", "ed25519", "-N", "", "-q", "-f", priv, "-C", `craftspace-${os5.hostname()}`]);
23368
23528
  return (await readFile2(pub, "utf8")).trim();
23369
23529
  }
@@ -23403,7 +23563,7 @@ ${KEYS_END}
23403
23563
  const next = `${trimEnd(before)}${trimEnd(before) === "" ? "" : "\n"}${block}${after.replace(/^\n/, "")}`;
23404
23564
  if (next === current)
23405
23565
  return;
23406
- await mkdir(path4.dirname(target), { recursive: true, mode: 448 });
23566
+ await mkdir2(path4.dirname(target), { recursive: true, mode: 448 });
23407
23567
  await writeFile(target, next, { mode: 384 });
23408
23568
  }
23409
23569
  function trimEnd(text) {
@@ -23414,7 +23574,7 @@ async function writeAgentConfigs({ url: url2, token, writes }) {
23414
23574
  const owned = [];
23415
23575
  for (const write of planned) {
23416
23576
  const target = expand(write.path);
23417
- await mkdir(path4.dirname(target), { recursive: true });
23577
+ await mkdir2(path4.dirname(target), { recursive: true });
23418
23578
  const current = await readFile2(target, "utf8").catch(() => null);
23419
23579
  if (current !== null)
23420
23580
  await writeFile(`${target}.craftspace-backup`, current, { flag: "wx" }).catch(() => void 0);
@@ -23429,22 +23589,29 @@ async function writeAgentConfigs({ url: url2, token, writes }) {
23429
23589
  function serversIn(writes) {
23430
23590
  return writes.flatMap((write) => isPlainObject3(write.contents.mcpServers) ? Object.keys(write.contents.mcpServers) : []);
23431
23591
  }
23432
- async function acceptClaudeGates(servers) {
23592
+ async function acceptClaudeGates(servers, dirs = [os5.homedir()]) {
23433
23593
  const target = path4.join(os5.homedir(), ".claude.json");
23434
23594
  const raw = await readFile2(target, "utf8").catch(() => null);
23435
23595
  if (raw === null)
23436
23596
  return;
23437
23597
  const parsed = JSON.parse(raw);
23438
23598
  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)
23599
+ const opened = { ...projects };
23600
+ let moved = false;
23601
+ for (const dir of dirs) {
23602
+ const found = opened[dir];
23603
+ const here2 = isPlainObject3(found) ? { ...found } : {};
23604
+ const enabled = /* @__PURE__ */ new Set([...asArray(here2.enabledMcpjsonServers), ...servers]);
23605
+ if (here2.hasTrustDialogAccepted === true && enabled.size === asArray(here2.enabledMcpjsonServers).length)
23606
+ continue;
23607
+ here2.hasTrustDialogAccepted = true;
23608
+ here2.enabledMcpjsonServers = [...enabled];
23609
+ opened[dir] = here2;
23610
+ moved = true;
23611
+ }
23612
+ if (!moved)
23443
23613
  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)}
23614
+ await writeFile(target, `${JSON.stringify({ ...parsed, projects: opened }, null, 2)}
23448
23615
  `, { mode: 384 });
23449
23616
  }
23450
23617
  function asArray(value) {
@@ -23495,16 +23662,17 @@ async function installService() {
23495
23662
  return;
23496
23663
  if (process.platform === "linux") {
23497
23664
  const dir = path4.join(os5.homedir(), ".config", "systemd", "user");
23498
- await mkdir(dir, { recursive: true });
23665
+ await mkdir2(dir, { recursive: true });
23499
23666
  await writeFile(path4.join(dir, `${SERVICE_NAME}.service`), systemdUnit());
23500
23667
  await run2("loginctl", ["enable-linger", os5.userInfo().username]).catch(() => void 0);
23501
23668
  await run2("systemctl", ["--user", "daemon-reload"]).catch(() => void 0);
23502
- await run2("systemctl", ["--user", "enable", "--now", SERVICE_NAME]).catch(() => void 0);
23669
+ await run2("systemctl", ["--user", "enable", SERVICE_NAME]).catch(() => void 0);
23670
+ await run2("systemctl", ["--user", "restart", SERVICE_NAME]).catch(() => void 0);
23503
23671
  return;
23504
23672
  }
23505
23673
  if (process.platform === "darwin") {
23506
23674
  const target = path4.join(os5.homedir(), "Library", "LaunchAgents", `${LAUNCH_LABEL}.plist`);
23507
- await mkdir(path4.dirname(target), { recursive: true });
23675
+ await mkdir2(path4.dirname(target), { recursive: true });
23508
23676
  await writeFile(target, launchdPlist());
23509
23677
  await run2("launchctl", ["unload", target]).catch(() => void 0);
23510
23678
  await run2("launchctl", ["load", target]).catch(() => void 0);
@@ -23600,6 +23768,7 @@ var ConfigSchema = external_exports.object({ url: external_exports.string(), mac
23600
23768
  var BuiltSchema = external_exports.object({
23601
23769
  templateId: external_exports.string(),
23602
23770
  results: external_exports.array(WorkstationStepResultSchema),
23771
+ repos: external_exports.array(MachineRepoSchema).default([]),
23603
23772
  at: external_exports.number()
23604
23773
  });
23605
23774
  var OwnedSchema = external_exports.object({ paths: external_exports.array(external_exports.string()) });
@@ -23611,6 +23780,7 @@ var MAX_BACKOFF_MS = 12e4;
23611
23780
  var KEYS_BEGIN = "# craftspace begin";
23612
23781
  var KEYS_END = "# craftspace end";
23613
23782
  var UPDATE_EVERY_MS = 6 * 60 * 60 * 1e3;
23783
+ var UPDATE_AFTER_BEAT_MS = 5 * 60 * 1e3;
23614
23784
  var BEATING = "beating every 15s, outbound only, and keeps this set up";
23615
23785
  var PORTABLE_TERMS = /* @__PURE__ */ new Set([
23616
23786
  "ansi",
@@ -23631,6 +23801,7 @@ var LINGER_LINE = `sudo loginctl enable-linger ${os5.userInfo().username}`;
23631
23801
  var DEFAULT_URL = process.env.CRAFTSPACE_URL ?? "https://craftspace.app";
23632
23802
  var PROBE_EVERY_MS = 3e5;
23633
23803
  var REBUILD_AFTER_MS = 216e5;
23804
+ var updateWanted = null;
23634
23805
  var probed = null;
23635
23806
  var building = false;
23636
23807
  var reports = /* @__PURE__ */ new Map();
package/dist/machine.js CHANGED
@@ -3,14 +3,14 @@ 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';
10
10
  import { runner } from './run.js';
11
11
  import { setup } from './setup.js';
12
12
  import { ui } from './ui.js';
13
- import { update } from './update.js';
13
+ import { isNewer, update } from './update.js';
14
14
  const run = promisify(execFile);
15
15
  export const machine = {
16
16
  home() {
@@ -89,10 +89,14 @@ 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,
95
97
  });
98
+ if (answer.cli !== null && isNewer(answer.cli.version))
99
+ updateWanted = answer.cli.version;
96
100
  await writeAuthorizedKeys(answer.authorizedKeys);
97
101
  const building = buildEnvironment(answer.environment, say);
98
102
  if (say === undefined)
@@ -104,7 +108,7 @@ export const machine = {
104
108
  continue;
105
109
  running.add(work.id);
106
110
  void runner
107
- .unattended({ work, onProgress: report })
111
+ .unattended({ work, cwd: startIn(built), onProgress: report })
108
112
  .then(report)
109
113
  .catch((error) => report({ id: work.id, state: 'failed', output: messageOf(error) }))
110
114
  .finally(() => running.delete(work.id));
@@ -116,8 +120,10 @@ export const machine = {
116
120
  let busy = false;
117
121
  let checkedAt = 0;
118
122
  for (;;) {
119
- if (!busy && Date.now() - checkedAt > UPDATE_EVERY_MS) {
123
+ const asked = updateWanted !== null && Date.now() - checkedAt > UPDATE_AFTER_BEAT_MS;
124
+ if (!busy && (asked || failures > 2 || Date.now() - checkedAt > UPDATE_EVERY_MS)) {
120
125
  checkedAt = Date.now();
126
+ updateWanted = null;
121
127
  const moved = await update.toLatest();
122
128
  if (moved !== null) {
123
129
  process.stderr.write(`craftspace ${CLI_VERSION} -> ${moved}, restarting\n`);
@@ -238,18 +244,41 @@ async function buildEnvironment(next, say) {
238
244
  if (building || !installs() || next.templateId === null)
239
245
  return;
240
246
  const built = await readEnvironment();
241
- if (!buildIsDue(built, next.templateId))
247
+ const buildsSteps = buildIsDue(built, next.templateId);
248
+ const clonesRepos = cloneIsDue(built, next.repos);
249
+ if (!buildsSteps && !clonesRepos)
242
250
  return;
243
251
  building = true;
244
252
  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() });
253
+ let results = built?.results ?? [];
254
+ let repos = built?.repos ?? [];
255
+ if (buildsSteps) {
256
+ say?.(ui.heading(`Building the ${next.steps.length} setup scripts your team runs on every box`));
257
+ results = await setup.runSteps({ steps: next.steps, say });
258
+ }
259
+ 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));
263
+ }
264
+ await writeEnvironment({ templateId: next.templateId, results, repos, at: Date.now() });
248
265
  }
249
266
  finally {
250
267
  building = false;
251
268
  }
252
269
  }
270
+ function cloneIsDue(built, wanted) {
271
+ const cloned = built?.repos ?? [];
272
+ if (wanted.length === 0 && cloned.length === 0)
273
+ return false;
274
+ if (wanted.length !== cloned.length)
275
+ return true;
276
+ const missing = wanted.some((repo) => !cloned.some((seen) => seen.name === repo.name && seen.ref === repo.ref && seen.ok));
277
+ return missing || Date.now() - (built?.at ?? 0) > REBUILD_AFTER_MS;
278
+ }
279
+ function startIn(built) {
280
+ return built?.repos.find((repo) => repo.ok)?.path ?? os.homedir();
281
+ }
253
282
  function buildIsDue(built, templateId) {
254
283
  if (built === null || built.templateId !== templateId)
255
284
  return true;
@@ -403,22 +432,29 @@ async function writeAgentConfigs({ url, token, writes, }) {
403
432
  function serversIn(writes) {
404
433
  return writes.flatMap((write) => isPlainObject(write.contents.mcpServers) ? Object.keys(write.contents.mcpServers) : []);
405
434
  }
406
- async function acceptClaudeGates(servers) {
435
+ async function acceptClaudeGates(servers, dirs = [os.homedir()]) {
407
436
  const target = path.join(os.homedir(), '.claude.json');
408
437
  const raw = await readFile(target, 'utf8').catch(() => null);
409
438
  if (raw === null)
410
439
  return;
411
440
  const parsed = JSON.parse(raw);
412
441
  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)
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)
417
456
  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 });
457
+ await writeFile(target, `${JSON.stringify({ ...parsed, projects: opened }, null, 2)}\n`, { mode: 0o600 });
422
458
  }
423
459
  function asArray(value) {
424
460
  return Array.isArray(value) ? value : [];
@@ -472,7 +508,8 @@ async function installService() {
472
508
  await writeFile(path.join(dir, `${SERVICE_NAME}.service`), systemdUnit());
473
509
  await run('loginctl', ['enable-linger', os.userInfo().username]).catch(() => undefined);
474
510
  await run('systemctl', ['--user', 'daemon-reload']).catch(() => undefined);
475
- await run('systemctl', ['--user', 'enable', '--now', SERVICE_NAME]).catch(() => undefined);
511
+ await run('systemctl', ['--user', 'enable', SERVICE_NAME]).catch(() => undefined);
512
+ await run('systemctl', ['--user', 'restart', SERVICE_NAME]).catch(() => undefined);
476
513
  return;
477
514
  }
478
515
  if (process.platform === 'darwin') {
@@ -573,6 +610,7 @@ const ConfigSchema = z.object({ url: z.string(), machineId: z.string() });
573
610
  const BuiltSchema = z.object({
574
611
  templateId: z.string(),
575
612
  results: z.array(WorkstationStepResultSchema),
613
+ repos: z.array(MachineRepoSchema).default([]),
576
614
  at: z.number(),
577
615
  });
578
616
  const OwnedSchema = z.object({ paths: z.array(z.string()) });
@@ -584,6 +622,7 @@ const MAX_BACKOFF_MS = 120_000;
584
622
  const KEYS_BEGIN = '# craftspace begin';
585
623
  const KEYS_END = '# craftspace end';
586
624
  const UPDATE_EVERY_MS = 6 * 60 * 60 * 1_000;
625
+ const UPDATE_AFTER_BEAT_MS = 5 * 60 * 1_000;
587
626
  const BEATING = 'beating every 15s, outbound only, and keeps this set up';
588
627
  const PORTABLE_TERMS = new Set([
589
628
  'ansi',
@@ -604,6 +643,7 @@ const LINGER_LINE = `sudo loginctl enable-linger ${os.userInfo().username}`;
604
643
  const DEFAULT_URL = process.env.CRAFTSPACE_URL ?? 'https://craftspace.app';
605
644
  const PROBE_EVERY_MS = 300_000;
606
645
  const REBUILD_AFTER_MS = 21_600_000;
646
+ let updateWanted = null;
607
647
  let probed = null;
608
648
  let building = false;
609
649
  const reports = new Map();
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.7.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",