@tablation/crew 0.8.0 → 0.9.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.
Files changed (3) hide show
  1. package/README.md +2 -2
  2. package/dist/cli.js +165 -43
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -213,8 +213,8 @@ crew inbox [--member NAME] your tickets across every workspace
213
213
  crew connect WS[/PROJECT] resolve a workspace/project's ids into the state tree
214
214
  crew repos add [ROUTE] [NAME] [PATH] attach a local checkout to a route in crew.yaml, creating its Repos row if missing (--dry-run previews)
215
215
  crew repos list ROUTE the checkouts a route has, and the tracker Repos row each matches
216
- crew install write and load this platform's scheduler unit
217
- crew uninstall unload and remove it
216
+ crew install [--replace] write and load this platform's scheduler unit (macOS: clears a crew installed from another path)
217
+ crew uninstall [--all] unload and remove it (--all: every crew launchd unit, whichever path it came from)
218
218
  ```
219
219
 
220
220
  Every command that could change something takes `--dry-run`.
package/dist/cli.js CHANGED
@@ -1930,7 +1930,7 @@ function describeRelease(o, route) {
1930
1930
  // src/install.ts
1931
1931
  import { dirname as dirname3, join as join5, extname } from "path";
1932
1932
  import { homedir as homedir2 } from "os";
1933
- import { accessSync, constants, existsSync as existsSync5, mkdirSync as mkdirSync3, statSync as statSync2, writeFileSync as writeFileSync2, unlinkSync, realpathSync } from "fs";
1933
+ import { accessSync, constants, existsSync as existsSync5, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync5, statSync as statSync2, writeFileSync as writeFileSync2, unlinkSync, realpathSync } from "fs";
1934
1934
  import { spawnSync } from "child_process";
1935
1935
  import { createHash } from "crypto";
1936
1936
 
@@ -2164,7 +2164,8 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
2164
2164
  mechanism: "launchd",
2165
2165
  unitPaths: [unitPath],
2166
2166
  crontabLine: null,
2167
- unloadCommands: [["launchctl", "unload", unitPath]]
2167
+ unloadCommands: [["launchctl", "unload", unitPath]],
2168
+ removeLabel: label
2168
2169
  };
2169
2170
  }
2170
2171
  if (hostShip === "linux" && hasSystemd) {
@@ -2282,6 +2283,15 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
2282
2283
  if (!ok) log.warn("unload command failed \u2014 it may not have been loaded; continuing to remove files");
2283
2284
  }
2284
2285
  }
2286
+ if (plan.removeLabel) {
2287
+ if (dryRun) {
2288
+ log.emit(`would run: launchctl remove ${plan.removeLabel}`);
2289
+ } else {
2290
+ if (spawnSync("launchctl", ["remove", plan.removeLabel], { encoding: "utf8" }).status === 0) {
2291
+ log.emit(`removed loaded job ${plan.removeLabel}`);
2292
+ }
2293
+ }
2294
+ }
2285
2295
  for (const path of plan.unitPaths) {
2286
2296
  if (!existsSync5(path)) continue;
2287
2297
  if (dryRun) {
@@ -2300,6 +2310,91 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
2300
2310
  }
2301
2311
  }
2302
2312
  }
2313
+ var CREW_LAUNCHD_PREFIX = "com.tablation.crew";
2314
+ function launchAgentsDir() {
2315
+ return join5(homedir2(), "Library", "LaunchAgents");
2316
+ }
2317
+ function xmlUnescape(s) {
2318
+ return s.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
2319
+ }
2320
+ function programFromPlist(text) {
2321
+ const block = /<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/.exec(text);
2322
+ if (!block) return null;
2323
+ const args = [...block[1].matchAll(/<string>([\s\S]*?)<\/string>/g)].map((m) => xmlUnescape(m[1]));
2324
+ return args.find((a) => a.endsWith("/bin/crew")) ?? args[0] ?? null;
2325
+ }
2326
+ function listLaunchdUnits(dir = launchAgentsDir()) {
2327
+ if (!existsSync5(dir)) return [];
2328
+ return readdirSync(dir).filter((f) => f.startsWith(CREW_LAUNCHD_PREFIX) && f.endsWith(".plist")).sort().map((f) => {
2329
+ const plistPath = join5(dir, f);
2330
+ let program = null;
2331
+ try {
2332
+ program = programFromPlist(readFileSync5(plistPath, "utf8"));
2333
+ } catch {
2334
+ }
2335
+ return {
2336
+ label: f.slice(0, -".plist".length),
2337
+ plistPath,
2338
+ program,
2339
+ programExists: program !== null && existsSync5(program)
2340
+ };
2341
+ });
2342
+ }
2343
+ function foreignLaunchdUnits(crewHome, dir) {
2344
+ const own = new Set(["run", "release", "passengers"].map((job) => labelFor(crewHome, job)));
2345
+ return listLaunchdUnits(dir).filter((u) => !own.has(u.label));
2346
+ }
2347
+ function removeLaunchdUnit(unit, dryRun, log) {
2348
+ if (dryRun) {
2349
+ log.emit(`would unload and remove ${unit.label}`);
2350
+ return;
2351
+ }
2352
+ if (existsSync5(unit.plistPath)) spawnSync("launchctl", ["unload", unit.plistPath], { encoding: "utf8" });
2353
+ spawnSync("launchctl", ["remove", unit.label], { encoding: "utf8" });
2354
+ if (existsSync5(unit.plistPath)) unlinkSync(unit.plistPath);
2355
+ log.emit(`removed ${unit.label} (${unit.plistPath})`);
2356
+ }
2357
+ function clearForeignLaunchdUnits(crewHome, replace, dryRun, log, dir) {
2358
+ const foreign = foreignLaunchdUnits(crewHome, dir);
2359
+ const blocking = foreign.filter((u) => u.program === null || u.programExists);
2360
+ if (blocking.length > 0 && !replace) {
2361
+ log.warn(
2362
+ `two crew installs target this machine: ${blocking.map((u) => `${u.label} runs ${u.program ?? "an unreadable ProgramArguments"}`).join("; ")}. Run \`crew uninstall\` from the other one, or pass --replace.`
2363
+ );
2364
+ return false;
2365
+ }
2366
+ for (const unit of foreign) {
2367
+ const why = unit.program === null ? "replacing a unit with no readable ProgramArguments" : unit.programExists ? "replacing a live other install" : "removing a stale unit whose program is gone";
2368
+ log.emit(`${why}: ${unit.label}`);
2369
+ removeLaunchdUnit(unit, dryRun, log);
2370
+ }
2371
+ return true;
2372
+ }
2373
+ function removeAllLaunchdUnits(dryRun, log, dir) {
2374
+ for (const unit of listLaunchdUnits(dir)) removeLaunchdUnit(unit, dryRun, log);
2375
+ }
2376
+ function parseLaunchctlList(text) {
2377
+ const jobs = /* @__PURE__ */ new Map();
2378
+ for (const line of text.split("\n")) {
2379
+ const cols = line.split(" ");
2380
+ if (cols.length !== 3 || cols[2] === "Label") continue;
2381
+ jobs.set(cols[2], { pid: cols[0], lastExit: cols[1] });
2382
+ }
2383
+ return jobs;
2384
+ }
2385
+ function describeForeignLaunchdUnits(crewHome, dir) {
2386
+ const foreign = foreignLaunchdUnits(crewHome, dir);
2387
+ if (foreign.length === 0) return [];
2388
+ const list = spawnSync("launchctl", ["list"], { encoding: "utf8" });
2389
+ const jobs = parseLaunchctlList(list.status === 0 ? list.stdout ?? "" : "");
2390
+ return foreign.map((u) => {
2391
+ const status2 = jobs.get(u.label);
2392
+ const loaded = status2 ? `loaded, last exit ${status2.lastExit}` : "not loaded";
2393
+ const program = u.program ? `${u.program} (${u.programExists ? "present" : "GONE"})` : "no readable ProgramArguments";
2394
+ const stale = status2 && !u.programExists ? " \u2014 stale: `crew install` removes it" : "";
2395
+ return ` ${u.label}: ${loaded}; runs ${program}${stale}`;
2396
+ });
2397
+ }
2303
2398
  function readCrontab() {
2304
2399
  const res = spawnSync("crontab", ["-l"], { encoding: "utf8" });
2305
2400
  return res.status === 0 ? res.stdout ?? "" : "";
@@ -4511,7 +4606,7 @@ var alreadyReported = (ticket, g, comments) => comments.some((c) => c.ticket_id
4511
4606
 
4512
4607
  // src/agent.ts
4513
4608
  import { spawn as spawn2 } from "child_process";
4514
- import { readFileSync as readFileSync5, existsSync as existsSync7, statSync as statSync4, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4, createWriteStream } from "fs";
4609
+ import { readFileSync as readFileSync6, existsSync as existsSync7, statSync as statSync4, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4, createWriteStream } from "fs";
4515
4610
  import { finished } from "stream/promises";
4516
4611
  import { join as join7, dirname as dirname5 } from "path";
4517
4612
  import { createHash as createHash2 } from "crypto";
@@ -4954,10 +5049,10 @@ function planAgentRun(o) {
4954
5049
  let ageSeconds;
4955
5050
  if (existsSync7(digestPath2)) {
4956
5051
  ageSeconds = Math.floor((now() - statSync4(digestPath2).mtimeMs) / 1e3);
4957
- if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync5(digestPath2, "utf8");
5052
+ if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync6(digestPath2, "utf8");
4958
5053
  }
4959
- const common = readFileSync5(commonPath, "utf8");
4960
- const brief = readFileSync5(briefPath, "utf8");
5054
+ const common = readFileSync6(commonPath, "utf8");
5055
+ const brief = readFileSync6(briefPath, "utf8");
4961
5056
  const effectiveCommon = o.divergedPrompt ? "" : common;
4962
5057
  const effectiveBrief = o.divergedPrompt ?? brief;
4963
5058
  const prompt = assemblePrompt({
@@ -5337,7 +5432,7 @@ Wrote ${o.configPath}.
5337
5432
  }
5338
5433
 
5339
5434
  // src/release-run.ts
5340
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync6, existsSync as existsSync9 } from "fs";
5435
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, existsSync as existsSync9 } from "fs";
5341
5436
  import { join as join9 } from "path";
5342
5437
 
5343
5438
  // src/release.ts
@@ -5802,7 +5897,7 @@ async function bump(o, size) {
5802
5897
  o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
5803
5898
  return null;
5804
5899
  }
5805
- const current = JSON.parse(readFileSync6(first, "utf8")).version;
5900
+ const current = JSON.parse(readFileSync7(first, "utf8")).version;
5806
5901
  if (!current) {
5807
5902
  o.emit.error(`no version field in ${o.repo.release.versionFiles[0]}`);
5808
5903
  return null;
@@ -5815,7 +5910,7 @@ async function bump(o, size) {
5815
5910
  return version;
5816
5911
  }
5817
5912
  for (const f of files) {
5818
- const src = readFileSync6(f, "utf8");
5913
+ const src = readFileSync7(f, "utf8");
5819
5914
  const out = src.replace(/("version"\s*:\s*")[^"]*(")/, `$1${version}$2`);
5820
5915
  if (out === src) {
5821
5916
  o.emit.error(`no version field in ${f}`);
@@ -5834,7 +5929,7 @@ function writeChangelog(o, version, entries) {
5834
5929
  o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
5835
5930
  return;
5836
5931
  }
5837
- const existing = existsSync9(path) ? readFileSync6(path, "utf8") : "# Changelog\n";
5932
+ const existing = existsSync9(path) ? readFileSync7(path, "utf8") : "# Changelog\n";
5838
5933
  writeFileSync6(path, insertChangelogSection(existing, section));
5839
5934
  }
5840
5935
  async function confirm(o, expected) {
@@ -6682,7 +6777,7 @@ function nextRoles(decision, ran) {
6682
6777
  }
6683
6778
 
6684
6779
  // src/agents.ts
6685
- import { readFileSync as readFileSync7, existsSync as existsSync12 } from "fs";
6780
+ import { readFileSync as readFileSync8, existsSync as existsSync12 } from "fs";
6686
6781
  import { join as join11 } from "path";
6687
6782
  import { TablationClient as TablationClient2, StaleWriteError as StaleWriteError6, TablationApiError } from "@tablation/client";
6688
6783
  var PERSONA_NAME = {
@@ -6699,10 +6794,10 @@ var AgentsSyncError = class extends Error {
6699
6794
  function personaDefaultPrompt(promptsDir, role) {
6700
6795
  const briefPath = join11(promptsDir, "personas", `lane-${role}.md`);
6701
6796
  if (!existsSync12(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
6702
- if (role === "pair") return readFileSync7(briefPath, "utf8");
6797
+ if (role === "pair") return readFileSync8(briefPath, "utf8");
6703
6798
  const commonPath = join11(promptsDir, "personas", "common.md");
6704
6799
  if (!existsSync12(commonPath)) throw new AgentsSyncError(`no shared policy at ${commonPath}`);
6705
- return `${readFileSync7(commonPath, "utf8")}${readFileSync7(briefPath, "utf8")}`;
6800
+ return `${readFileSync8(commonPath, "utf8")}${readFileSync8(briefPath, "utf8")}`;
6706
6801
  }
6707
6802
  async function resolveAgentId(route, role, opts) {
6708
6803
  const cached = route.resolved?.agentPersonas?.[role]?.agentId;
@@ -7323,14 +7418,14 @@ import {
7323
7418
  } from "@tablation/client";
7324
7419
 
7325
7420
  // src/skills.ts
7326
- import { readFileSync as readFileSync8, readdirSync, existsSync as existsSync13 } from "fs";
7421
+ import { readFileSync as readFileSync9, readdirSync as readdirSync2, existsSync as existsSync13 } from "fs";
7327
7422
  import { join as join12 } from "path";
7328
7423
  import { parse as parseYaml } from "yaml";
7329
7424
  import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError7, TablationApiError as TablationApiError2 } from "@tablation/client";
7330
7425
  var SkillSyncError = class extends Error {
7331
7426
  };
7332
7427
  function parseSkillFile(path) {
7333
- const text = readFileSync8(path, "utf8");
7428
+ const text = readFileSync9(path, "utf8");
7334
7429
  const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(text);
7335
7430
  if (!match) throw new SkillSyncError(`${path}: missing YAML frontmatter (expected a leading --- block)`);
7336
7431
  const frontmatter = match[1];
@@ -7345,7 +7440,7 @@ function parseSkillFile(path) {
7345
7440
  function listLocalSkills(promptsDir) {
7346
7441
  const dir = join12(promptsDir, "skills");
7347
7442
  if (!existsSync13(dir)) return [];
7348
- return readdirSync(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join12(dir, f)));
7443
+ return readdirSync2(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join12(dir, f)));
7349
7444
  }
7350
7445
  async function syncSkills(route, opts) {
7351
7446
  if (!route.resolved) {
@@ -7670,7 +7765,7 @@ function resolveRepoProject(projects, opt, routeAreaId) {
7670
7765
 
7671
7766
  // src/claude-mcp.ts
7672
7767
  import { spawnSync as spawnSync2 } from "child_process";
7673
- import { readFileSync as readFileSync9 } from "fs";
7768
+ import { readFileSync as readFileSync10 } from "fs";
7674
7769
  import { homedir as homedir4 } from "os";
7675
7770
  import { join as join13 } from "path";
7676
7771
  var MCP_SERVER_NAME = "tablation";
@@ -7685,7 +7780,7 @@ function runClaude(bin, args) {
7685
7780
  }
7686
7781
  function userScopeEntry(home) {
7687
7782
  try {
7688
- const j = JSON.parse(readFileSync9(join13(home, ".claude.json"), "utf8"));
7783
+ const j = JSON.parse(readFileSync10(join13(home, ".claude.json"), "utf8"));
7689
7784
  return j.mcpServers?.[MCP_SERVER_NAME];
7690
7785
  } catch {
7691
7786
  return void 0;
@@ -7884,14 +7979,14 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
7884
7979
  }
7885
7980
 
7886
7981
  // src/stream-sweep.ts
7887
- import { existsSync as existsSync15, readdirSync as readdirSync2, rmSync as rmSync2, statSync as statSync6 } from "fs";
7982
+ import { existsSync as existsSync15, readdirSync as readdirSync3, rmSync as rmSync2, statSync as statSync6 } from "fs";
7888
7983
  import { join as join14 } from "path";
7889
7984
  var DAY_MS = 24 * 60 * 60 * 1e3;
7890
7985
  function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
7891
7986
  const dir = join14(stateDir, "streams");
7892
7987
  if (!existsSync15(dir)) return [];
7893
7988
  const cutoff = now - retentionDays * DAY_MS;
7894
- return readdirSync2(dir).map((name) => join14(dir, name)).filter((path) => {
7989
+ return readdirSync3(dir).map((name) => join14(dir, name)).filter((path) => {
7895
7990
  try {
7896
7991
  return statSync6(path).mtimeMs < cutoff;
7897
7992
  } catch {
@@ -7918,13 +8013,13 @@ function applyStreamSweep(paths, dryRun, log) {
7918
8013
  }
7919
8014
 
7920
8015
  // src/cli.ts
7921
- import { readFileSync as readFileSync13, existsSync as existsSync19, mkdirSync as mkdirSync10, writeFileSync as writeFileSync9, unlinkSync as unlinkSync5, copyFileSync, renameSync } from "fs";
8016
+ import { readFileSync as readFileSync14, existsSync as existsSync19, mkdirSync as mkdirSync10, writeFileSync as writeFileSync9, unlinkSync as unlinkSync5, copyFileSync, renameSync } from "fs";
7922
8017
  import { createInterface } from "readline/promises";
7923
8018
  import { dirname as dirOf, resolve as resolvePath } from "path";
7924
8019
 
7925
8020
  // src/ssh-keys.ts
7926
8021
  import { execFileSync as execFileSync3 } from "child_process";
7927
- import { existsSync as existsSync16, mkdirSync as mkdirSync7, readFileSync as readFileSync10, chmodSync } from "fs";
8022
+ import { existsSync as existsSync16, mkdirSync as mkdirSync7, readFileSync as readFileSync11, chmodSync } from "fs";
7928
8023
  import { dirname as dirname8, join as join15 } from "path";
7929
8024
  var SshKeyError = class extends Error {
7930
8025
  };
@@ -7958,7 +8053,7 @@ function ensureShipSshKeypair(stateDir) {
7958
8053
  if (!existsSync16(publicKeyPath)) {
7959
8054
  throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
7960
8055
  }
7961
- const publicKey = readFileSync10(publicKeyPath, "utf8").trim();
8056
+ const publicKey = readFileSync11(publicKeyPath, "utf8").trim();
7962
8057
  return { privateKeyPath, publicKeyPath, publicKey };
7963
8058
  }
7964
8059
  function sshKeygenAvailable() {
@@ -8172,7 +8267,7 @@ function syncAllPassengerCheckouts(cfg) {
8172
8267
 
8173
8268
  // src/tunnel.ts
8174
8269
  import { spawn as spawn3 } from "child_process";
8175
- import { existsSync as existsSync17, mkdirSync as mkdirSync8, readFileSync as readFileSync11, writeFileSync as writeFileSync7, unlinkSync as unlinkSync3, readdirSync as readdirSync3 } from "fs";
8270
+ import { existsSync as existsSync17, mkdirSync as mkdirSync8, readFileSync as readFileSync12, writeFileSync as writeFileSync7, unlinkSync as unlinkSync3, readdirSync as readdirSync4 } from "fs";
8176
8271
  import { randomInt } from "crypto";
8177
8272
  import { join as join16 } from "path";
8178
8273
  var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
@@ -8247,7 +8342,7 @@ function readPersistedTunnel(stateDir, workspaceId) {
8247
8342
  const path = tunnelStatePath(stateDir, workspaceId);
8248
8343
  if (!existsSync17(path)) return void 0;
8249
8344
  try {
8250
- return JSON.parse(readFileSync11(path, "utf8"));
8345
+ return JSON.parse(readFileSync12(path, "utf8"));
8251
8346
  } catch {
8252
8347
  return void 0;
8253
8348
  }
@@ -8354,7 +8449,7 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
8354
8449
  function readAllPersistedWorkspaceIds(stateDir) {
8355
8450
  const dir = join16(stateDir, "passengers");
8356
8451
  if (!existsSync17(dir)) return [];
8357
- return readdirSync3(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
8452
+ return readdirSync4(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
8358
8453
  }
8359
8454
 
8360
8455
  // src/mcp-endpoint-registry.ts
@@ -8474,7 +8569,7 @@ async function unregisterEndpoint(workspaceId, info, d) {
8474
8569
  import { createServer as createHttpServer } from "http";
8475
8570
  import { spawn as spawn4 } from "child_process";
8476
8571
  import { randomBytes } from "crypto";
8477
- import { existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as readFileSync12, writeFileSync as writeFileSync8, unlinkSync as unlinkSync4, readdirSync as readdirSync4 } from "fs";
8572
+ import { existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as readFileSync13, writeFileSync as writeFileSync8, unlinkSync as unlinkSync4, readdirSync as readdirSync5 } from "fs";
8478
8573
  import { join as join17 } from "path";
8479
8574
 
8480
8575
  // src/secret-compare.ts
@@ -8570,7 +8665,7 @@ function readPersistedSyncDaemon(stateDir, workspaceId) {
8570
8665
  const path = syncDaemonStatePath(stateDir, workspaceId);
8571
8666
  if (!existsSync18(path)) return void 0;
8572
8667
  try {
8573
- return JSON.parse(readFileSync12(path, "utf8"));
8668
+ return JSON.parse(readFileSync13(path, "utf8"));
8574
8669
  } catch {
8575
8670
  return void 0;
8576
8671
  }
@@ -8601,7 +8696,7 @@ function readAllPersistedSyncWorkspaceIds(stateDir) {
8601
8696
  const dir = join17(stateDir, "passengers");
8602
8697
  if (!existsSync18(dir)) return [];
8603
8698
  const suffix = "-sync.json";
8604
- return readdirSync4(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
8699
+ return readdirSync5(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
8605
8700
  }
8606
8701
  function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
8607
8702
  const alive = deps.isPidAlive ?? isPidAlive;
@@ -8656,7 +8751,7 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
8656
8751
  }
8657
8752
 
8658
8753
  // src/daemon.ts
8659
- import { readdirSync as readdirSync5, statSync as statSync7 } from "fs";
8754
+ import { readdirSync as readdirSync6, statSync as statSync7 } from "fs";
8660
8755
  import { join as join18 } from "path";
8661
8756
  var DEFAULT_FLOOR_MS = 5e3;
8662
8757
  var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
@@ -8695,7 +8790,7 @@ function latestMtime(path) {
8695
8790
  if (stat.isFile()) return stat.mtimeMs;
8696
8791
  if (!stat.isDirectory()) return 0;
8697
8792
  let latest = stat.mtimeMs;
8698
- for (const entry of readdirSync5(path, { withFileTypes: true })) {
8793
+ for (const entry of readdirSync6(path, { withFileTypes: true })) {
8699
8794
  latest = Math.max(latest, latestMtime(join18(path, entry.name)));
8700
8795
  }
8701
8796
  return latest;
@@ -9374,12 +9469,21 @@ if (command === "passenger-sync-daemon") {
9374
9469
  const trimmed = typeof raw === "string" ? raw.trim() : "";
9375
9470
  return trimmed ? `${trimmed.toLowerCase()}-` : void 0;
9376
9471
  }
9377
- async function installScheduler() {
9472
+ async function installScheduler(opts = {}) {
9378
9473
  const host = hostPlatform();
9379
9474
  if (host === "windows") {
9380
9475
  process.stderr.write("crew install: no scheduler support yet for Windows (Task Scheduler is planned, not built)\n");
9381
9476
  process.exit(2);
9382
9477
  }
9478
+ if (host === "macos") {
9479
+ const ok = clearForeignLaunchdUnits(CREW_HOME, !!opts.replace, dryRun, {
9480
+ emit: (m) => process.stdout.write(`${m}
9481
+ `),
9482
+ warn: (m) => process.stderr.write(`crew install: ${m}
9483
+ `)
9484
+ });
9485
+ if (!ok) return false;
9486
+ }
9383
9487
  const installPathProblem = cfg.routes.some((r) => r.enabled && r.hostPassengers) ? dockerPathProblem(cfg.ship) : void 0;
9384
9488
  if (installPathProblem) {
9385
9489
  process.stderr.write(`crew install: Host Passengers is on but ${installPathProblem}
@@ -9431,6 +9535,7 @@ if (command === "passenger-sync-daemon") {
9431
9535
  }
9432
9536
  }
9433
9537
  }
9538
+ return true;
9434
9539
  }
9435
9540
  async function routeProblems(c) {
9436
9541
  const host = hostPlatform();
@@ -9477,14 +9582,13 @@ if (command === "passenger-sync-daemon") {
9477
9582
  for (const a of plan.actions) {
9478
9583
  if (a.kind === "enable") {
9479
9584
  if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
9480
- writeFileSync9(cfg.configFile, planEnable(readFileSync13(cfg.configFile, "utf8"), a.route));
9585
+ writeFileSync9(cfg.configFile, planEnable(readFileSync14(cfg.configFile, "utf8"), a.route));
9481
9586
  enabledSound.add(a.route);
9482
9587
  changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
9483
9588
  } else {
9484
9589
  if (enabledSound.size === 0) continue;
9485
9590
  if (prompter && !await prompter.confirm("No scheduler is installed for this ship \u2014 run `crew install` now?", false)) continue;
9486
- await installScheduler();
9487
- changed2.push("ran `crew install` (scheduler units written and loaded)");
9591
+ if (await installScheduler()) changed2.push("ran `crew install` (scheduler units written and loaded)");
9488
9592
  }
9489
9593
  }
9490
9594
  } finally {
@@ -10376,7 +10480,7 @@ ${changed2.map((c) => ` - ${c}
10376
10480
  `);
10377
10481
  if (!dryRun) {
10378
10482
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
10379
- const raw = existsSync19(resolvedPath) ? JSON.parse(readFileSync13(resolvedPath, "utf8")) : {};
10483
+ const raw = existsSync19(resolvedPath) ? JSON.parse(readFileSync14(resolvedPath, "utf8")) : {};
10380
10484
  raw.agentPersonas = personaResult.agentPersonas;
10381
10485
  raw.agentSkills = skillResult.agentSkills;
10382
10486
  mkdirSync10(dirOf(resolvedPath), { recursive: true });
@@ -10425,7 +10529,7 @@ ${changed2.map((c) => ` - ${c}
10425
10529
  `);
10426
10530
  if (!dryRun) {
10427
10531
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
10428
- const raw = existsSync19(resolvedPath) ? JSON.parse(readFileSync13(resolvedPath, "utf8")) : {};
10532
+ const raw = existsSync19(resolvedPath) ? JSON.parse(readFileSync14(resolvedPath, "utf8")) : {};
10429
10533
  raw.agentSkills = result.agentSkills;
10430
10534
  mkdirSync10(dirOf(resolvedPath), { recursive: true });
10431
10535
  writeFileSync9(resolvedPath, `${JSON.stringify(raw, null, 2)}
@@ -10441,7 +10545,7 @@ ${changed2.map((c) => ` - ${c}
10441
10545
  process.exit(2);
10442
10546
  };
10443
10547
  if (sub !== "add" && sub !== "list") usage2();
10444
- const configText = readFileSync13(cfg.configFile, "utf8");
10548
+ const configText = readFileSync14(cfg.configFile, "utf8");
10445
10549
  let routeName = positional[2];
10446
10550
  let addPath = ".";
10447
10551
  let addName;
@@ -10465,7 +10569,7 @@ ${changed2.map((c) => ` - ${c}
10465
10569
  let resolvedRemotes;
10466
10570
  if (existsSync19(resolvedFile)) {
10467
10571
  try {
10468
- resolvedRemotes = JSON.parse(readFileSync13(resolvedFile, "utf8"));
10572
+ resolvedRemotes = JSON.parse(readFileSync14(resolvedFile, "utf8"));
10469
10573
  } catch {
10470
10574
  }
10471
10575
  }
@@ -10536,7 +10640,7 @@ ${changed2.map((c) => ` - ${c}
10536
10640
  process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
10537
10641
  `);
10538
10642
  if (existsSync19(resolvedFile)) {
10539
- const raw = JSON.parse(readFileSync13(resolvedFile, "utf8"));
10643
+ const raw = JSON.parse(readFileSync14(resolvedFile, "utf8"));
10540
10644
  raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
10541
10645
  raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
10542
10646
  writeFileSync9(resolvedFile, `${JSON.stringify(raw, null, 2)}
@@ -10771,7 +10875,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
10771
10875
  const hadPreviousFile = existsSync19(resolvedPath);
10772
10876
  if (hadPreviousFile) {
10773
10877
  try {
10774
- previous = JSON.parse(readFileSync13(resolvedPath, "utf8"));
10878
+ previous = JSON.parse(readFileSync14(resolvedPath, "utf8"));
10775
10879
  } catch {
10776
10880
  }
10777
10881
  }
@@ -11274,7 +11378,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
11274
11378
  break;
11275
11379
  }
11276
11380
  case "install": {
11277
- await installScheduler();
11381
+ if (!await installScheduler({ replace: flag("replace") })) process.exit(2);
11278
11382
  break;
11279
11383
  }
11280
11384
  case "uninstall": {
@@ -11291,6 +11395,16 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
11291
11395
  emit: (m) => process.stdout.write(`${m}
11292
11396
  `),
11293
11397
  warn: (m) => process.stderr.write(`crew uninstall: ${m}
11398
+ `)
11399
+ });
11400
+ }
11401
+ if (flag("all") && host === "macos") {
11402
+ process.stdout.write(`uninstalling every crew launchd unit${dryRun ? " (dry run)" : ""}
11403
+ `);
11404
+ removeAllLaunchdUnits(dryRun, {
11405
+ emit: (m) => process.stdout.write(`${m}
11406
+ `),
11407
+ warn: (m) => process.stderr.write(`crew uninstall: ${m}
11294
11408
  `)
11295
11409
  });
11296
11410
  }
@@ -11302,7 +11416,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
11302
11416
  }
11303
11417
  case "log": {
11304
11418
  try {
11305
- const text = readFileSync13(cfg.ship.logFile, "utf8").split("\n");
11419
+ const text = readFileSync14(cfg.ship.logFile, "utf8").split("\n");
11306
11420
  process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
11307
11421
  `);
11308
11422
  } catch {
@@ -11557,6 +11671,14 @@ git: ${gitPath ? `OK (${gitPath})` : "NOT FOUND on the scheduler P
11557
11671
  process.stdout.write(`attention: ${i.kind} \u2014 ${i.message} (since ${i.since})
11558
11672
  `);
11559
11673
  }
11674
+ if (host === "macos") {
11675
+ const foreign = describeForeignLaunchdUnits(CREW_HOME);
11676
+ if (foreign.length > 0) {
11677
+ process.stdout.write(`foreign launchd units (not this checkout's \u2014 \`crew install\` or \`crew uninstall --all\` clears them):
11678
+ ${foreign.join("\n")}
11679
+ `);
11680
+ }
11681
+ }
11560
11682
  const passengerRoutes = cfg.routes.filter((r) => r.enabled && r.hostPassengers);
11561
11683
  if (passengerRoutes.length === 0) {
11562
11684
  process.stdout.write("host passengers: off (no enabled route sets hostPassengers: true)\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "A standing team of headless agents that picks work off a Tablation board, does it on a machine you control, and reports back.",
5
5
  "type": "module",
6
6
  "license": "MIT",