@rubytech/create-maxy-code 0.1.388 → 0.1.391

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 (121) hide show
  1. package/dist/__tests__/claude-bin.test.js +51 -0
  2. package/dist/__tests__/claude-upgrade-privilege.test.js +17 -0
  3. package/dist/__tests__/joblogic-excluded.test.js +39 -0
  4. package/dist/__tests__/launchd-plist.test.js +36 -1
  5. package/dist/__tests__/macos-darwin-branch.test.js +18 -0
  6. package/dist/__tests__/neo4j-datadir.test.js +14 -0
  7. package/dist/claude-bin.js +78 -0
  8. package/dist/claude-upgrade-privilege.js +21 -0
  9. package/dist/index.js +262 -34
  10. package/dist/launchd-plist.js +25 -9
  11. package/dist/neo4j-datadir.js +15 -0
  12. package/package.json +1 -1
  13. package/payload/platform/config/brand.json +1 -1
  14. package/payload/platform/plugins/.claude-plugin/marketplace.json +0 -5
  15. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +24 -5
  16. package/payload/platform/plugins/docs/references/admin-session.md +4 -0
  17. package/payload/platform/plugins/docs/references/admin-ui.md +18 -1
  18. package/payload/platform/plugins/docs/references/joblogic.md +2 -0
  19. package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +2 -0
  20. package/payload/platform/plugins/work/PLUGIN.md +3 -0
  21. package/payload/platform/plugins/work/mcp/dist/__tests__/session-metering.test.d.ts +2 -0
  22. package/payload/platform/plugins/work/mcp/dist/__tests__/session-metering.test.d.ts.map +1 -0
  23. package/payload/platform/plugins/work/mcp/dist/__tests__/session-metering.test.js +98 -0
  24. package/payload/platform/plugins/work/mcp/dist/__tests__/session-metering.test.js.map +1 -0
  25. package/payload/platform/plugins/work/mcp/dist/index.js +16 -0
  26. package/payload/platform/plugins/work/mcp/dist/index.js.map +1 -1
  27. package/payload/platform/plugins/work/mcp/dist/tools/session-metering.d.ts +53 -0
  28. package/payload/platform/plugins/work/mcp/dist/tools/session-metering.d.ts.map +1 -0
  29. package/payload/platform/plugins/work/mcp/dist/tools/session-metering.js +80 -0
  30. package/payload/platform/plugins/work/mcp/dist/tools/session-metering.js.map +1 -0
  31. package/payload/platform/plugins/work/mcp/package.json +2 -1
  32. package/payload/platform/scripts/smoke-boot-services.sh +35 -10
  33. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
  34. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +1 -0
  35. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  36. package/payload/platform/services/claude-session-manager/dist/claude-host-creds.d.ts +11 -0
  37. package/payload/platform/services/claude-session-manager/dist/claude-host-creds.d.ts.map +1 -0
  38. package/payload/platform/services/claude-session-manager/dist/claude-host-creds.js +37 -0
  39. package/payload/platform/services/claude-session-manager/dist/claude-host-creds.js.map +1 -0
  40. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  41. package/payload/platform/services/claude-session-manager/dist/http-server.js +98 -8
  42. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  43. package/payload/platform/services/claude-session-manager/dist/pricing.d.ts +45 -0
  44. package/payload/platform/services/claude-session-manager/dist/pricing.d.ts.map +1 -0
  45. package/payload/platform/services/claude-session-manager/dist/pricing.js +57 -0
  46. package/payload/platform/services/claude-session-manager/dist/pricing.js.map +1 -0
  47. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  48. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +7 -0
  49. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  50. package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts +1 -1
  51. package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts.map +1 -1
  52. package/payload/platform/services/claude-session-manager/dist/rc-daemon.js +5 -1
  53. package/payload/platform/services/claude-session-manager/dist/rc-daemon.js.map +1 -1
  54. package/payload/platform/services/claude-session-manager/dist/session-metering.d.ts +38 -0
  55. package/payload/platform/services/claude-session-manager/dist/session-metering.d.ts.map +1 -0
  56. package/payload/platform/services/claude-session-manager/dist/session-metering.js +292 -0
  57. package/payload/platform/services/claude-session-manager/dist/session-metering.js.map +1 -0
  58. package/payload/platform/templates/specialists/agents/project-manager.md +1 -1
  59. package/payload/server/public/assets/{AdminLoginScreens-BejIjbmU.js → AdminLoginScreens-CL27ZV86.js} +1 -1
  60. package/payload/server/public/assets/AdminShell-CBcSh9Yd.js +1 -0
  61. package/payload/server/public/assets/{Checkbox-1F1tzqca.js → Checkbox-DRIcYN9r.js} +1 -1
  62. package/payload/server/public/assets/{Transcript-DkGa4CQH.js → Transcript-CRc72hkG.js} +1 -1
  63. package/payload/server/public/assets/{admin-DqQARkjy.js → admin-iKYYnv3w.js} +1 -1
  64. package/payload/server/public/assets/{browser-nDtEK6RC.js → browser--8gzAe-E.js} +1 -1
  65. package/payload/server/public/assets/calendar-7xAT81N_.js +1 -0
  66. package/payload/server/public/assets/chat-B8Z9W42Z.js +1 -0
  67. package/payload/server/public/assets/chevron-left-DDselYau.js +1 -0
  68. package/payload/server/public/assets/data-Ece320dZ.js +1 -0
  69. package/payload/server/public/assets/{graph-DFyicKd7.js → graph-B3haz2DN.js} +2 -2
  70. package/payload/server/public/assets/{graph-labels-D3BQdcpD.js → graph-labels-Ba_fyw6I.js} +1 -1
  71. package/payload/server/public/assets/{operator-BxrjWdT9.js → operator-BYL1IFz9.js} +1 -1
  72. package/payload/server/public/assets/page-Bj6lB07z.js +32 -0
  73. package/payload/server/public/assets/page-CLKVCquw.js +1 -0
  74. package/payload/server/public/assets/{public-DbdqfdYq.js → public-BKJUKC76.js} +1 -1
  75. package/payload/server/public/assets/{rotate-ccw-BkX8HODe.js → rotate-ccw-BypBbSI4.js} +1 -1
  76. package/payload/server/public/assets/useSubAccountSwitcher-4qilMyPX.css +1 -0
  77. package/payload/server/public/assets/useSubAccountSwitcher-CsRv6MEd.js +9 -0
  78. package/payload/server/public/browser.html +4 -4
  79. package/payload/server/public/calendar.html +5 -5
  80. package/payload/server/public/chat.html +8 -8
  81. package/payload/server/public/data.html +7 -7
  82. package/payload/server/public/graph.html +8 -8
  83. package/payload/server/public/index.html +10 -10
  84. package/payload/server/public/operator.html +10 -10
  85. package/payload/server/public/public.html +8 -8
  86. package/payload/server/server.js +327 -255
  87. package/payload/platform/plugins/joblogic/.claude-plugin/plugin.json +0 -21
  88. package/payload/platform/plugins/joblogic/PLUGIN.md +0 -182
  89. package/payload/platform/plugins/joblogic/lib/mcp-spawn-tee/index.js +0 -193
  90. package/payload/platform/plugins/joblogic/lib/mcp-spawn-tee/package.json +0 -3
  91. package/payload/platform/plugins/joblogic/mcp/dist/index.d.ts +0 -2
  92. package/payload/platform/plugins/joblogic/mcp/dist/index.d.ts.map +0 -1
  93. package/payload/platform/plugins/joblogic/mcp/dist/index.js +0 -229
  94. package/payload/platform/plugins/joblogic/mcp/dist/index.js.map +0 -1
  95. package/payload/platform/plugins/joblogic/mcp/dist/lib/auth.d.ts +0 -31
  96. package/payload/platform/plugins/joblogic/mcp/dist/lib/auth.d.ts.map +0 -1
  97. package/payload/platform/plugins/joblogic/mcp/dist/lib/auth.js +0 -78
  98. package/payload/platform/plugins/joblogic/mcp/dist/lib/auth.js.map +0 -1
  99. package/payload/platform/plugins/joblogic/mcp/dist/lib/client.d.ts +0 -35
  100. package/payload/platform/plugins/joblogic/mcp/dist/lib/client.d.ts.map +0 -1
  101. package/payload/platform/plugins/joblogic/mcp/dist/lib/client.js +0 -106
  102. package/payload/platform/plugins/joblogic/mcp/dist/lib/client.js.map +0 -1
  103. package/payload/platform/plugins/joblogic/mcp/dist/lib/idempotency.d.ts +0 -8
  104. package/payload/platform/plugins/joblogic/mcp/dist/lib/idempotency.d.ts.map +0 -1
  105. package/payload/platform/plugins/joblogic/mcp/dist/lib/idempotency.js +0 -41
  106. package/payload/platform/plugins/joblogic/mcp/dist/lib/idempotency.js.map +0 -1
  107. package/payload/platform/plugins/joblogic/mcp/dist/lib/secrets.d.ts +0 -21
  108. package/payload/platform/plugins/joblogic/mcp/dist/lib/secrets.d.ts.map +0 -1
  109. package/payload/platform/plugins/joblogic/mcp/dist/lib/secrets.js +0 -47
  110. package/payload/platform/plugins/joblogic/mcp/dist/lib/secrets.js.map +0 -1
  111. package/payload/platform/plugins/joblogic/mcp/package.json +0 -10
  112. package/payload/platform/plugins/joblogic/skills/joblogic/SKILL.md +0 -32
  113. package/payload/server/public/assets/AdminShell-D2-uBSB5.js +0 -1
  114. package/payload/server/public/assets/calendar-CO4Bwmho.js +0 -1
  115. package/payload/server/public/assets/chat-DeIge_bC.js +0 -1
  116. package/payload/server/public/assets/chevron-left-DhVdq0aN.js +0 -1
  117. package/payload/server/public/assets/data-B1GIdzHk.js +0 -1
  118. package/payload/server/public/assets/page-ByDLq_o1.js +0 -1
  119. package/payload/server/public/assets/page-D-Ep4bXd.js +0 -32
  120. package/payload/server/public/assets/useSubAccountSwitcher-DMbRhLPv.js +0 -9
  121. package/payload/server/public/assets/useSubAccountSwitcher-DS0iqSkP.css +0 -1
