@craftspace/cli 0.4.3 → 0.5.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
@@ -4632,8 +4632,8 @@ var BIGINT_FORMAT_RANGES = {
4632
4632
  };
4633
4633
  function pick(schema, mask) {
4634
4634
  const currDef = schema._zod.def;
4635
- const checks = currDef.checks;
4636
- const hasChecks = checks && checks.length > 0;
4635
+ const checks2 = currDef.checks;
4636
+ const hasChecks = checks2 && checks2.length > 0;
4637
4637
  if (hasChecks) {
4638
4638
  throw new Error(".pick() cannot be used on object schemas containing refinements");
4639
4639
  }
@@ -4657,8 +4657,8 @@ function pick(schema, mask) {
4657
4657
  }
4658
4658
  function omit(schema, mask) {
4659
4659
  const currDef = schema._zod.def;
4660
- const checks = currDef.checks;
4661
- const hasChecks = checks && checks.length > 0;
4660
+ const checks2 = currDef.checks;
4661
+ const hasChecks = checks2 && checks2.length > 0;
4662
4662
  if (hasChecks) {
4663
4663
  throw new Error(".omit() cannot be used on object schemas containing refinements");
4664
4664
  }
@@ -4684,8 +4684,8 @@ function extend(schema, shape) {
4684
4684
  if (!isPlainObject(shape)) {
4685
4685
  throw new Error("Invalid input to extend: expected a plain object");
4686
4686
  }
4687
- const checks = schema._zod.def.checks;
4688
- const hasChecks = checks && checks.length > 0;
4687
+ const checks2 = schema._zod.def.checks;
4688
+ const hasChecks = checks2 && checks2.length > 0;
4689
4689
  if (hasChecks) {
4690
4690
  const existingShape = schema._zod.def.shape;
4691
4691
  for (const key in shape) {
@@ -4735,8 +4735,8 @@ function merge(a, b) {
4735
4735
  }
4736
4736
  function partial(Class2, schema, mask) {
4737
4737
  const currDef = schema._zod.def;
4738
- const checks = currDef.checks;
4739
- const hasChecks = checks && checks.length > 0;
4738
+ const checks2 = currDef.checks;
4739
+ const hasChecks = checks2 && checks2.length > 0;
4740
4740
  if (hasChecks) {
4741
4741
  throw new Error(".partial() cannot be used on object schemas containing refinements");
4742
4742
  }
@@ -5238,10 +5238,10 @@ var nanoid = /^[a-zA-Z0-9_-]{21}$/;
5238
5238
  var duration = /^P(?:(\d+W)|(?!.*W)(?=\d|T\d)(\d+Y)?(\d+M)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+([.,]\d+)?S)?)?)$/;
5239
5239
  var extendedDuration = /^[-+]?P(?!$)(?:(?:[-+]?\d+Y)|(?:[-+]?\d+[.,]\d+Y$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:(?:[-+]?\d+W)|(?:[-+]?\d+[.,]\d+W$))?(?:(?:[-+]?\d+D)|(?:[-+]?\d+[.,]\d+D$))?(?:T(?=[\d+-])(?:(?:[-+]?\d+H)|(?:[-+]?\d+[.,]\d+H$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:[-+]?\d+(?:[.,]\d+)?S)?)??$/;
5240
5240
  var guid = /^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$/;
5241
- var uuid = (version3) => {
5242
- if (!version3)
5241
+ var uuid = (version2) => {
5242
+ if (!version2)
5243
5243
  return /^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$/;
5244
- return new RegExp(`^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-${version3}[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$`);
5244
+ return new RegExp(`^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-${version2}[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$`);
5245
5245
  };
5246
5246
  var uuid4 = /* @__PURE__ */ uuid(4);
5247
5247
  var uuid6 = /* @__PURE__ */ uuid(6);
@@ -5923,25 +5923,25 @@ var $ZodType = /* @__PURE__ */ $constructor("$ZodType", (inst, def) => {
5923
5923
  inst._zod.def = def;
5924
5924
  inst._zod.bag = inst._zod.bag || {};
5925
5925
  inst._zod.version = version;
5926
- const checks = [...inst._zod.def.checks ?? []];
5926
+ const checks2 = [...inst._zod.def.checks ?? []];
5927
5927
  if (inst._zod.traits.has("$ZodCheck")) {
5928
- checks.unshift(inst);
5928
+ checks2.unshift(inst);
5929
5929
  }
5930
- for (const ch of checks) {
5930
+ for (const ch of checks2) {
5931
5931
  for (const fn of ch._zod.onattach) {
5932
5932
  fn(inst);
5933
5933
  }
5934
5934
  }
5935
- if (checks.length === 0) {
5935
+ if (checks2.length === 0) {
5936
5936
  (_a3 = inst._zod).deferred ?? (_a3.deferred = []);
5937
5937
  inst._zod.deferred?.push(() => {
5938
5938
  inst._zod.run = inst._zod.parse;
5939
5939
  });
5940
5940
  } else {
5941
- const runChecks = (payload, checks2, ctx) => {
5941
+ const runChecks = (payload, checks3, ctx) => {
5942
5942
  let isAborted = aborted(payload);
5943
5943
  let asyncResult;
5944
- for (const ch of checks2) {
5944
+ for (const ch of checks3) {
5945
5945
  if (ch._zod.def.when) {
5946
5946
  if (explicitlyAborted(payload))
5947
5947
  continue;
@@ -5985,7 +5985,7 @@ var $ZodType = /* @__PURE__ */ $constructor("$ZodType", (inst, def) => {
5985
5985
  canary.aborted = true;
5986
5986
  return canary;
5987
5987
  }
5988
- const checkResult = runChecks(payload, checks, ctx);
5988
+ const checkResult = runChecks(payload, checks2, ctx);
5989
5989
  if (checkResult instanceof Promise) {
5990
5990
  if (ctx.async === false)
5991
5991
  throw new $ZodAsyncError();
@@ -6010,9 +6010,9 @@ var $ZodType = /* @__PURE__ */ $constructor("$ZodType", (inst, def) => {
6010
6010
  if (result instanceof Promise) {
6011
6011
  if (ctx.async === false)
6012
6012
  throw new $ZodAsyncError();
6013
- return result.then((result2) => runChecks(result2, checks, ctx));
6013
+ return result.then((result2) => runChecks(result2, checks2, ctx));
6014
6014
  }
6015
- return runChecks(result, checks, ctx);
6015
+ return runChecks(result, checks2, ctx);
6016
6016
  };
6017
6017
  }
6018
6018
  defineLazy(inst, "~standard", () => ({
@@ -18117,10 +18117,10 @@ function fromJSONSchema(schema, params) {
18117
18117
  } catch {
18118
18118
  throw new Error("fromJSONSchema input is not valid JSON (possibly cyclic); use $defs/$ref for recursive schemas");
18119
18119
  }
18120
- const version3 = detectVersion(normalized, params?.defaultTarget);
18120
+ const version2 = detectVersion(normalized, params?.defaultTarget);
18121
18121
  const defs = normalized.$defs || normalized.definitions || {};
18122
18122
  const ctx = {
18123
- version: version3,
18123
+ version: version2,
18124
18124
  defs,
18125
18125
  refs: /* @__PURE__ */ new Map(),
18126
18126
  processing: /* @__PURE__ */ new Set(),
@@ -18710,6 +18710,16 @@ var GUS_MODELS = [
18710
18710
  reasoning: true,
18711
18711
  harness: GusHarness.OpenCode
18712
18712
  },
18713
+ {
18714
+ id: "deepseek/deepseek-v4.1-flash",
18715
+ label: "DeepSeek V4.1 Flash",
18716
+ blurb: "The V4 Flash refresh. Same pennies, steadier across a long tool run.",
18717
+ inputUsdPerMillion: 0.08,
18718
+ outputUsdPerMillion: 0.18,
18719
+ contextWindow: 1048576,
18720
+ reasoning: true,
18721
+ harness: GusHarness.OpenCode
18722
+ },
18713
18723
  {
18714
18724
  id: "deepseek/deepseek-v4-pro-0813",
18715
18725
  label: "DeepSeek V4 Pro",
@@ -19310,7 +19320,8 @@ var OAuthCallbackQuerySchema = external_exports.object({
19310
19320
  });
19311
19321
  var ListConnectionsResponseSchema = external_exports.object({
19312
19322
  connections: external_exports.array(ConnectionSchema),
19313
- canManage: external_exports.boolean()
19323
+ canManage: external_exports.boolean(),
19324
+ egressIp: external_exports.string()
19314
19325
  });
19315
19326
  var CONNECTION_CATEGORIES = [
19316
19327
  "Sales & support",
@@ -19751,6 +19762,10 @@ var GUS_MAX_TURN_MS = 45 * 60 * 1e3;
19751
19762
  // ../../shared/dist/gus/workstation.js
19752
19763
  var WORKSTATION_CORES = 2;
19753
19764
  var WORKSTATION_RECIPE_MAX_CHARS = 8e3;
19765
+ var WORKSTATION_MAX_STEPS = 20;
19766
+ var WORKSTATION_STEP_NAME_MAX_CHARS = 60;
19767
+ var WORKSTATION_STEP_DETAIL_MAX_CHARS = 600;
19768
+ var WORKSTATION_MAX_REPOS = 20;
19754
19769
  var WORKSTATION_ALLOWLIST_MAX_HOSTS = 50;
19755
19770
  var WORKSTATION_HOST_MAX_CHARS = 200;
19756
19771
  var SANDBOX_BROWSERS_PATH = "/vercel/sandbox/.cache/ms-playwright";
@@ -19762,14 +19777,53 @@ var WORKSTATION_SPECS = {
19762
19777
  };
19763
19778
  var PLAYWRIGHT_VERSION = "1.49.1";
19764
19779
  var WORKSTATION_BASE_RECIPE = [
19765
- "# Craftspace base v2: a headless browser, ready to use.",
19780
+ "# Craftspace base v3: a headless browser, ready to use.",
19781
+ `export PLAYWRIGHT_BROWSERS_PATH=${SANDBOX_BROWSERS_PATH}`,
19766
19782
  "sudo dnf install -y nss nspr atk at-spi2-atk at-spi2-core cups-libs libdrm libxkbcommon \\",
19767
19783
  " libXcomposite libXdamage libXrandr libXfixes libXrender mesa-libgbm pango alsa-lib >/dev/null",
19768
- `PLAYWRIGHT_BROWSERS_PATH=${SANDBOX_BROWSERS_PATH} npx --yes playwright@${PLAYWRIGHT_VERSION} install chromium`
19784
+ `npx --yes playwright@${PLAYWRIGHT_VERSION} install chromium`
19769
19785
  ].join("\n");
19770
19786
  var HostSchema = external_exports.string().trim().min(1).max(WORKSTATION_HOST_MAX_CHARS).regex(/^[A-Za-z0-9*._:-]+$/, "A host is a domain, optionally with a leading *. and a :port.");
19787
+ var WorkstationStepSchema = external_exports.object({
19788
+ id: external_exports.string().min(1).max(64),
19789
+ name: external_exports.string().trim().min(1).max(WORKSTATION_STEP_NAME_MAX_CHARS),
19790
+ script: external_exports.string().max(WORKSTATION_RECIPE_MAX_CHARS)
19791
+ });
19792
+ var WorkstationRepoSchema = external_exports.object({
19793
+ name: external_exports.string().trim().min(3).max(200).regex(/^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/, "A repo is owner/name."),
19794
+ ref: external_exports.string().trim().min(1).max(200)
19795
+ });
19796
+ var WORKSTATION_SEED_STEPS = [
19797
+ { id: "step_claude_code", name: "Claude Code", script: "npm install -g @anthropic-ai/claude-code" },
19798
+ { id: "step_codex", name: "Codex", script: "npm install -g @openai/codex" },
19799
+ {
19800
+ id: "step_github_cli",
19801
+ name: "GitHub CLI",
19802
+ script: [
19803
+ "if command -v gh >/dev/null; then exit 0; fi",
19804
+ "if command -v brew >/dev/null; then brew install gh",
19805
+ "elif command -v dnf >/dev/null; then sudo -n dnf install -y gh",
19806
+ "elif command -v apt-get >/dev/null; then sudo -n apt-get install -y gh",
19807
+ 'else echo "no package manager here, install the GitHub CLI by hand" >&2; exit 1',
19808
+ "fi"
19809
+ ].join("\n")
19810
+ },
19811
+ {
19812
+ id: "step_chrome",
19813
+ name: "Chrome for Playwright",
19814
+ script: `npx --yes playwright@${PLAYWRIGHT_VERSION} install chromium`
19815
+ }
19816
+ ];
19817
+ var WorkstationStepResultSchema = external_exports.object({
19818
+ id: external_exports.string().min(1).max(64),
19819
+ name: external_exports.string().max(WORKSTATION_STEP_NAME_MAX_CHARS),
19820
+ ok: external_exports.boolean(),
19821
+ detail: external_exports.string().max(WORKSTATION_STEP_DETAIL_MAX_CHARS),
19822
+ ms: external_exports.number().int().nonnegative()
19823
+ });
19771
19824
  var WorkstationSchema = external_exports.object({
19772
- recipe: external_exports.string(),
19825
+ steps: external_exports.array(WorkstationStepSchema),
19826
+ repos: external_exports.array(WorkstationRepoSchema),
19773
19827
  allowlist: external_exports.array(external_exports.string()),
19774
19828
  templateId: external_exports.string().nullable(),
19775
19829
  publicKey: external_exports.string(),
@@ -19777,13 +19831,14 @@ var WorkstationSchema = external_exports.object({
19777
19831
  updatedByName: external_exports.string().nullable()
19778
19832
  });
19779
19833
  var SaveWorkstationBodySchema = external_exports.object({
19780
- recipe: external_exports.string().max(WORKSTATION_RECIPE_MAX_CHARS),
19834
+ steps: external_exports.array(WorkstationStepSchema).max(WORKSTATION_MAX_STEPS),
19835
+ repos: external_exports.array(WorkstationRepoSchema).max(WORKSTATION_MAX_REPOS),
19781
19836
  allowlist: external_exports.array(HostSchema).max(WORKSTATION_ALLOWLIST_MAX_HOSTS)
19782
19837
  });
19783
19838
  var WorkstationResponseSchema = external_exports.object({ workstation: WorkstationSchema });
19784
19839
 
19785
19840
  // ../../shared/dist/machines/machine.js
19786
- var CLI_VERSION = "0.4.3";
19841
+ var CLI_VERSION = "0.5.0";
19787
19842
  var CLI_PACKAGE = "@craftspace/cli";
19788
19843
  var CLI_INSTALL = `npm i -g ${CLI_PACKAGE}`;
19789
19844
  var MACHINE_BEAT_INTERVAL_MS = 15e3;
@@ -19804,7 +19859,7 @@ var MachineAuthorizedKeySchema = external_exports.object({
19804
19859
  addedByName: external_exports.string(),
19805
19860
  addedAt: external_exports.coerce.date()
19806
19861
  });
19807
- var MACHINE_TOOLS = ["brain", "claude", "codex", "git", "chrome", "ssh"];
19862
+ var MACHINE_TOOLS = ["brain", "ssh"];
19808
19863
  var MachineToolIdSchema = external_exports.enum(MACHINE_TOOLS);
19809
19864
  var MachineToolSchema = external_exports.object({
19810
19865
  id: MachineToolIdSchema,
@@ -19832,6 +19887,8 @@ var MachineSchema = external_exports.object({
19832
19887
  state: MachineStateSchema,
19833
19888
  sessions: external_exports.array(MachineSessionSchema),
19834
19889
  tools: external_exports.array(MachineToolSchema),
19890
+ environment: external_exports.array(WorkstationStepResultSchema),
19891
+ templateId: external_exports.string().nullable(),
19835
19892
  authorizedKeys: external_exports.array(MachineAuthorizedKeySchema),
19836
19893
  lastBeatAt: external_exports.coerce.date().nullable(),
19837
19894
  lastAskedAt: external_exports.coerce.date().nullable(),
@@ -19894,12 +19951,19 @@ var MachineBeatBodySchema = external_exports.object({
19894
19951
  sessions: external_exports.array(MachineSessionSchema).max(50),
19895
19952
  runs: external_exports.array(MachineRunReportSchema).max(20).default([]),
19896
19953
  tools: external_exports.array(MachineToolSchema).max(10).default([]),
19954
+ environment: external_exports.array(WorkstationStepResultSchema).max(WORKSTATION_MAX_STEPS).default([]),
19955
+ templateId: external_exports.string().max(64).nullable().default(null),
19897
19956
  sshUser: external_exports.string().min(1).max(64).optional()
19898
19957
  });
19958
+ var MachineEnvironmentSchema = external_exports.object({
19959
+ templateId: external_exports.string().nullable(),
19960
+ steps: external_exports.array(WorkstationStepSchema)
19961
+ });
19899
19962
  var MachineBeatResponseSchema = external_exports.object({
19900
19963
  cli: external_exports.object({ version: external_exports.string(), url: external_exports.string(), sha256: external_exports.string() }).nullable(),
19901
19964
  work: external_exports.array(MachineWorkSchema),
19902
- authorizedKeys: external_exports.array(external_exports.string())
19965
+ authorizedKeys: external_exports.array(external_exports.string()),
19966
+ environment: MachineEnvironmentSchema
19903
19967
  });
19904
19968
  var AddMachineKeyBodySchema = external_exports.object({
19905
19969
  key: external_exports.string().min(60).max(4e3),
@@ -20251,11 +20315,11 @@ var MemberActivityListResponse = external_exports.object({
20251
20315
  });
20252
20316
 
20253
20317
  // dist/machine.js
20254
- import { execFile as execFile4, spawn as spawn2 } from "node:child_process";
20318
+ import { execFile as execFile3, spawn as spawn2 } from "node:child_process";
20255
20319
  import { mkdir, readFile as readFile2, readdir, rm, writeFile } from "node:fs/promises";
20256
20320
  import os5 from "node:os";
20257
20321
  import path4 from "node:path";
20258
- import { promisify as promisify4 } from "node:util";
20322
+ import { promisify as promisify2 } from "node:util";
20259
20323
 
20260
20324
  // ../../../node_modules/@inquirer/core/dist/lib/key.js
20261
20325
  var keybindings = ["emacs", "vim"];
@@ -22121,9 +22185,50 @@ var dist_default6 = createPrompt((config2, done) => {
22121
22185
  return `${lines}${cursorHide}`;
22122
22186
  });
22123
22187
 
22188
+ // dist/probe.js
22189
+ import net from "node:net";
22190
+ import { readFile } from "node:fs/promises";
22191
+ import os from "node:os";
22192
+ import path2 from "node:path";
22193
+ var probe = {
22194
+ async tools({ mcpPath = path2.join(os.homedir(), ".mcp.json") } = {}) {
22195
+ return Promise.all([brainWired(mcpPath), sshListening()]);
22196
+ }
22197
+ };
22198
+ async function brainWired(mcpPath) {
22199
+ const raw = await readFile(mcpPath, "utf8").catch(() => null);
22200
+ if (raw === null)
22201
+ return { id: "brain", ok: false, detail: `no ${short(mcpPath)} on this machine` };
22202
+ const parsed = JSON.parse(raw);
22203
+ const servers = parsed?.mcpServers;
22204
+ const wired = servers !== void 0 && servers !== null && "craftspace" in servers;
22205
+ return {
22206
+ id: "brain",
22207
+ ok: wired,
22208
+ detail: wired ? short(mcpPath) : `${short(mcpPath)} has no craftspace entry`
22209
+ };
22210
+ }
22211
+ function sshListening() {
22212
+ return new Promise((resolve) => {
22213
+ const socket = net.connect({ host: "127.0.0.1", port: 22 });
22214
+ const answer = (ok, detail) => {
22215
+ socket.destroy();
22216
+ resolve({ id: "ssh", ok, detail });
22217
+ };
22218
+ socket.setTimeout(SSH_PROBE_TIMEOUT_MS);
22219
+ socket.on("connect", () => answer(true, "sshd is listening on port 22"));
22220
+ socket.on("timeout", () => answer(false, "nothing answered on port 22"));
22221
+ socket.on("error", () => answer(false, "sshd is not running, so nobody can open a terminal here"));
22222
+ });
22223
+ }
22224
+ function short(target) {
22225
+ return target.startsWith(os.homedir()) ? `~${target.slice(os.homedir().length)}` : target;
22226
+ }
22227
+ var SSH_PROBE_TIMEOUT_MS = 1500;
22228
+
22124
22229
  // dist/run.js
22125
22230
  import { spawn } from "node:child_process";
22126
- import os from "node:os";
22231
+ import os2 from "node:os";
22127
22232
  var runner = {
22128
22233
  attended(argv) {
22129
22234
  return new Promise((resolve) => {
@@ -22162,7 +22267,7 @@ var runner = {
22162
22267
  const code = await new Promise((resolve) => {
22163
22268
  const child = spawn(argv[0], argv.slice(1), {
22164
22269
  stdio: ["ignore", "pipe", "pipe"],
22165
- cwd: os.homedir(),
22270
+ cwd: os2.homedir(),
22166
22271
  env: {
22167
22272
  ...process.env,
22168
22273
  TERM: process.env.TERM ?? "xterm-256color",
@@ -22259,112 +22364,9 @@ function parseJson(line) {
22259
22364
  }
22260
22365
 
22261
22366
  // dist/setup.js
22262
- import { execFile as execFile2 } from "node:child_process";
22367
+ import { execFile } from "node:child_process";
22263
22368
  import os4 from "node:os";
22264
22369
  import path3 from "node:path";
22265
- import { promisify as promisify2 } from "node:util";
22266
-
22267
- // dist/probe.js
22268
- import { execFile } from "node:child_process";
22269
- import net from "node:net";
22270
- import { access, readFile } from "node:fs/promises";
22271
- import os2 from "node:os";
22272
- import path2 from "node:path";
22273
- import { promisify } from "node:util";
22274
- var run = promisify(execFile);
22275
- var probe = {
22276
- async tools({ mcpPath = path2.join(os2.homedir(), ".mcp.json") } = {}) {
22277
- const [brain, claude, codex, git, chrome, ssh] = await Promise.all([
22278
- brainWired(mcpPath),
22279
- version2({ id: "claude", command: "claude" }),
22280
- version2({ id: "codex", command: "codex" }),
22281
- gitAndGithub(),
22282
- chromeInstalled(),
22283
- sshListening()
22284
- ]);
22285
- return [brain, claude, codex, git, chrome, ssh];
22286
- }
22287
- };
22288
- async function brainWired(mcpPath) {
22289
- const raw = await readFile(mcpPath, "utf8").catch(() => null);
22290
- if (raw === null)
22291
- return { id: "brain", ok: false, detail: `no ${short(mcpPath)} on this machine` };
22292
- const parsed = JSON.parse(raw);
22293
- const servers = parsed?.mcpServers;
22294
- const wired = servers !== void 0 && servers !== null && "craftspace" in servers;
22295
- return {
22296
- id: "brain",
22297
- ok: wired,
22298
- detail: wired ? short(mcpPath) : `${short(mcpPath)} has no craftspace entry`
22299
- };
22300
- }
22301
- async function version2({ id, command }) {
22302
- const found = await firstLine(command, ["--version"]);
22303
- if (found.ran)
22304
- return { id, ok: true, detail: found.line };
22305
- return { id, ok: false, detail: found.why === "absent" ? "not installed" : "installed, but it will not report a version" };
22306
- }
22307
- async function gitAndGithub() {
22308
- const git = await firstLine("git", ["--version"]);
22309
- if (!git.ran)
22310
- return { id: "git", ok: false, detail: "git is not installed" };
22311
- const gh = await firstLine("gh", ["--version"]);
22312
- if (!gh.ran)
22313
- return { id: "git", ok: false, detail: `${git.line} \xB7 the GitHub CLI is not installed yet` };
22314
- const account = await githubAccount();
22315
- return {
22316
- id: "git",
22317
- ok: account !== null,
22318
- detail: account === null ? `${git.line} \xB7 ${gh.line}, run gh auth login on this machine` : `${git.line} \xB7 ${gh.line}, signed in as ${account}`
22319
- };
22320
- }
22321
- async function githubAccount() {
22322
- const raw = await readFile(path2.join(os2.homedir(), ".config", "gh", "hosts.yml"), "utf8").catch(() => null);
22323
- return raw === null ? null : githubUser(raw);
22324
- }
22325
- function githubUser(hostsYml) {
22326
- return /^ {4}user: *(\S+) *$/m.exec(hostsYml)?.[1] ?? null;
22327
- }
22328
- async function chromeInstalled() {
22329
- for (const command of [...MAC_CHROME, ...LINUX_CHROME]) {
22330
- const found = await firstLine(command, ["--version"]);
22331
- if (found.ran)
22332
- return { id: "chrome", ok: true, detail: found.line };
22333
- }
22334
- return { id: "chrome", ok: false, detail: "not installed yet, so browser runs will fail" };
22335
- }
22336
- function sshListening() {
22337
- return new Promise((resolve) => {
22338
- const socket = net.connect({ host: "127.0.0.1", port: 22 });
22339
- const answer = (ok, detail) => {
22340
- socket.destroy();
22341
- resolve({ id: "ssh", ok, detail });
22342
- };
22343
- socket.setTimeout(SSH_PROBE_TIMEOUT_MS);
22344
- socket.on("connect", () => answer(true, "sshd is listening on port 22"));
22345
- socket.on("timeout", () => answer(false, "nothing answered on port 22"));
22346
- socket.on("error", () => answer(false, "sshd is not running, so nobody can ssh in"));
22347
- });
22348
- }
22349
- async function firstLine(command, args) {
22350
- if (command.includes("/")) {
22351
- const reachable = await access(command).then(() => true, () => false);
22352
- if (!reachable)
22353
- return { ran: false, why: "absent" };
22354
- }
22355
- const answer = await run(command, args, { timeout: PROBE_TIMEOUT_MS }).catch((thrown) => thrown);
22356
- if (answer instanceof Error)
22357
- return { ran: false, why: answer.code === "ENOENT" ? "absent" : "broken" };
22358
- const line = answer.stdout.split("\n")[0]?.trim() ?? "";
22359
- return line === "" ? { ran: false, why: "broken" } : { ran: true, line: line.slice(0, 200) };
22360
- }
22361
- function short(target) {
22362
- return target.startsWith(os2.homedir()) ? `~${target.slice(os2.homedir().length)}` : target;
22363
- }
22364
- var MAC_CHROME = ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"];
22365
- var LINUX_CHROME = ["google-chrome", "google-chrome-stable", "chromium"];
22366
- var PROBE_TIMEOUT_MS = 5e3;
22367
- var SSH_PROBE_TIMEOUT_MS = 1500;
22368
22370
 
22369
22371
  // node_modules/chalk/source/vendor/ansi-styles/index.js
22370
22372
  var ANSI_BACKGROUND_OFFSET = 10;
@@ -22650,10 +22652,10 @@ function _supportsColor(haveStream, { streamIsTTY, sniffFlags = true } = {}) {
22650
22652
  return 3;
22651
22653
  }
22652
22654
  if ("TERM_PROGRAM" in env) {
22653
- const version3 = Number.parseInt((env.TERM_PROGRAM_VERSION || "").split(".")[0], 10);
22655
+ const version2 = Number.parseInt((env.TERM_PROGRAM_VERSION || "").split(".")[0], 10);
22654
22656
  switch (env.TERM_PROGRAM) {
22655
22657
  case "iTerm.app": {
22656
- return version3 >= 3 ? 3 : 2;
22658
+ return version2 >= 3 ? 3 : 2;
22657
22659
  }
22658
22660
  case "Apple_Terminal": {
22659
22661
  return 2;
@@ -22929,7 +22931,6 @@ var LABEL_WIDTH = 13;
22929
22931
  var BAR_CELLS = 10;
22930
22932
 
22931
22933
  // dist/setup.js
22932
- var run2 = promisify2(execFile2);
22933
22934
  var setup = {
22934
22935
  widenPath() {
22935
22936
  const current = (process.env.PATH ?? "").split(path3.delimiter).filter((entry) => entry !== "");
@@ -22937,44 +22938,34 @@ var setup = {
22937
22938
  if (missing.length > 0)
22938
22939
  process.env.PATH = [...current, ...missing].join(path3.delimiter);
22939
22940
  },
22940
- async run({ install, rewire: rewire2, say }) {
22941
- const found = await probe.tools();
22942
- if (!install)
22943
- return withReasons(found);
22944
- const repairs = { ...REPAIRS, ...rewire2 === void 0 ? {} : { brain: { name: "the brain", fix: rewire2 } } };
22945
- const broken = found.filter((tool) => !tool.ok && repairs[tool.id] !== void 0 && isDue(tool.id));
22946
- if (broken.length === 0)
22947
- return withReasons(found);
22948
- let ready = 0;
22949
- for (const [index, tool] of broken.entries()) {
22950
- const repair = repairs[tool.id];
22951
- if (repair === void 0)
22952
- continue;
22953
- say?.(ui.progress({ name: repair.name, done: index, total: broken.length }));
22954
- tried.set(tool.id, Date.now());
22955
- const failed = await repair.fix().then(() => null, (error51) => firstLine2(error51.message));
22956
- if (failed === null)
22957
- reasons.delete(tool.id);
22958
- else
22959
- reasons.set(tool.id, failed);
22960
- if (failed === null)
22961
- ready += 1;
22962
- say?.(`${ui.clear()}${failed === null ? ui.ok(repair.name) : ui.bad(repair.name, failed)}`);
22941
+ async runSteps({ steps, say }) {
22942
+ const results = [];
22943
+ for (const [index, step] of steps.entries()) {
22944
+ say?.(ui.progress({ name: step.name, done: index, total: steps.length }));
22945
+ const startedAt = Date.now();
22946
+ const outcome = await runScript(step.script);
22947
+ results.push({ ...outcome, id: step.id, name: step.name, ms: Date.now() - startedAt });
22948
+ say?.(`${ui.clear()}${outcome.ok ? ui.ok(step.name) : ui.bad(step.name, outcome.detail)}`);
22949
+ }
22950
+ if (results.length > 0) {
22951
+ say?.(ui.tally({ done: results.filter((result) => result.ok).length, total: results.length, noun: "ready" }));
22963
22952
  }
22964
- say?.(ui.tally({ done: ready, total: broken.length, noun: "ready" }));
22965
- return withReasons(await probe.tools());
22953
+ return results;
22966
22954
  }
22967
22955
  };
22968
- function isDue(id) {
22969
- const last = tried.get(id);
22970
- return last === void 0 || Date.now() - last > RETRY_AFTER_MS;
22971
- }
22972
- function withReasons(tools2) {
22973
- return tools2.map((tool) => {
22974
- const reason = tool.ok ? void 0 : reasons.get(tool.id);
22975
- return reason === void 0 ? tool : { ...tool, detail: `${tool.detail} \xB7 ${reason}`.slice(0, 200) };
22956
+ function runScript(script) {
22957
+ return new Promise((resolve) => {
22958
+ execFile("/bin/sh", ["-e", "-c", script], { timeout: STEP_TIMEOUT_MS, maxBuffer: SCRIPT_BUFFER_BYTES }, (error51, stdout, stderr) => {
22959
+ if (error51 === null)
22960
+ return resolve({ ok: true, detail: lastLines(stdout) });
22961
+ resolve({ ok: false, detail: lastLines(`${stderr}
22962
+ ${stdout}`) || error51.message.split("\n")[0] || "it failed" });
22963
+ });
22976
22964
  });
22977
22965
  }
22966
+ function lastLines(output) {
22967
+ return output.split("\n").map((line) => line.trim()).filter((line) => line !== "").slice(-KEPT_LINES).join(" \xB7 ").slice(0, WORKSTATION_STEP_DETAIL_MAX_CHARS);
22968
+ }
22978
22969
  function toolDirs() {
22979
22970
  const prefix = process.env.npm_config_prefix;
22980
22971
  return [
@@ -22986,56 +22977,14 @@ function toolDirs() {
22986
22977
  "/usr/local/bin"
22987
22978
  ];
22988
22979
  }
22989
- var REPAIRS = {
22990
- claude: { name: "Claude Code", fix: () => npmGlobal("@anthropic-ai/claude-code") },
22991
- codex: { name: "Codex", fix: () => npmGlobal("@openai/codex") },
22992
- git: {
22993
- name: "the GitHub CLI",
22994
- fix: async () => {
22995
- if (await firstAvailable(["gh"]) !== null)
22996
- return;
22997
- await installPackage({ brew: ["gh"], apt: ["gh"], dnf: ["gh"] });
22998
- }
22999
- },
23000
- chrome: {
23001
- name: "Google Chrome",
23002
- fix: () => installPackage({ brew: ["--cask", "google-chrome"], apt: ["chromium"], dnf: ["chromium"] })
23003
- }
23004
- };
23005
- async function npmGlobal(name) {
23006
- await run2("npm", ["install", "-g", name], { timeout: INSTALL_TIMEOUT_MS });
23007
- }
23008
- async function installPackage({ brew, apt, dnf }) {
23009
- if (process.platform === "darwin") {
23010
- await run2("brew", ["install", ...brew], { timeout: INSTALL_TIMEOUT_MS });
23011
- return;
23012
- }
23013
- const manager = await firstAvailable(["apt-get", "dnf"]);
23014
- if (manager === null)
23015
- throw new Error("no package manager this CLI knows how to drive");
23016
- const packages = manager === "apt-get" ? apt : dnf;
23017
- await run2("sudo", ["-n", manager, "install", "-y", ...packages], { timeout: INSTALL_TIMEOUT_MS });
23018
- }
23019
- async function firstAvailable(commands) {
23020
- for (const command of commands) {
23021
- const there = await run2("/bin/sh", ["-c", `command -v ${command}`]).then(() => true, () => false);
23022
- if (there)
23023
- return command;
23024
- }
23025
- return null;
23026
- }
23027
- function firstLine2(message) {
23028
- return message.split("\n")[0] ?? "it failed";
23029
- }
23030
- var tried = /* @__PURE__ */ new Map();
23031
- var reasons = /* @__PURE__ */ new Map();
23032
- var INSTALL_TIMEOUT_MS = 18e4;
23033
- var RETRY_AFTER_MS = 216e5;
22980
+ var STEP_TIMEOUT_MS = 6e5;
22981
+ var SCRIPT_BUFFER_BYTES = 4 * 1024 * 1024;
22982
+ var KEPT_LINES = 3;
23034
22983
 
23035
22984
  // dist/update.js
23036
- import { execFile as execFile3 } from "node:child_process";
23037
- import { promisify as promisify3 } from "node:util";
23038
- var run3 = promisify3(execFile3);
22985
+ import { execFile as execFile2 } from "node:child_process";
22986
+ import { promisify } from "node:util";
22987
+ var run = promisify(execFile2);
23039
22988
  var update = {
23040
22989
  async latest() {
23041
22990
  const answer = await fetch(`${REGISTRY}/${CLI_PACKAGE}/latest`, {
@@ -23045,17 +22994,17 @@ var update = {
23045
22994
  if (answer === null || !answer.ok)
23046
22995
  return null;
23047
22996
  const body = await answer.json().catch(() => null);
23048
- const version3 = isRecord(body) ? body.version : void 0;
23049
- return typeof version3 === "string" && isNewer(version3) ? version3 : null;
22997
+ const version2 = isRecord(body) ? body.version : void 0;
22998
+ return typeof version2 === "string" && isNewer(version2) ? version2 : null;
23050
22999
  },
23051
23000
  async toLatest() {
23052
- const version3 = await update.latest();
23053
- if (version3 === null)
23001
+ const version2 = await update.latest();
23002
+ if (version2 === null)
23054
23003
  return null;
23055
- const installed = await run3("npm", ["install", "--global", "--no-fund", `${CLI_PACKAGE}@${version3}`], {
23056
- timeout: INSTALL_TIMEOUT_MS2
23004
+ const installed = await run("npm", ["install", "--global", "--no-fund", `${CLI_PACKAGE}@${version2}`], {
23005
+ timeout: INSTALL_TIMEOUT_MS
23057
23006
  }).then(() => true, () => false);
23058
- return installed ? version3 : null;
23007
+ return installed ? version2 : null;
23059
23008
  }
23060
23009
  };
23061
23010
  function isNewer(candidate, current = CLI_VERSION) {
@@ -23069,18 +23018,18 @@ function isNewer(candidate, current = CLI_VERSION) {
23069
23018
  }
23070
23019
  return false;
23071
23020
  }
23072
- function parts(version3) {
23073
- return (/^\d+\.\d+\.\d+/.exec(version3.trim())?.[0] ?? "").split(".").map(Number);
23021
+ function parts(version2) {
23022
+ return (/^\d+\.\d+\.\d+/.exec(version2.trim())?.[0] ?? "").split(".").map(Number);
23074
23023
  }
23075
23024
  function isRecord(value) {
23076
23025
  return typeof value === "object" && value !== null;
23077
23026
  }
23078
23027
  var REGISTRY = "https://registry.npmjs.org";
23079
23028
  var REQUEST_TIMEOUT_MS = 1e4;
23080
- var INSTALL_TIMEOUT_MS2 = 18e4;
23029
+ var INSTALL_TIMEOUT_MS = 18e4;
23081
23030
 
23082
23031
  // dist/machine.js
23083
- var run4 = promisify4(execFile4);
23032
+ var run2 = promisify2(execFile3);
23084
23033
  var machine = {
23085
23034
  home() {
23086
23035
  return process.env.CRAFTSPACE_CLI_HOME ?? path4.join(os5.homedir(), ".craftspace");
@@ -23107,10 +23056,8 @@ var machine = {
23107
23056
  await writeFile(tokenPath(), token, { mode: 384 });
23108
23057
  await writeAgentConfigs({ url: url2, token, writes: answer.write });
23109
23058
  await installService();
23110
- probed = {
23111
- at: Date.now(),
23112
- tools: await setup.run({ install, rewire, say: ui.say })
23113
- };
23059
+ if (install)
23060
+ await machine.beatOnce(ui.say);
23114
23061
  return answer.machine;
23115
23062
  },
23116
23063
  async controlPlane(override) {
@@ -23147,8 +23094,9 @@ ${ui.ok(`Signed in as ${signed.ownerName}. This machine is ${ui.name(signed.name
23147
23094
  const found = await withAuth(await signedInOrAsk(), (auth) => call({ ...auth, method: "GET", path: "/api/machines/me", schema: MachineSchema }));
23148
23095
  return { machine: found.value, url: found.auth.url };
23149
23096
  },
23150
- async beatOnce() {
23097
+ async beatOnce(say) {
23151
23098
  const { url: url2, token } = await requireSignedIn();
23099
+ const built = await readEnvironment();
23152
23100
  const answer = await call({
23153
23101
  url: url2,
23154
23102
  token,
@@ -23159,12 +23107,19 @@ ${ui.ok(`Signed in as ${signed.ownerName}. This machine is ${ui.name(signed.name
23159
23107
  ...load2(),
23160
23108
  sessions: await readSessions(),
23161
23109
  runs: drainReports(),
23162
- tools: await tools(),
23110
+ tools: await checks(),
23111
+ environment: built?.results ?? [],
23112
+ templateId: built?.templateId ?? null,
23163
23113
  sshUser: os5.userInfo().username
23164
23114
  },
23165
23115
  schema: MachineBeatResponseSchema
23166
23116
  });
23167
23117
  await writeAuthorizedKeys(answer.authorizedKeys);
23118
+ const building2 = buildEnvironment(answer.environment, say);
23119
+ if (say === void 0)
23120
+ void building2.catch(() => void 0);
23121
+ else
23122
+ await building2;
23168
23123
  for (const work of answer.work) {
23169
23124
  if (running.has(work.id))
23170
23125
  continue;
@@ -23240,7 +23195,7 @@ async function pick2(machines) {
23240
23195
  choices: machines.map((candidate) => ({
23241
23196
  name: `${candidate.name.padEnd(24)} ${candidate.state.padEnd(12)} ${specs(candidate)} ${beat(candidate)}`,
23242
23197
  value: candidate,
23243
- description: candidate.sshAddress === null ? "No address yet, so nothing to open" : void 0
23198
+ disabled: candidate.sshAddress === null ? "(no address yet, so nothing to open)" : false
23244
23199
  })),
23245
23200
  pageSize: 12
23246
23201
  });
@@ -23292,13 +23247,51 @@ ${ui.attention("No workstations here yet.")}
23292
23247
  ui.say("\n");
23293
23248
  return 1;
23294
23249
  }
23295
- async function tools() {
23250
+ async function checks() {
23296
23251
  if (probed !== null && Date.now() - probed.at < PROBE_EVERY_MS)
23297
23252
  return probed.tools;
23298
- const found = await setup.run({ install: process.env.CRAFTSPACE_NO_INSTALL !== "1", rewire });
23253
+ const first = await probe.tools();
23254
+ const unwired = first.some((tool) => tool.id === "brain" && !tool.ok);
23255
+ const found = unwired && installs() ? await rewire().then(() => probe.tools(), () => first) : first;
23299
23256
  probed = { at: Date.now(), tools: found };
23300
23257
  return found;
23301
23258
  }
23259
+ async function buildEnvironment(next, say) {
23260
+ if (building || !installs() || next.templateId === null)
23261
+ return;
23262
+ const built = await readEnvironment();
23263
+ if (!buildIsDue(built, next.templateId))
23264
+ return;
23265
+ building = true;
23266
+ 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() });
23270
+ } finally {
23271
+ building = false;
23272
+ }
23273
+ }
23274
+ function buildIsDue(built, templateId) {
23275
+ if (built === null || built.templateId !== templateId)
23276
+ return true;
23277
+ if (built.results.every((result) => result.ok))
23278
+ return false;
23279
+ return Date.now() - built.at > REBUILD_AFTER_MS;
23280
+ }
23281
+ async function readEnvironment() {
23282
+ const raw = await readFile2(environmentPath(), "utf8").catch(() => null);
23283
+ if (raw === null)
23284
+ return null;
23285
+ const parsed = BuiltSchema.safeParse(JSON.parse(raw));
23286
+ return parsed.success ? parsed.data : null;
23287
+ }
23288
+ async function writeEnvironment(built) {
23289
+ await mkdir(machine.home(), { recursive: true, mode: 448 });
23290
+ await writeFile(environmentPath(), JSON.stringify(built, null, 2), { mode: 384 });
23291
+ }
23292
+ function installs() {
23293
+ return process.env.CRAFTSPACE_NO_INSTALL !== "1";
23294
+ }
23302
23295
  async function rewire() {
23303
23296
  const { url: url2, token } = await requireSignedIn();
23304
23297
  await writeAgentConfigs({ url: url2, token, writes: [] });
@@ -23359,7 +23352,7 @@ ${ui.note("Picking up where you left off.")}
23359
23352
  }
23360
23353
  async function openInBrowser(url2) {
23361
23354
  const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
23362
- await run4(opener, [url2]).catch(() => void 0);
23355
+ await run2(opener, [url2]).catch(() => void 0);
23363
23356
  }
23364
23357
  function tokenIn(line) {
23365
23358
  return /cst_[A-Za-z0-9_-]+/.exec(line)?.[0];
@@ -23371,7 +23364,7 @@ async function ensureKey() {
23371
23364
  if (existing !== null)
23372
23365
  return existing.trim();
23373
23366
  await mkdir(machine.home(), { recursive: true, mode: 448 });
23374
- await run4("ssh-keygen", ["-t", "ed25519", "-N", "", "-q", "-f", priv, "-C", `craftspace-${os5.hostname()}`]);
23367
+ await run2("ssh-keygen", ["-t", "ed25519", "-N", "", "-q", "-f", priv, "-C", `craftspace-${os5.hostname()}`]);
23375
23368
  return (await readFile2(pub, "utf8")).trim();
23376
23369
  }
23377
23370
  async function readConfig() {
@@ -23504,28 +23497,28 @@ async function installService() {
23504
23497
  const dir = path4.join(os5.homedir(), ".config", "systemd", "user");
23505
23498
  await mkdir(dir, { recursive: true });
23506
23499
  await writeFile(path4.join(dir, `${SERVICE_NAME}.service`), systemdUnit());
23507
- await run4("loginctl", ["enable-linger", os5.userInfo().username]).catch(() => void 0);
23508
- await run4("systemctl", ["--user", "daemon-reload"]).catch(() => void 0);
23509
- await run4("systemctl", ["--user", "enable", "--now", SERVICE_NAME]).catch(() => void 0);
23500
+ await run2("loginctl", ["enable-linger", os5.userInfo().username]).catch(() => void 0);
23501
+ await run2("systemctl", ["--user", "daemon-reload"]).catch(() => void 0);
23502
+ await run2("systemctl", ["--user", "enable", "--now", SERVICE_NAME]).catch(() => void 0);
23510
23503
  return;
23511
23504
  }
23512
23505
  if (process.platform === "darwin") {
23513
23506
  const target = path4.join(os5.homedir(), "Library", "LaunchAgents", `${LAUNCH_LABEL}.plist`);
23514
23507
  await mkdir(path4.dirname(target), { recursive: true });
23515
23508
  await writeFile(target, launchdPlist());
23516
- await run4("launchctl", ["unload", target]).catch(() => void 0);
23517
- await run4("launchctl", ["load", target]).catch(() => void 0);
23509
+ await run2("launchctl", ["unload", target]).catch(() => void 0);
23510
+ await run2("launchctl", ["load", target]).catch(() => void 0);
23518
23511
  }
23519
23512
  }
23520
23513
  async function uninstallService() {
23521
23514
  if (process.platform === "linux") {
23522
- await run4("systemctl", ["--user", "disable", "--now", SERVICE_NAME]).catch(() => void 0);
23515
+ await run2("systemctl", ["--user", "disable", "--now", SERVICE_NAME]).catch(() => void 0);
23523
23516
  await rm(path4.join(os5.homedir(), ".config", "systemd", "user", `${SERVICE_NAME}.service`), { force: true });
23524
23517
  return;
23525
23518
  }
23526
23519
  if (process.platform === "darwin") {
23527
23520
  const target = path4.join(os5.homedir(), "Library", "LaunchAgents", `${LAUNCH_LABEL}.plist`);
23528
- await run4("launchctl", ["unload", target]).catch(() => void 0);
23521
+ await run2("launchctl", ["unload", target]).catch(() => void 0);
23529
23522
  await rm(target, { force: true });
23530
23523
  }
23531
23524
  }
@@ -23562,10 +23555,10 @@ function here() {
23562
23555
  return `${process.execPath} ${entryPath()}`;
23563
23556
  }
23564
23557
  async function serviceRunning() {
23565
- return await run4("systemctl", ["--user", "is-active", SERVICE_NAME]).catch(() => null) !== null;
23558
+ return await run2("systemctl", ["--user", "is-active", SERVICE_NAME]).catch(() => null) !== null;
23566
23559
  }
23567
23560
  async function userLingers() {
23568
- const shown = await run4("loginctl", ["show-user", os5.userInfo().username, "-p", "Linger"]).catch(() => null);
23561
+ const shown = await run2("loginctl", ["show-user", os5.userInfo().username, "-p", "Linger"]).catch(() => null);
23569
23562
  return shown?.stdout.includes("Linger=yes") === true;
23570
23563
  }
23571
23564
  function entryPath() {
@@ -23583,6 +23576,9 @@ function tokenPath() {
23583
23576
  function ownedPath() {
23584
23577
  return path4.join(machine.home(), "owned.json");
23585
23578
  }
23579
+ function environmentPath() {
23580
+ return path4.join(machine.home(), "environment.json");
23581
+ }
23586
23582
  function load2() {
23587
23583
  const cores = Math.max(1, os5.cpus().length);
23588
23584
  const busy = Math.min(100, (os5.loadavg()[0] ?? 0) / cores * 100);
@@ -23601,6 +23597,11 @@ function messageOf(error51) {
23601
23597
  return error51 instanceof Error ? error51.message : String(error51);
23602
23598
  }
23603
23599
  var ConfigSchema = external_exports.object({ url: external_exports.string(), machineId: external_exports.string() });
23600
+ var BuiltSchema = external_exports.object({
23601
+ templateId: external_exports.string(),
23602
+ results: external_exports.array(WorkstationStepResultSchema),
23603
+ at: external_exports.number()
23604
+ });
23604
23605
  var OwnedSchema = external_exports.object({ paths: external_exports.array(external_exports.string()) });
23605
23606
  var SERVICE_NAME = "craftspace";
23606
23607
  var LAUNCH_LABEL = "app.craftspace.machine";
@@ -23629,7 +23630,9 @@ var PORTABLE_TERMS = /* @__PURE__ */ new Set([
23629
23630
  var LINGER_LINE = `sudo loginctl enable-linger ${os5.userInfo().username}`;
23630
23631
  var DEFAULT_URL = process.env.CRAFTSPACE_URL ?? "https://craftspace.app";
23631
23632
  var PROBE_EVERY_MS = 3e5;
23633
+ var REBUILD_AFTER_MS = 216e5;
23632
23634
  var probed = null;
23635
+ var building = false;
23633
23636
  var reports = /* @__PURE__ */ new Map();
23634
23637
  var running = /* @__PURE__ */ new Set();
23635
23638
 
package/dist/machine.d.ts CHANGED
@@ -16,7 +16,7 @@ export declare const machine: {
16
16
  machine: Machine;
17
17
  url: string;
18
18
  }>;
19
- beatOnce(): Promise<boolean>;
19
+ beatOnce(say?: (line: string) => void): Promise<boolean>;
20
20
  beatForever(): Promise<void>;
21
21
  run(): Promise<number>;
22
22
  daemon(): Promise<{
package/dist/machine.js CHANGED
@@ -3,9 +3,10 @@ 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, } from '@craftspace/shared';
6
+ import { CLI_VERSION, MACHINE_BEAT_BUSY_INTERVAL_MS, MACHINE_BEAT_INTERVAL_MS, MachineBeatResponseSchema, MachineLoginResponseSchema, MachineSchema, MachineSessionSchema, MachinesResponseSchema, WorkstationStepResultSchema, } from '@craftspace/shared';
7
7
  import { confirm, input, select } from '@inquirer/prompts';
8
8
  import { z } from 'zod';
9
+ import { probe } from './probe.js';
9
10
  import { runner } from './run.js';
10
11
  import { setup } from './setup.js';
11
12
  import { ui } from './ui.js';
@@ -37,10 +38,8 @@ export const machine = {
37
38
  await writeFile(tokenPath(), token, { mode: 0o600 });
38
39
  await writeAgentConfigs({ url, token, writes: answer.write });
39
40
  await installService();
40
- probed = {
41
- at: Date.now(),
42
- tools: await setup.run({ install, rewire, say: ui.say }),
43
- };
41
+ if (install)
42
+ await machine.beatOnce(ui.say);
44
43
  return answer.machine;
45
44
  },
46
45
  async controlPlane(override) {
@@ -74,8 +73,9 @@ export const machine = {
74
73
  const found = await withAuth(await signedInOrAsk(), (auth) => call({ ...auth, method: 'GET', path: '/api/machines/me', schema: MachineSchema }));
75
74
  return { machine: found.value, url: found.auth.url };
76
75
  },
77
- async beatOnce() {
76
+ async beatOnce(say) {
78
77
  const { url, token } = await requireSignedIn();
78
+ const built = await readEnvironment();
79
79
  const answer = await call({
80
80
  url,
81
81
  token,
@@ -86,12 +86,19 @@ export const machine = {
86
86
  ...load(),
87
87
  sessions: await readSessions(),
88
88
  runs: drainReports(),
89
- tools: await tools(),
89
+ tools: await checks(),
90
+ environment: built?.results ?? [],
91
+ templateId: built?.templateId ?? null,
90
92
  sshUser: os.userInfo().username,
91
93
  },
92
94
  schema: MachineBeatResponseSchema,
93
95
  });
94
96
  await writeAuthorizedKeys(answer.authorizedKeys);
97
+ const building = buildEnvironment(answer.environment, say);
98
+ if (say === undefined)
99
+ void building.catch(() => undefined);
100
+ else
101
+ await building;
95
102
  for (const work of answer.work) {
96
103
  if (running.has(work.id))
97
104
  continue;
@@ -170,7 +177,7 @@ async function pick(machines) {
170
177
  choices: machines.map((candidate) => ({
171
178
  name: `${candidate.name.padEnd(24)} ${candidate.state.padEnd(12)} ${specs(candidate)} ${beat(candidate)}`,
172
179
  value: candidate,
173
- description: candidate.sshAddress === null ? 'No address yet, so nothing to open' : undefined,
180
+ disabled: candidate.sshAddress === null ? '(no address yet, so nothing to open)' : false,
174
181
  })),
175
182
  pageSize: 12,
176
183
  });
@@ -218,13 +225,52 @@ function nowhereToGo({ url }) {
218
225
  ui.say('\n');
219
226
  return 1;
220
227
  }
221
- async function tools() {
228
+ async function checks() {
222
229
  if (probed !== null && Date.now() - probed.at < PROBE_EVERY_MS)
223
230
  return probed.tools;
224
- const found = await setup.run({ install: process.env.CRAFTSPACE_NO_INSTALL !== '1', rewire });
231
+ const first = await probe.tools();
232
+ const unwired = first.some((tool) => tool.id === 'brain' && !tool.ok);
233
+ const found = unwired && installs() ? await rewire().then(() => probe.tools(), () => first) : first;
225
234
  probed = { at: Date.now(), tools: found };
226
235
  return found;
227
236
  }
237
+ async function buildEnvironment(next, say) {
238
+ if (building || !installs() || next.templateId === null)
239
+ return;
240
+ const built = await readEnvironment();
241
+ if (!buildIsDue(built, next.templateId))
242
+ return;
243
+ building = true;
244
+ 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() });
248
+ }
249
+ finally {
250
+ building = false;
251
+ }
252
+ }
253
+ function buildIsDue(built, templateId) {
254
+ if (built === null || built.templateId !== templateId)
255
+ return true;
256
+ if (built.results.every((result) => result.ok))
257
+ return false;
258
+ return Date.now() - built.at > REBUILD_AFTER_MS;
259
+ }
260
+ async function readEnvironment() {
261
+ const raw = await readFile(environmentPath(), 'utf8').catch(() => null);
262
+ if (raw === null)
263
+ return null;
264
+ const parsed = BuiltSchema.safeParse(JSON.parse(raw));
265
+ return parsed.success ? parsed.data : null;
266
+ }
267
+ async function writeEnvironment(built) {
268
+ await mkdir(machine.home(), { recursive: true, mode: 0o700 });
269
+ await writeFile(environmentPath(), JSON.stringify(built, null, 2), { mode: 0o600 });
270
+ }
271
+ function installs() {
272
+ return process.env.CRAFTSPACE_NO_INSTALL !== '1';
273
+ }
228
274
  async function rewire() {
229
275
  const { url, token } = await requireSignedIn();
230
276
  await writeAgentConfigs({ url, token, writes: [] });
@@ -503,6 +549,9 @@ function tokenPath() {
503
549
  function ownedPath() {
504
550
  return path.join(machine.home(), 'owned.json');
505
551
  }
552
+ function environmentPath() {
553
+ return path.join(machine.home(), 'environment.json');
554
+ }
506
555
  function load() {
507
556
  const cores = Math.max(1, os.cpus().length);
508
557
  const busy = Math.min(100, ((os.loadavg()[0] ?? 0) / cores) * 100);
@@ -521,6 +570,11 @@ function messageOf(error) {
521
570
  return error instanceof Error ? error.message : String(error);
522
571
  }
523
572
  const ConfigSchema = z.object({ url: z.string(), machineId: z.string() });
573
+ const BuiltSchema = z.object({
574
+ templateId: z.string(),
575
+ results: z.array(WorkstationStepResultSchema),
576
+ at: z.number(),
577
+ });
524
578
  const OwnedSchema = z.object({ paths: z.array(z.string()) });
525
579
  const SERVICE_NAME = 'craftspace';
526
580
  const LAUNCH_LABEL = 'app.craftspace.machine';
@@ -549,6 +603,8 @@ const PORTABLE_TERMS = new Set([
549
603
  const LINGER_LINE = `sudo loginctl enable-linger ${os.userInfo().username}`;
550
604
  const DEFAULT_URL = process.env.CRAFTSPACE_URL ?? 'https://craftspace.app';
551
605
  const PROBE_EVERY_MS = 300_000;
606
+ const REBUILD_AFTER_MS = 21_600_000;
552
607
  let probed = null;
608
+ let building = false;
553
609
  const reports = new Map();
554
610
  const running = new Set();
package/dist/probe.d.ts CHANGED
@@ -4,4 +4,3 @@ export declare const probe: {
4
4
  mcpPath?: string;
5
5
  }): Promise<MachineTool[]>;
6
6
  };
7
- export declare function githubUser(hostsYml: string): string | null;
package/dist/probe.js CHANGED
@@ -1,21 +1,10 @@
1
- import { execFile } from 'node:child_process';
2
1
  import net from 'node:net';
3
- import { access, readFile } from 'node:fs/promises';
2
+ import { readFile } from 'node:fs/promises';
4
3
  import os from 'node:os';
5
4
  import path from 'node:path';
6
- import { promisify } from 'node:util';
7
- const run = promisify(execFile);
8
5
  export const probe = {
9
6
  async tools({ mcpPath = path.join(os.homedir(), '.mcp.json') } = {}) {
10
- const [brain, claude, codex, git, chrome, ssh] = await Promise.all([
11
- brainWired(mcpPath),
12
- version({ id: 'claude', command: 'claude' }),
13
- version({ id: 'codex', command: 'codex' }),
14
- gitAndGithub(),
15
- chromeInstalled(),
16
- sshListening(),
17
- ]);
18
- return [brain, claude, codex, git, chrome, ssh];
7
+ return Promise.all([brainWired(mcpPath), sshListening()]);
19
8
  },
20
9
  };
21
10
  async function brainWired(mcpPath) {
@@ -31,43 +20,6 @@ async function brainWired(mcpPath) {
31
20
  detail: wired ? short(mcpPath) : `${short(mcpPath)} has no craftspace entry`,
32
21
  };
33
22
  }
34
- async function version({ id, command }) {
35
- const found = await firstLine(command, ['--version']);
36
- if (found.ran)
37
- return { id, ok: true, detail: found.line };
38
- return { id, ok: false, detail: found.why === 'absent' ? 'not installed' : 'installed, but it will not report a version' };
39
- }
40
- async function gitAndGithub() {
41
- const git = await firstLine('git', ['--version']);
42
- if (!git.ran)
43
- return { id: 'git', ok: false, detail: 'git is not installed' };
44
- const gh = await firstLine('gh', ['--version']);
45
- if (!gh.ran)
46
- return { id: 'git', ok: false, detail: `${git.line} · the GitHub CLI is not installed yet` };
47
- const account = await githubAccount();
48
- return {
49
- id: 'git',
50
- ok: account !== null,
51
- detail: account === null
52
- ? `${git.line} · ${gh.line}, run gh auth login on this machine`
53
- : `${git.line} · ${gh.line}, signed in as ${account}`,
54
- };
55
- }
56
- async function githubAccount() {
57
- const raw = await readFile(path.join(os.homedir(), '.config', 'gh', 'hosts.yml'), 'utf8').catch(() => null);
58
- return raw === null ? null : githubUser(raw);
59
- }
60
- export function githubUser(hostsYml) {
61
- return /^ {4}user: *(\S+) *$/m.exec(hostsYml)?.[1] ?? null;
62
- }
63
- async function chromeInstalled() {
64
- for (const command of [...MAC_CHROME, ...LINUX_CHROME]) {
65
- const found = await firstLine(command, ['--version']);
66
- if (found.ran)
67
- return { id: 'chrome', ok: true, detail: found.line };
68
- }
69
- return { id: 'chrome', ok: false, detail: 'not installed yet, so browser runs will fail' };
70
- }
71
23
  function sshListening() {
72
24
  return new Promise((resolve) => {
73
25
  const socket = net.connect({ host: '127.0.0.1', port: 22 });
@@ -78,25 +30,10 @@ function sshListening() {
78
30
  socket.setTimeout(SSH_PROBE_TIMEOUT_MS);
79
31
  socket.on('connect', () => answer(true, 'sshd is listening on port 22'));
80
32
  socket.on('timeout', () => answer(false, 'nothing answered on port 22'));
81
- socket.on('error', () => answer(false, 'sshd is not running, so nobody can ssh in'));
33
+ socket.on('error', () => answer(false, 'sshd is not running, so nobody can open a terminal here'));
82
34
  });
83
35
  }
84
- async function firstLine(command, args) {
85
- if (command.includes('/')) {
86
- const reachable = await access(command).then(() => true, () => false);
87
- if (!reachable)
88
- return { ran: false, why: 'absent' };
89
- }
90
- const answer = await run(command, args, { timeout: PROBE_TIMEOUT_MS }).catch((thrown) => thrown);
91
- if (answer instanceof Error)
92
- return { ran: false, why: answer.code === 'ENOENT' ? 'absent' : 'broken' };
93
- const line = answer.stdout.split('\n')[0]?.trim() ?? '';
94
- return line === '' ? { ran: false, why: 'broken' } : { ran: true, line: line.slice(0, 200) };
95
- }
96
36
  function short(target) {
97
37
  return target.startsWith(os.homedir()) ? `~${target.slice(os.homedir().length)}` : target;
98
38
  }
99
- const MAC_CHROME = ['/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'];
100
- const LINUX_CHROME = ['google-chrome', 'google-chrome-stable', 'chromium'];
101
- const PROBE_TIMEOUT_MS = 5_000;
102
39
  const SSH_PROBE_TIMEOUT_MS = 1_500;
package/dist/setup.d.ts CHANGED
@@ -1,9 +1,8 @@
1
- import type { MachineTool } from '@craftspace/shared';
1
+ import { type WorkstationStep, type WorkstationStepResult } from '@craftspace/shared';
2
2
  export declare const setup: {
3
3
  widenPath(): void;
4
- run({ install, rewire, say, }: {
5
- install: boolean;
6
- rewire?: () => Promise<void>;
4
+ runSteps({ steps, say, }: {
5
+ steps: WorkstationStep[];
7
6
  say?: (line: string) => void;
8
- }): Promise<MachineTool[]>;
7
+ }): Promise<WorkstationStepResult[]>;
9
8
  };
package/dist/setup.js CHANGED
@@ -1,10 +1,8 @@
1
1
  import { execFile } from 'node:child_process';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
- import { promisify } from 'node:util';
5
- import { probe } from './probe.js';
4
+ import { WORKSTATION_STEP_DETAIL_MAX_CHARS, } from '@craftspace/shared';
6
5
  import { ui } from './ui.js';
7
- const run = promisify(execFile);
8
6
  export const setup = {
9
7
  widenPath() {
10
8
  const current = (process.env.PATH ?? '').split(path.delimiter).filter((entry) => entry !== '');
@@ -12,44 +10,39 @@ export const setup = {
12
10
  if (missing.length > 0)
13
11
  process.env.PATH = [...current, ...missing].join(path.delimiter);
14
12
  },
15
- async run({ install, rewire, say, }) {
16
- const found = await probe.tools();
17
- if (!install)
18
- return withReasons(found);
19
- const repairs = { ...REPAIRS, ...(rewire === undefined ? {} : { brain: { name: 'the brain', fix: rewire } }) };
20
- const broken = found.filter((tool) => !tool.ok && repairs[tool.id] !== undefined && isDue(tool.id));
21
- if (broken.length === 0)
22
- return withReasons(found);
23
- let ready = 0;
24
- for (const [index, tool] of broken.entries()) {
25
- const repair = repairs[tool.id];
26
- if (repair === undefined)
27
- continue;
28
- say?.(ui.progress({ name: repair.name, done: index, total: broken.length }));
29
- tried.set(tool.id, Date.now());
30
- const failed = await repair.fix().then(() => null, (error) => firstLine(error.message));
31
- if (failed === null)
32
- reasons.delete(tool.id);
33
- else
34
- reasons.set(tool.id, failed);
35
- if (failed === null)
36
- ready += 1;
37
- say?.(`${ui.clear()}${failed === null ? ui.ok(repair.name) : ui.bad(repair.name, failed)}`);
13
+ async runSteps({ steps, say, }) {
14
+ const results = [];
15
+ for (const [index, step] of steps.entries()) {
16
+ say?.(ui.progress({ name: step.name, done: index, total: steps.length }));
17
+ const startedAt = Date.now();
18
+ const outcome = await runScript(step.script);
19
+ results.push({ ...outcome, id: step.id, name: step.name, ms: Date.now() - startedAt });
20
+ say?.(`${ui.clear()}${outcome.ok ? ui.ok(step.name) : ui.bad(step.name, outcome.detail)}`);
38
21
  }
39
- say?.(ui.tally({ done: ready, total: broken.length, noun: 'ready' }));
40
- return withReasons(await probe.tools());
22
+ if (results.length > 0) {
23
+ say?.(ui.tally({ done: results.filter((result) => result.ok).length, total: results.length, noun: 'ready' }));
24
+ }
25
+ return results;
41
26
  },
42
27
  };
43
- function isDue(id) {
44
- const last = tried.get(id);
45
- return last === undefined || Date.now() - last > RETRY_AFTER_MS;
46
- }
47
- function withReasons(tools) {
48
- return tools.map((tool) => {
49
- const reason = tool.ok ? undefined : reasons.get(tool.id);
50
- return reason === undefined ? tool : { ...tool, detail: `${tool.detail} · ${reason}`.slice(0, 200) };
28
+ function runScript(script) {
29
+ return new Promise((resolve) => {
30
+ execFile('/bin/sh', ['-e', '-c', script], { timeout: STEP_TIMEOUT_MS, maxBuffer: SCRIPT_BUFFER_BYTES }, (error, stdout, stderr) => {
31
+ if (error === null)
32
+ return resolve({ ok: true, detail: lastLines(stdout) });
33
+ resolve({ ok: false, detail: lastLines(`${stderr}\n${stdout}`) || error.message.split('\n')[0] || 'it failed' });
34
+ });
51
35
  });
52
36
  }
37
+ function lastLines(output) {
38
+ return output
39
+ .split('\n')
40
+ .map((line) => line.trim())
41
+ .filter((line) => line !== '')
42
+ .slice(-KEPT_LINES)
43
+ .join(' · ')
44
+ .slice(0, WORKSTATION_STEP_DETAIL_MAX_CHARS);
45
+ }
53
46
  function toolDirs() {
54
47
  const prefix = process.env.npm_config_prefix;
55
48
  return [
@@ -61,48 +54,6 @@ function toolDirs() {
61
54
  '/usr/local/bin',
62
55
  ];
63
56
  }
64
- const REPAIRS = {
65
- claude: { name: 'Claude Code', fix: () => npmGlobal('@anthropic-ai/claude-code') },
66
- codex: { name: 'Codex', fix: () => npmGlobal('@openai/codex') },
67
- git: {
68
- name: 'the GitHub CLI',
69
- fix: async () => {
70
- if ((await firstAvailable(['gh'])) !== null)
71
- return;
72
- await installPackage({ brew: ['gh'], apt: ['gh'], dnf: ['gh'] });
73
- },
74
- },
75
- chrome: {
76
- name: 'Google Chrome',
77
- fix: () => installPackage({ brew: ['--cask', 'google-chrome'], apt: ['chromium'], dnf: ['chromium'] }),
78
- },
79
- };
80
- async function npmGlobal(name) {
81
- await run('npm', ['install', '-g', name], { timeout: INSTALL_TIMEOUT_MS });
82
- }
83
- async function installPackage({ brew, apt, dnf }) {
84
- if (process.platform === 'darwin') {
85
- await run('brew', ['install', ...brew], { timeout: INSTALL_TIMEOUT_MS });
86
- return;
87
- }
88
- const manager = await firstAvailable(['apt-get', 'dnf']);
89
- if (manager === null)
90
- throw new Error('no package manager this CLI knows how to drive');
91
- const packages = manager === 'apt-get' ? apt : dnf;
92
- await run('sudo', ['-n', manager, 'install', '-y', ...packages], { timeout: INSTALL_TIMEOUT_MS });
93
- }
94
- async function firstAvailable(commands) {
95
- for (const command of commands) {
96
- const there = await run('/bin/sh', ['-c', `command -v ${command}`]).then(() => true, () => false);
97
- if (there)
98
- return command;
99
- }
100
- return null;
101
- }
102
- function firstLine(message) {
103
- return message.split('\n')[0] ?? 'it failed';
104
- }
105
- const tried = new Map();
106
- const reasons = new Map();
107
- const INSTALL_TIMEOUT_MS = 180_000;
108
- const RETRY_AFTER_MS = 21_600_000;
57
+ const STEP_TIMEOUT_MS = 600_000;
58
+ const SCRIPT_BUFFER_BYTES = 4 * 1024 * 1024;
59
+ const KEPT_LINES = 3;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@craftspace/cli",
3
- "version": "0.4.3",
3
+ "version": "0.5.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",