@@ -0,0 +1,51 @@
1
+ // acceptance grid for claude-bin.ts (Task 1395 gap 4).
2
+ //
3
+ // Pure resolver: version parse/compare, canonical pick (highest version),
4
+ // and PATH enumeration with injected fs/spawn so no real binary is probed.
5
+ // Runs under node --test alongside the rest of the package.
6
+ import test from "node:test";
7
+ import assert from "node:assert/strict";
8
+ import { pickCanonicalClaude, compareVersions, parseClaudeVersion, enumerateClaudeCandidates, } from "../claude-bin.js";
9
+ test("parseClaudeVersion extracts leading version", () => {
10
+ assert.equal(parseClaudeVersion("2.1.201 (Claude Code)"), "2.1.201");
11
+ assert.equal(parseClaudeVersion(" 2.1.153 (Claude Code)\n"), "2.1.153");
12
+ assert.equal(parseClaudeVersion("not installed"), null);
13
+ });
14
+ test("compareVersions orders by numeric parts", () => {
15
+ assert.ok(compareVersions("2.1.201", "2.1.153") > 0);
16
+ assert.ok(compareVersions("2.1.153", "2.1.201") < 0);
17
+ assert.equal(compareVersions("2.1.1", "2.1.1"), 0);
18
+ assert.ok(compareVersions("2.2.0", "2.1.999") > 0);
19
+ });
20
+ test("pickCanonicalClaude picks highest version (nvm over stale Cask)", () => {
21
+ const pick = pickCanonicalClaude([
22
+ { path: "/usr/local/bin/claude", version: "2.1.153" },
23
+ { path: "/Users/x/.nvm/versions/node/v22/bin/claude", version: "2.1.201" },
24
+ ]);
25
+ assert.equal(pick, "/Users/x/.nvm/versions/node/v22/bin/claude");
26
+ });
27
+ test("pickCanonicalClaude: empty -> null, ties -> first", () => {
28
+ assert.equal(pickCanonicalClaude([]), null);
29
+ assert.equal(pickCanonicalClaude([
30
+ { path: "/a/claude", version: "2.1.1" },
31
+ { path: "/b/claude", version: "2.1.1" },
32
+ ]), "/a/claude");
33
+ });
34
+ test("enumerateClaudeCandidates dedupes by realpath, skips non-exec/unversioned", () => {
35
+ const versions = {
36
+ "/opt/homebrew/bin/claude": "2.1.201 (Claude Code)",
37
+ "/usr/local/bin/claude": "2.1.153 (Claude Code)",
38
+ };
39
+ const deps = {
40
+ isExecutable: (p) => p === "/opt/homebrew/bin/claude" || p === "/usr/local/bin/claude" || p === "/dup/claude",
41
+ // /dup/claude is a symlink to the homebrew one -> deduped away
42
+ realpath: (p) => (p === "/dup/claude" ? "/opt/homebrew/bin/claude" : p),
43
+ runVersion: (p) => versions[p] ?? null,
44
+ };
45
+ const cands = enumerateClaudeCandidates("/opt/homebrew/bin:/usr/local/bin:/dup:/empty", deps);
46
+ assert.deepEqual(cands, [
47
+ { path: "/opt/homebrew/bin/claude", version: "2.1.201" },
48
+ { path: "/usr/local/bin/claude", version: "2.1.153" },
49
+ ]);
50
+ assert.equal(pickCanonicalClaude(cands), "/opt/homebrew/bin/claude");
51
+ });
@@ -0,0 +1,17 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { decideClaudeUpgradePrivilege } from "../claude-upgrade-privilege.js";
4
+ test("darwin: writable user-owned prefix runs the upgrade without sudo", () => {
5
+ assert.equal(decideClaudeUpgradePrivilege({ platform: "darwin", prefixWritable: true, canSudo: false }), "run-no-sudo");
6
+ assert.equal(decideClaudeUpgradePrivilege({ platform: "darwin", prefixWritable: true, canSudo: true }), "run-no-sudo");
7
+ });
8
+ test("darwin: non-writable prefix falls back to sudo when available, else skips", () => {
9
+ assert.equal(decideClaudeUpgradePrivilege({ platform: "darwin", prefixWritable: false, canSudo: true }), "run-sudo");
10
+ assert.equal(decideClaudeUpgradePrivilege({ platform: "darwin", prefixWritable: false, canSudo: false }), "skip");
11
+ });
12
+ test("linux: unchanged — sudo when available, skip when not; prefixWritable is ignored", () => {
13
+ assert.equal(decideClaudeUpgradePrivilege({ platform: "linux", prefixWritable: false, canSudo: true }), "run-sudo");
14
+ assert.equal(decideClaudeUpgradePrivilege({ platform: "linux", prefixWritable: false, canSudo: false }), "skip");
15
+ // prefixWritable must not change the Linux decision (root-owned global prefix assumed).
16
+ assert.equal(decideClaudeUpgradePrivilege({ platform: "linux", prefixWritable: true, canSudo: false }), "skip");
17
+ });
@@ -0,0 +1,39 @@
1
+ // Task 1386: the `joblogic` connector plugin is suspended by exclusion. Its skill
2
+ // triggers on the generic word "jobs" and its tools are all adminAllowlist, so an
3
+ // admin asking about jobs is mis-routed to a connector that ships inert and is used
4
+ // by zero accounts — colliding head-on with SiteDesk's `Job` ontology. Suspension is
5
+ // uniform across every brand: each brand.json must carry "joblogic" in
6
+ // plugins.excluded so the bundler drops it from the payload and it never registers as
7
+ // a native CC plugin. This guard fails loud if any brand drops the exclusion.
8
+ //
9
+ // Runs via Node's built-in test runner (codebase convention). Path resolution mirrors
10
+ // known-brand-hostnames.test.ts.
11
+ import test from "node:test";
12
+ import assert from "node:assert/strict";
13
+ import { readdirSync, readFileSync, existsSync } from "node:fs";
14
+ import { fileURLToPath } from "node:url";
15
+ import { dirname, resolve, join } from "node:path";
16
+ // dist/__tests__/joblogic-excluded.test.js → maxy-code/brands
17
+ const here = dirname(fileURLToPath(import.meta.url));
18
+ const BRANDS_DIR = resolve(here, "../../../../brands");
19
+ function shippedBrands() {
20
+ return readdirSync(BRANDS_DIR, { withFileTypes: true })
21
+ .filter((e) => e.isDirectory())
22
+ .map((e) => join(BRANDS_DIR, e.name, "brand.json"))
23
+ .filter((p) => existsSync(p))
24
+ .map((p) => {
25
+ const parsed = JSON.parse(readFileSync(p, "utf-8"));
26
+ return { name: parsed.hostname, excluded: (parsed.plugins?.excluded ?? []) };
27
+ });
28
+ }
29
+ test("brands/ directory is discoverable and non-empty", () => {
30
+ assert.ok(existsSync(BRANDS_DIR), `brands dir not found at ${BRANDS_DIR}`);
31
+ assert.ok(shippedBrands().length > 0, "no shipped brand.json found");
32
+ });
33
+ test("every shipped brand suspends joblogic via plugins.excluded", () => {
34
+ for (const { name, excluded } of shippedBrands()) {
35
+ assert.ok(excluded.includes("joblogic"), `brand "${name}" does not list "joblogic" in plugins.excluded; the joblogic ` +
36
+ `connector would ship in its payload and register as a native CC plugin, ` +
37
+ `hijacking generic "jobs" queries (Task 1386)`);
38
+ }
39
+ });
@@ -10,7 +10,7 @@
10
10
  // package so `node --test dist/__tests__/*.test.js` picks it up after build.
11
11
  import test from "node:test";
12
12
  import assert from "node:assert/strict";
13
- import { renderPlist } from "../launchd-plist.js";
13
+ import { renderPlist, buildSessionManagerWrapper } from "../launchd-plist.js";
14
14
  // ---------------------------------------------------------------------------
15
15
  // Standard render — every required field present, no escaping needed.
16
16
  // ---------------------------------------------------------------------------
@@ -193,3 +193,38 @@ test("renderPlist omits WorkingDirectory key when workingDirectory is absent", (
193
193
  assert.match(xml, /<key>KeepAlive<\/key>/);
194
194
  assert.match(xml, /<key>RunAtLoad<\/key>/);
195
195
  });
196
+ // ---------------------------------------------------------------------------
197
+ // buildSessionManagerWrapper — Task 1395 gap 2
198
+ // ---------------------------------------------------------------------------
199
+ test("buildSessionManagerWrapper sources .env and execs the manager", () => {
200
+ const w = buildSessionManagerWrapper({
201
+ envPath: "/Users/x/.maxy/.env",
202
+ installDir: "/Users/x/maxy-code",
203
+ nodeBin: "/usr/local/bin/node",
204
+ persistDir: "/Users/x/.maxy",
205
+ brandPort: 19200,
206
+ });
207
+ assert.match(w, /^#!\/bin\/bash\n/);
208
+ assert.match(w, /set -a; \[ -f "\/Users\/x\/\.maxy\/\.env" \] && \. "\/Users\/x\/\.maxy\/\.env"; set \+a/);
209
+ assert.match(w, /export CLAUDE_SESSION_MANAGER_PERSIST_DIR="\/Users\/x\/\.maxy"/);
210
+ assert.match(w, /export PLATFORM_ROOT="\/Users\/x\/maxy-code\/platform"/);
211
+ assert.match(w, /export MAXY_BRAND_PORT="19200"/);
212
+ assert.match(w, /export PLATFORM_PORT="19200"/);
213
+ assert.match(w, /cd "\/Users\/x\/maxy-code\/platform\/services\/claude-session-manager"/);
214
+ assert.match(w, /exec \/usr\/local\/bin\/node dist\/index\.js\n$/);
215
+ });
216
+ test("session-manager plist renders bash wrapper as sole ProgramArguments", () => {
217
+ const xml = renderPlist({
218
+ label: "com.rubytech.maxy-code-claude-session-manager",
219
+ programArguments: ["/bin/bash", "/Users/x/.maxy/claude-session-manager-wrapper.sh"],
220
+ stdoutPath: "/Users/x/.maxy/logs/server.log",
221
+ stderrPath: "/Users/x/.maxy/logs/server.log",
222
+ keepAlive: true,
223
+ runAtLoad: true,
224
+ workingDirectory: "/Users/x/maxy-code/platform/services/claude-session-manager",
225
+ });
226
+ assert.match(xml, /<string>com\.rubytech\.maxy-code-claude-session-manager<\/string>/);
227
+ assert.match(xml, /<string>\/bin\/bash<\/string>\s*<string>\/Users\/x\/\.maxy\/claude-session-manager-wrapper\.sh<\/string>/);
228
+ assert.match(xml, /<key>KeepAlive<\/key>\s*<true\/>/);
229
+ assert.match(xml, /<key>RunAtLoad<\/key>\s*<true\/>/);
230
+ });
@@ -81,3 +81,21 @@ test("uninstall darwin path bootsout the LaunchAgent and removes the plist", ()
81
81
  // brand-keyed — install + uninstall both depend on the same label shape.
82
82
  assert.match(SRC, /function launchdLabel\(\):\s*string\s*\{\s*return\s*`com\.rubytech\.\$\{BRAND\.hostname\}`/);
83
83
  });
84
+ test("resetNeo4jAuth has a darwin branch that uses brew services and no sudo", () => {
85
+ // On darwin Neo4j runs under a user launchd agent (brew services) and its
86
+ // datadir is user-owned — no systemctl, no sudo. Task 1394. Static-grep, not
87
+ // behaviour: resetNeo4jAuth spawns brew/neo4j-admin and can't run headless.
88
+ const fn = SRC.slice(SRC.indexOf("function resetNeo4jAuth"), SRC.indexOf("function redactInstallLogs"));
89
+ assert.match(fn, /platform === "darwin"/, "resetNeo4jAuth must branch on darwin");
90
+ assert.match(fn, /brew["']?,\s*\[["']services["']\s*,\s*["']stop["']/, "darwin branch stops neo4j via brew services");
91
+ assert.match(fn, /brew["']?,\s*\[["']services["']\s*,\s*["']start["']/, "darwin branch starts neo4j via brew services");
92
+ assert.match(fn, /neo4j-darwin-auth-reset/, "darwin branch emits the reset observability marker");
93
+ // The darwin branch must NOT shell out to systemctl or sudo (that path is
94
+ // Linux-only). Match invocation shapes — quoted command literals and the
95
+ // `sudo: true` option — not prose, so the branch's own explanatory comment
96
+ // ("no systemctl, no sudo") does not defeat the assertion.
97
+ const darwinBranch = fn.slice(fn.indexOf('platform === "darwin"'), fn.indexOf("} else {"));
98
+ assert.ok(!/["']systemctl["']/.test(darwinBranch), "darwin branch must not invoke systemctl");
99
+ assert.ok(!/["']sudo["']/.test(darwinBranch), "darwin branch must not spawn sudo");
100
+ assert.ok(!/sudo:\s*true/.test(darwinBranch), "darwin branch must not pass { sudo: true } on the user-owned datadir");
101
+ });
@@ -0,0 +1,14 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { resolveNeo4jDataDir } from "../neo4j-datadir.js";
4
+ test("darwin resolves the Homebrew datadir under the brew prefix (Intel + Apple Silicon)", () => {
5
+ assert.equal(resolveNeo4jDataDir({ platform: "darwin", brewPrefix: "/usr/local", dedicated: false, hostname: "maxy-code" }), "/usr/local/var/neo4j");
6
+ assert.equal(resolveNeo4jDataDir({ platform: "darwin", brewPrefix: "/opt/homebrew", dedicated: false, hostname: "maxy-code" }), "/opt/homebrew/var/neo4j");
7
+ });
8
+ test("darwin ignores dedicated — only the shared Homebrew instance exists on darwin", () => {
9
+ assert.equal(resolveNeo4jDataDir({ platform: "darwin", brewPrefix: "/usr/local", dedicated: true, hostname: "realagent-code" }), "/usr/local/var/neo4j");
10
+ });
11
+ test("linux shared and dedicated datadirs are unchanged", () => {
12
+ assert.equal(resolveNeo4jDataDir({ platform: "linux", brewPrefix: "", dedicated: false, hostname: "maxy-code" }), "/var/lib/neo4j");
13
+ assert.equal(resolveNeo4jDataDir({ platform: "linux", brewPrefix: "", dedicated: true, hostname: "realagent" }), "/var/lib/neo4j-realagent");
14
+ });
@@ -0,0 +1,78 @@
1
+ // Task 1395 gap 4 — resolve one canonical `claude` binary.
2
+ //
3
+ // A Mac can carry more than one `claude` on PATH at different versions — e.g.
4
+ // an npm-global (nvm/Homebrew node) claude AND a Homebrew Cask `claude-code` at
5
+ // /usr/local/bin/claude. Whichever the runtime PATH hits first wins, which
6
+ // during the Task 1393 migration was the STALE Cask (2.1.153) rather than the
7
+ // logged-in nvm binary (2.1.201). The installer resolves a single canonical
8
+ // path and persists it as CLAUDE_BIN in `.env`; every runtime spawn then uses
9
+ // that exact binary instead of a PATH lottery.
10
+ //
11
+ // Canonical = highest version. This is deterministic and picks the newest
12
+ // claude, which is the defensible choice when two are present. All fs/spawn I/O
13
+ // is injected so the resolver stays pure and unit-testable.
14
+ import { join } from "node:path";
15
+ /** Extract the leading dotted-numeric version from `claude --version` output,
16
+ * e.g. "2.1.201 (Claude Code)" -> "2.1.201". Returns null when absent. */
17
+ export function parseClaudeVersion(raw) {
18
+ const m = raw.trim().match(/(\d+(?:\.\d+)+)/);
19
+ return m ? m[1] : null;
20
+ }
21
+ function toParts(v) {
22
+ const m = v.trim().match(/^(\d+(?:\.\d+)*)/);
23
+ return m ? m[1].split(".").map((n) => parseInt(n, 10)) : [];
24
+ }
25
+ /** Numeric dotted-version compare (a<b -> <0, a>b -> >0). Missing parts = 0. */
26
+ export function compareVersions(a, b) {
27
+ const pa = toParts(a);
28
+ const pb = toParts(b);
29
+ const len = Math.max(pa.length, pb.length);
30
+ for (let i = 0; i < len; i++) {
31
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
32
+ if (d !== 0)
33
+ return d;
34
+ }
35
+ return 0;
36
+ }
37
+ /** Pick the highest-version candidate's path; first-wins on ties. Null when the
38
+ * list is empty. */
39
+ export function pickCanonicalClaude(candidates) {
40
+ if (candidates.length === 0)
41
+ return null;
42
+ let best = candidates[0];
43
+ for (let i = 1; i < candidates.length; i++) {
44
+ if (compareVersions(candidates[i].version, best.version) > 0)
45
+ best = candidates[i];
46
+ }
47
+ return best.path;
48
+ }
49
+ /** Enumerate every distinct `claude` reachable from `pathEnv` (colon-separated
50
+ * PATH), deduped by realpath, each probed for its version. Non-executable
51
+ * entries, symlink duplicates, and version-probe failures are skipped. */
52
+ export function enumerateClaudeCandidates(pathEnv, deps) {
53
+ const seen = new Set();
54
+ const out = [];
55
+ for (const dir of pathEnv.split(":").filter(Boolean)) {
56
+ const cand = join(dir, "claude");
57
+ if (!deps.isExecutable(cand))
58
+ continue;
59
+ let real;
60
+ try {
61
+ real = deps.realpath(cand);
62
+ }
63
+ catch {
64
+ real = cand;
65
+ }
66
+ if (seen.has(real))
67
+ continue;
68
+ seen.add(real);
69
+ const raw = deps.runVersion(cand);
70
+ if (!raw)
71
+ continue;
72
+ const version = parseClaudeVersion(raw);
73
+ if (!version)
74
+ continue;
75
+ out.push({ path: cand, version });
76
+ }
77
+ return out;
78
+ }
@@ -0,0 +1,21 @@
1
+ // Pure decision for whether the phase-3 `npm install -g @anthropic-ai/claude-code`
2
+ // upgrade runs under sudo, runs unprivileged, or is skipped. Peer of
3
+ // apt-resolve.ts / brew-resolve.ts: no spawnSync, no fs — the caller injects the
4
+ // probed inputs and applies the result.
5
+ //
6
+ // Root cause this encodes: on darwin the npm global prefix (nvm or Homebrew) is
7
+ // user-owned, so `npm install -g` needs no sudo; wrapping it in sudo forces a
8
+ // non-interactive password prompt that hard-fails. On Linux the global prefix is
9
+ // root-owned by default, so the existing sudo-or-skip behaviour is preserved
10
+ // unchanged (prefixWritable is not consulted on Linux).
11
+ export function decideClaudeUpgradePrivilege(input) {
12
+ if (input.platform === "darwin") {
13
+ if (input.prefixWritable)
14
+ return "run-no-sudo";
15
+ if (input.canSudo)
16
+ return "run-sudo";
17
+ return "skip";
18
+ }
19
+ // linux: unchanged from the historical `isLinux() && !canSudo()` skip.
20
+ return input.canSudo ? "run-sudo" : "skip";
21
+ }