@rubytech/create-maxy 1.0.705 → 1.0.706

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 (74) hide show
  1. package/dist/__tests__/apt-resolve.test.js +179 -0
  2. package/dist/apt-resolve.js +73 -0
  3. package/dist/index.js +48 -46
  4. package/package.json +3 -3
  5. package/payload/platform/lib/graph-mcp/dist/__tests__/schema-cypher-parser.test.d.ts +2 -0
  6. package/payload/platform/lib/graph-mcp/dist/__tests__/schema-cypher-parser.test.d.ts.map +1 -0
  7. package/payload/platform/lib/graph-mcp/dist/__tests__/schema-cypher-parser.test.js +89 -0
  8. package/payload/platform/lib/graph-mcp/dist/__tests__/schema-cypher-parser.test.js.map +1 -0
  9. package/payload/platform/lib/graph-mcp/dist/schema-cypher-parser.d.ts +42 -0
  10. package/payload/platform/lib/graph-mcp/dist/schema-cypher-parser.d.ts.map +1 -0
  11. package/payload/platform/lib/graph-mcp/dist/schema-cypher-parser.js +87 -0
  12. package/payload/platform/lib/graph-mcp/dist/schema-cypher-parser.js.map +1 -0
  13. package/payload/platform/lib/graph-mcp/src/__tests__/schema-cypher-parser.test.ts +99 -0
  14. package/payload/platform/lib/graph-mcp/src/schema-cypher-parser.ts +84 -0
  15. package/payload/platform/plugins/admin/PLUGIN.md +1 -0
  16. package/payload/platform/plugins/admin/mcp/dist/index.js +30 -0
  17. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  18. package/payload/platform/plugins/admin/skills/business-profile/SKILL.md +2 -2
  19. package/payload/platform/plugins/admin/skills/onboarding/SKILL.md +46 -5
  20. package/payload/platform/plugins/memory/PLUGIN.md +3 -1
  21. package/payload/platform/plugins/memory/mcp/dist/index.js +56 -6
  22. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  23. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/live-schema-source.test.d.ts +2 -0
  24. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/live-schema-source.test.d.ts.map +1 -0
  25. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/live-schema-source.test.js +92 -0
  26. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/live-schema-source.test.js.map +1 -0
  27. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-loader.test.d.ts +2 -0
  28. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-loader.test.d.ts.map +1 -0
  29. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-loader.test.js +51 -0
  30. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-loader.test.js.map +1 -0
  31. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-validator.test.d.ts +2 -0
  32. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-validator.test.d.ts.map +1 -0
  33. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-validator.test.js +222 -0
  34. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-validator.test.js.map +1 -0
  35. package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.d.ts +16 -0
  36. package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.d.ts.map +1 -1
  37. package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.js +38 -11
  38. package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.js.map +1 -1
  39. package/payload/platform/plugins/memory/mcp/dist/lib/live-schema-source.d.ts +136 -0
  40. package/payload/platform/plugins/memory/mcp/dist/lib/live-schema-source.d.ts.map +1 -0
  41. package/payload/platform/plugins/memory/mcp/dist/lib/live-schema-source.js +180 -0
  42. package/payload/platform/plugins/memory/mcp/dist/lib/live-schema-source.js.map +1 -0
  43. package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.d.ts +11 -2
  44. package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.d.ts.map +1 -1
  45. package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.js +6 -3
  46. package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.js.map +1 -1
  47. package/payload/platform/plugins/memory/mcp/dist/lib/schema-validator.d.ts +44 -22
  48. package/payload/platform/plugins/memory/mcp/dist/lib/schema-validator.d.ts.map +1 -1
  49. package/payload/platform/plugins/memory/mcp/dist/lib/schema-validator.js +94 -57
  50. package/payload/platform/plugins/memory/mcp/dist/lib/schema-validator.js.map +1 -1
  51. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts +7 -5
  52. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts.map +1 -1
  53. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js +2 -2
  54. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js.map +1 -1
  55. package/payload/platform/plugins/memory/references/schema-base.md +4 -0
  56. package/payload/server/chunk-PE76FPYP.js +12040 -0
  57. package/payload/server/maxy-edge.js +1 -1
  58. package/payload/server/public/assets/{Checkbox-B2Lk8F4X.js → Checkbox-CjbS9JcG.js} +1 -1
  59. package/payload/server/public/assets/{admin-agtgi48Q.js → admin-Ce9DbUuu.js} +1 -1
  60. package/payload/server/public/assets/{data-B7nsyBTV.js → data-C-SxjLC9.js} +1 -1
  61. package/payload/server/public/assets/{file-DHWTu8LP.js → file-D4cbAAuo.js} +1 -1
  62. package/payload/server/public/assets/{graph-ChDwqqhJ.js → graph-D-Rqh0Md.js} +1 -1
  63. package/payload/server/public/assets/{house-CfjnRPO6.js → house-CYsVygEQ.js} +1 -1
  64. package/payload/server/public/assets/{jsx-runtime-81wg0w0Q.css → jsx-runtime-DPXE45W9.css} +1 -1
  65. package/payload/server/public/assets/{public-CE1kyVnz.js → public-BTOF98iO.js} +1 -1
  66. package/payload/server/public/assets/{share-2-CAd1beVT.js → share-2-B-sbkB36.js} +1 -1
  67. package/payload/server/public/assets/{useVoiceRecorder-LSAU68Eo.js → useVoiceRecorder-DLVFx3ms.js} +1 -1
  68. package/payload/server/public/assets/{x-B0xK3Aoq.js → x-BNidzSAn.js} +1 -1
  69. package/payload/server/public/data.html +6 -6
  70. package/payload/server/public/graph.html +7 -7
  71. package/payload/server/public/index.html +8 -8
  72. package/payload/server/public/public.html +5 -5
  73. package/payload/server/server.js +1 -1
  74. /package/payload/server/public/assets/{jsx-runtime-DhzH26q8.js → jsx-runtime-BUs3sHtV.js} +0 -0
@@ -0,0 +1,179 @@
1
+ // Task 638 — acceptance grid for apt-resolve.ts.
2
+ //
3
+ // Locks the four resolver branches from Task 637 plus the os-release parser
4
+ // and Candidate-line extractor. Inputs are passed directly so no mocking is
5
+ // needed — every test exercises pure logic with concrete fixtures captured
6
+ // from real devices (Noble `apt-cache policy chromium`, Bookworm ditto,
7
+ // Pop!_OS `/etc/os-release`).
8
+ //
9
+ // Runs via Node's built-in test runner; no vitest dependency. Compiles to
10
+ // dist/__tests__/apt-resolve.test.js alongside the rest of the package so
11
+ // `node --test dist/__tests__/*.test.js` picks it up after build.
12
+ import test from "node:test";
13
+ import assert from "node:assert/strict";
14
+ import { UBUNTU_ALIASES, parseOsRelease, isUbuntuLike, parseAptCacheCandidate, decideAptResolution, } from "../apt-resolve.js";
15
+ // ---------------------------------------------------------------------------
16
+ // Real fixtures — captured from devices the installer actually targets.
17
+ // ---------------------------------------------------------------------------
18
+ const UBUNTU_NOBLE_OS_RELEASE = `PRETTY_NAME="Ubuntu 24.04 LTS"
19
+ NAME="Ubuntu"
20
+ VERSION_ID="24.04"
21
+ VERSION="24.04 LTS (Noble Numbat)"
22
+ VERSION_CODENAME=noble
23
+ ID=ubuntu
24
+ ID_LIKE=debian
25
+ HOME_URL="https://www.ubuntu.com/"
26
+ UBUNTU_CODENAME=noble`;
27
+ const DEBIAN_BOOKWORM_OS_RELEASE = `PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"
28
+ NAME="Debian GNU/Linux"
29
+ VERSION_ID="12"
30
+ VERSION="12 (bookworm)"
31
+ VERSION_CODENAME=bookworm
32
+ ID=debian
33
+ HOME_URL="https://www.debian.org/"`;
34
+ const POP_OS_OS_RELEASE = `NAME="Pop!_OS"
35
+ VERSION="22.04 LTS"
36
+ ID=pop
37
+ ID_LIKE="ubuntu debian"
38
+ PRETTY_NAME="Pop!_OS 22.04 LTS"
39
+ VERSION_ID="22.04"
40
+ VERSION_CODENAME=jammy
41
+ UBUNTU_CODENAME=jammy`;
42
+ // `apt-cache policy chromium` on Ubuntu Noble (the alias case Task 637 fixed).
43
+ const NOBLE_CHROMIUM_POLICY = `chromium:
44
+ Installed: (none)
45
+ Candidate: (none)
46
+ Version table:`;
47
+ // `apt-cache policy chromium` on Debian Bookworm (concrete .deb, no alias).
48
+ const BOOKWORM_CHROMIUM_POLICY = `chromium:
49
+ Installed: (none)
50
+ Candidate: 120.0.6099.224-1~deb12u1
51
+ Version table:
52
+ 120.0.6099.224-1~deb12u1 500
53
+ 500 http://deb.debian.org/debian bookworm-security/main arm64 Packages`;
54
+ // ---------------------------------------------------------------------------
55
+ // parseOsRelease — every key/value pair lands, quoted values get unquoted.
56
+ // ---------------------------------------------------------------------------
57
+ test("parseOsRelease unquotes values and captures all keys", () => {
58
+ const noble = parseOsRelease(UBUNTU_NOBLE_OS_RELEASE);
59
+ assert.equal(noble.ID, "ubuntu");
60
+ assert.equal(noble.ID_LIKE, "debian");
61
+ assert.equal(noble.VERSION_CODENAME, "noble");
62
+ assert.equal(noble.NAME, "Ubuntu", "double-quoted values must be unquoted");
63
+ assert.equal(noble.PRETTY_NAME, "Ubuntu 24.04 LTS");
64
+ });
65
+ test("parseOsRelease handles space-separated ID_LIKE (Pop!_OS)", () => {
66
+ const pop = parseOsRelease(POP_OS_OS_RELEASE);
67
+ assert.equal(pop.ID, "pop");
68
+ assert.equal(pop.ID_LIKE, "ubuntu debian", "quoted multi-value must be preserved as a single string");
69
+ });
70
+ // ---------------------------------------------------------------------------
71
+ // isUbuntuLike — Ubuntu, Pop!_OS pick up; Debian does not.
72
+ // ---------------------------------------------------------------------------
73
+ test("isUbuntuLike accepts Ubuntu directly and via ID_LIKE", () => {
74
+ assert.equal(isUbuntuLike(parseOsRelease(UBUNTU_NOBLE_OS_RELEASE)), true, "Ubuntu Noble: ID=ubuntu");
75
+ assert.equal(isUbuntuLike(parseOsRelease(POP_OS_OS_RELEASE)), true, "Pop!_OS: ID_LIKE contains ubuntu");
76
+ assert.equal(isUbuntuLike(parseOsRelease(DEBIAN_BOOKWORM_OS_RELEASE)), false, "Debian Bookworm: not Ubuntu-like");
77
+ assert.equal(isUbuntuLike({}), false, "empty os-release (read failed): not Ubuntu-like");
78
+ });
79
+ // ---------------------------------------------------------------------------
80
+ // parseAptCacheCandidate — three states: real candidate, "(none)", absent.
81
+ // ---------------------------------------------------------------------------
82
+ test("parseAptCacheCandidate distinguishes real candidate, (none), and missing", () => {
83
+ assert.equal(parseAptCacheCandidate(BOOKWORM_CHROMIUM_POLICY), "120.0.6099.224-1~deb12u1");
84
+ assert.equal(parseAptCacheCandidate(NOBLE_CHROMIUM_POLICY), "(none)");
85
+ assert.equal(parseAptCacheCandidate(""), null, "empty stdout (spawn failed) → null");
86
+ assert.equal(parseAptCacheCandidate("totally unrelated text"), null, "no Candidate line → null");
87
+ });
88
+ // ---------------------------------------------------------------------------
89
+ // decideAptResolution — the four resolver branches from Task 637.
90
+ // ---------------------------------------------------------------------------
91
+ test("decideAptResolution: dpkg-installed package returns pkg, no log", () => {
92
+ // Branch (1): dpkg -s exits 0 — name is concrete and installed.
93
+ const r = decideAptResolution({
94
+ pkg: "curl",
95
+ dpkgInstalled: true,
96
+ aptCandidate: null,
97
+ ubuntuLike: true,
98
+ distro: "ubuntu-noble",
99
+ });
100
+ assert.equal(r.resolved, "curl");
101
+ assert.equal(r.log, null);
102
+ });
103
+ test("decideAptResolution: real apt candidate returns pkg, no log (Bookworm chromium)", () => {
104
+ // Branch (2): apt-cache policy says there IS a real candidate — leave name
105
+ // alone, the post-check will use the same name.
106
+ const r = decideAptResolution({
107
+ pkg: "chromium",
108
+ dpkgInstalled: false,
109
+ aptCandidate: "120.0.6099.224-1~deb12u1",
110
+ ubuntuLike: false,
111
+ distro: "debian-bookworm",
112
+ });
113
+ assert.equal(r.resolved, "chromium");
114
+ assert.equal(r.log, null);
115
+ });
116
+ test("decideAptResolution: Noble chromium → chromium-browser with log line", () => {
117
+ // Branch (3): the load-bearing case Task 637 fixed.
118
+ const r = decideAptResolution({
119
+ pkg: "chromium",
120
+ dpkgInstalled: false,
121
+ aptCandidate: "(none)",
122
+ ubuntuLike: true,
123
+ distro: "ubuntu-noble",
124
+ });
125
+ assert.equal(r.resolved, "chromium-browser");
126
+ assert.match(r.log ?? "", /^ apt-resolve chromium → chromium-browser \(reason=candidate-none, distro=ubuntu-noble\)$/);
127
+ });
128
+ test("decideAptResolution: Bookworm + alias-keyed pkg + candidate-none returns pkg unchanged", () => {
129
+ // Branch (4) variant: not Ubuntu-like, so even an alias-keyed pkg falls
130
+ // through. Preserves Task 634's loud-failure contract — apt-get install
131
+ // will fail, post-check throws, operator sees the real diagnostic.
132
+ const r = decideAptResolution({
133
+ pkg: "chromium",
134
+ dpkgInstalled: false,
135
+ aptCandidate: "(none)",
136
+ ubuntuLike: false,
137
+ distro: "debian-bookworm",
138
+ });
139
+ assert.equal(r.resolved, "chromium");
140
+ assert.equal(r.log, null);
141
+ });
142
+ test("decideAptResolution: Ubuntu-like + non-aliased pkg + candidate-none returns pkg unchanged", () => {
143
+ // Branch (4) variant: Ubuntu-like host but the package isn't in the alias
144
+ // map. No invented mapping — fall through and let the post-check fail loudly.
145
+ const r = decideAptResolution({
146
+ pkg: "some-package-with-no-alias",
147
+ dpkgInstalled: false,
148
+ aptCandidate: "(none)",
149
+ ubuntuLike: true,
150
+ distro: "ubuntu-noble",
151
+ });
152
+ assert.equal(r.resolved, "some-package-with-no-alias");
153
+ assert.equal(r.log, null);
154
+ });
155
+ test("decideAptResolution: aptCandidate null (policy spawn failed) still allows alias resolution", () => {
156
+ // Edge case from Task 637: if `apt-cache policy` itself fails, we still want
157
+ // alias resolution on Ubuntu-like hosts. Treat null-candidate the same as
158
+ // candidate-none for the alias decision.
159
+ const r = decideAptResolution({
160
+ pkg: "chromium",
161
+ dpkgInstalled: false,
162
+ aptCandidate: null,
163
+ ubuntuLike: true,
164
+ distro: "ubuntu-noble",
165
+ });
166
+ assert.equal(r.resolved, "chromium-browser");
167
+ assert.match(r.log ?? "", /apt-resolve chromium → chromium-browser/);
168
+ });
169
+ // ---------------------------------------------------------------------------
170
+ // UBUNTU_ALIASES — the alias map is the authoritative source. Locking its
171
+ // shape so adding a new alias is a deliberate one-line PR, not a surprise.
172
+ // ---------------------------------------------------------------------------
173
+ test("UBUNTU_ALIASES contains chromium → chromium-browser and is the only entry", () => {
174
+ assert.equal(UBUNTU_ALIASES.chromium, "chromium-browser");
175
+ // If a future change adds another alias, this test fails loudly — the
176
+ // intent is "every new alias gets a deliberate test grid case", not silent
177
+ // expansion. Bump the count and add cases when adding a new alias.
178
+ assert.equal(Object.keys(UBUNTU_ALIASES).length, 1);
179
+ });
@@ -0,0 +1,73 @@
1
+ // Task 638 — pure apt-name resolution, extracted from index.ts so a unit
2
+ // test can exercise every branch without spawning real apt/dpkg or reading
3
+ // /etc/os-release. Mirrors the port-resolution.ts pattern from Task 666:
4
+ // inputs in, decision out, no I/O. The installer wraps this with the actual
5
+ // spawnSync calls and side-effecting logFile.
6
+ //
7
+ // The original logic landed in Task 637 (chromium-noble alias). This file
8
+ // preserves that semantics verbatim — every branch maps 1:1.
9
+ // Virtual-package aliases applied when `apt-cache policy <pkg>` returns
10
+ // `Candidate: (none)`. Keyed by apt-level alias, valued by the real .deb
11
+ // name dpkg records post-install. Scoped per distro family by the caller's
12
+ // `ubuntuLike` input. Add entries here if a new alias class surfaces.
13
+ export const UBUNTU_ALIASES = {
14
+ // Noble ships `chromium` as a virtual package that resolves to
15
+ // `chromium-browser`, which is itself a transitional stub delegating to
16
+ // the snap. The runtime binary at /usr/bin/chromium is a snap symlink;
17
+ // dpkg records `chromium-browser` post-install, never `chromium`.
18
+ chromium: "chromium-browser",
19
+ };
20
+ /** Parse `/etc/os-release` content into a key→value map. */
21
+ export function parseOsRelease(raw) {
22
+ const out = {};
23
+ for (const line of raw.split("\n")) {
24
+ const m = line.match(/^([A-Z_]+)=(.*)$/);
25
+ if (m)
26
+ out[m[1]] = m[2].replace(/^"|"$/g, "");
27
+ }
28
+ return out;
29
+ }
30
+ /** True when the host is Ubuntu or Ubuntu-derivative (Pop!_OS, Mint, etc.). */
31
+ export function isUbuntuLike(os) {
32
+ if (os.ID === "ubuntu")
33
+ return true;
34
+ const likes = (os.ID_LIKE ?? "").split(/\s+/);
35
+ return likes.includes("ubuntu");
36
+ }
37
+ /**
38
+ * Extract the Candidate field from `apt-cache policy <pkg>` stdout. Returns
39
+ * the literal string after `Candidate:` (so `(none)` stays as `(none)` —
40
+ * meaningful to the resolver, not a magic value). Returns null when no
41
+ * Candidate line is present (spawn failure, malformed output, etc.).
42
+ */
43
+ export function parseAptCacheCandidate(stdout) {
44
+ const m = stdout.match(/Candidate:\s*(\S+)/);
45
+ return m ? m[1] : null;
46
+ }
47
+ /**
48
+ * Pure decision: given the dpkg/apt/os-release facts, decide which package
49
+ * name to use. Resolution order matches Task 637:
50
+ * 1. dpkg -s pkg succeeded → name is concrete and installed; return pkg.
51
+ * 2. apt-cache policy reports a real Candidate (not `(none)`) → return pkg.
52
+ * 3. Host is Ubuntu-like AND pkg is in UBUNTU_ALIASES → return the alias
53
+ * and emit the log line.
54
+ * 4. Otherwise return pkg unchanged — caller's post-check will throw,
55
+ * preserving Task 634's fail-loud contract for genuinely missing
56
+ * packages.
57
+ */
58
+ export function decideAptResolution(input) {
59
+ const { pkg, dpkgInstalled, aptCandidate, ubuntuLike, distro } = input;
60
+ if (dpkgInstalled)
61
+ return { resolved: pkg, log: null };
62
+ if (aptCandidate && aptCandidate !== "(none)") {
63
+ return { resolved: pkg, log: null };
64
+ }
65
+ if (ubuntuLike && Object.prototype.hasOwnProperty.call(UBUNTU_ALIASES, pkg)) {
66
+ const alias = UBUNTU_ALIASES[pkg];
67
+ return {
68
+ resolved: alias,
69
+ log: ` apt-resolve ${pkg} → ${alias} (reason=candidate-none, distro=${distro})`,
70
+ };
71
+ }
72
+ return { resolved: pkg, log: null };
73
+ }
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import { existsSync, mkdirSync, writeFileSync, cpSync, readFileSync, rmSync, rea
4
4
  import { resolve, join, dirname } from "node:path";
5
5
  import { randomBytes } from "node:crypto";
6
6
  import { resolveInstallPortFromFs, buildMaxyUnitFile } from "./port-resolution.js";
7
+ import { parseOsRelease, isUbuntuLike as isUbuntuLikePure, parseAptCacheCandidate, decideAptResolution, } from "./apt-resolve.js";
7
8
  const PAYLOAD_DIR = resolve(import.meta.dirname, "../payload");
8
9
  // Brand manifest — read from payload to derive all brand-specific installation values.
9
10
  // The bundler stamps brand.json into the payload at build time.
@@ -25,6 +26,14 @@ catch (err) {
25
26
  const INSTALL_DIR = resolve(process.env.HOME ?? "/root", BRAND.installDir);
26
27
  const LOG_DIR = resolve(process.env.HOME ?? "/root", BRAND.configDir, "logs");
27
28
  const LOG_FILE = join(LOG_DIR, `install-${new Date().toISOString().replace(/[:.]/g, "-")}.log`);
29
+ /** Known brand hostnames in the Maxy ecosystem. Each brand ships a main unit
30
+ * (`<hostname>.service`) and a per-brand edge unit (`<hostname>-edge.service`,
31
+ * Task 662). Peer-brand detection matches only these filenames — stale units,
32
+ * gnome-keyring disable markers, and unrelated user services are not peer
33
+ * evidence. When a third brand is added under `brands/`, append its hostname
34
+ * here AND in the matching constant in `uninstall.ts` (intentional duplication
35
+ * per `uninstall.ts:` "Shell helpers (duplicated from index.ts ...)" policy). */
36
+ const KNOWN_BRAND_HOSTNAMES = ["maxy", "realagent"];
28
37
  // The device's actual hostname — may differ from BRAND.hostname if the user customized it.
29
38
  // Updated by installSystemDeps() after hostname setup; used for user-facing URLs.
30
39
  let DEVICE_HOSTNAME = BRAND.hostname;
@@ -210,50 +219,28 @@ function canSudo() {
210
219
  // Task 637 — resolve package-name aliases before probing dpkg, so the
211
220
  // post-install check no longer false-negatives when apt resolves a virtual
212
221
  // name (e.g. Noble's `chromium` → `chromium-browser`).
213
- // Virtual-package aliases applied when `apt-cache policy <pkg>` returns
214
- // `Candidate: (none)`. Keyed by apt-level alias, valued by the real .deb
215
- // name actually recorded in dpkg after install. Scoped per distro family
216
- // by the caller in `resolveAptName`. Add entries here if a new alias class
217
- // surfaces no code surgery needed.
218
- const UBUNTU_ALIASES = {
219
- // Noble ships `chromium` as a virtual package that resolves to
220
- // `chromium-browser`, which is itself a transitional stub delegating to
221
- // the snap. The runtime binary at /usr/bin/chromium is a snap symlink;
222
- // dpkg records `chromium-browser` post-install, never `chromium`.
223
- chromium: "chromium-browser",
224
- };
222
+ // UBUNTU_ALIASES, parseOsRelease, isUbuntuLike, and decideAptResolution moved
223
+ // to ./apt-resolve.ts (Task 638). The pure logic lives there so a unit test
224
+ // can hit every branch without spawning real apt/dpkg or reading
225
+ // /etc/os-release. The thin wrappers below feed real spawn + fs results into
226
+ // that pure decision.
225
227
  function readOsRelease() {
226
228
  try {
227
- const raw = readFileSync("/etc/os-release", "utf-8");
228
- const out = {};
229
- for (const line of raw.split("\n")) {
230
- const m = line.match(/^([A-Z_]+)=(.*)$/);
231
- if (m)
232
- out[m[1]] = m[2].replace(/^"|"$/g, "");
233
- }
234
- return out;
229
+ return parseOsRelease(readFileSync("/etc/os-release", "utf-8"));
235
230
  }
236
231
  catch {
237
232
  return {};
238
233
  }
239
234
  }
240
- function isUbuntuLike() {
241
- const os = readOsRelease();
242
- if (os.ID === "ubuntu")
243
- return true;
244
- const likes = (os.ID_LIKE ?? "").split(/\s+/);
245
- return likes.includes("ubuntu");
246
- }
247
235
  /** Summarise `apt-cache policy` output for diagnostics — one token per package. */
248
236
  function aptCachePolicySummary(pkg) {
249
237
  const r = spawnSync("apt-cache", ["policy", pkg], { stdio: "pipe", encoding: "utf-8", timeout: 5_000 });
250
238
  if (r.status !== 0)
251
239
  return "policy-spawn-failed";
252
- const out = r.stdout ?? "";
253
- const cand = out.match(/Candidate:\s*(\S+)/);
254
- if (!cand)
240
+ const cand = parseAptCacheCandidate(r.stdout ?? "");
241
+ if (cand === null)
255
242
  return "no-candidate-line";
256
- return cand[1] === "(none)" ? "candidate-none" : `candidate=${cand[1]}`;
243
+ return cand === "(none)" ? "candidate-none" : `candidate=${cand}`;
257
244
  }
258
245
  /**
259
246
  * Task 637 — map an apt-level package name to the concrete name dpkg will
@@ -269,24 +256,30 @@ function aptCachePolicySummary(pkg) {
269
256
  */
270
257
  function resolveAptName(pkg) {
271
258
  const dpkg = spawnSync("dpkg", ["-s", pkg], { stdio: "pipe", timeout: 5_000 });
272
- if (dpkg.status === 0)
259
+ const dpkgInstalled = dpkg.status === 0;
260
+ // Short-circuit: when dpkg already records the name as installed, skip the
261
+ // apt-cache + os-release work — `decideAptResolution` returns pkg unchanged
262
+ // anyway and the extra spawn would burn ~10 ms per already-installed
263
+ // package across every `pkgsMissing` pass.
264
+ if (dpkgInstalled)
273
265
  return pkg;
274
266
  const policy = spawnSync("apt-cache", ["policy", pkg], {
275
267
  stdio: "pipe", encoding: "utf-8", timeout: 5_000,
276
268
  });
277
- if (policy.status === 0) {
278
- const cand = (policy.stdout ?? "").match(/Candidate:\s*(\S+)/);
279
- if (cand && cand[1] !== "(none)")
280
- return pkg;
281
- }
282
- if (isUbuntuLike() && Object.prototype.hasOwnProperty.call(UBUNTU_ALIASES, pkg)) {
283
- const alias = UBUNTU_ALIASES[pkg];
284
- const os = readOsRelease();
285
- const distro = `${os.ID ?? "unknown"}-${os.VERSION_CODENAME ?? "unknown"}`;
286
- logFile(` apt-resolve ${pkg} ${alias} (reason=candidate-none, distro=${distro})`);
287
- return alias;
288
- }
289
- return pkg;
269
+ const aptCandidate = policy.status === 0
270
+ ? parseAptCacheCandidate(policy.stdout ?? "")
271
+ : null;
272
+ const os = readOsRelease();
273
+ const decision = decideAptResolution({
274
+ pkg,
275
+ dpkgInstalled,
276
+ aptCandidate,
277
+ ubuntuLike: isUbuntuLikePure(os),
278
+ distro: `${os.ID ?? "unknown"}-${os.VERSION_CODENAME ?? "unknown"}`,
279
+ });
280
+ if (decision.log)
281
+ logFile(decision.log);
282
+ return decision.resolved;
290
283
  }
291
284
  /** Probe runtime binary presence on PATH (independent of dpkg-recorded state). */
292
285
  function commandVPath(pkg) {
@@ -432,10 +425,19 @@ function installSystemDeps() {
432
425
  // set by the user or the first installer — subsequent brands must not overwrite it.
433
426
  const systemdUserDir = resolve(process.env.HOME ?? "/root", ".config/systemd/user");
434
427
  const serviceExists = existsSync(join(systemdUserDir, BRAND.serviceName));
428
+ // Task 690: narrow peer detection to KNOWN_BRAND_HOSTNAMES. The previous
429
+ // predicate ("any *.service that isn't ours") matched stray user units
430
+ // such as a `gnome-keyring-daemon.service -> /dev/null` disable marker,
431
+ // silently flipping single-brand fresh installs into the "preserve hostname"
432
+ // branch. Mirrors Task 683's `peerBrandPresent` allowlist in `uninstall.ts`.
435
433
  let otherBrandService = false;
436
434
  if (!serviceExists) {
435
+ const peerUnits = KNOWN_BRAND_HOSTNAMES
436
+ .filter((h) => h !== BRAND.hostname)
437
+ .flatMap((h) => [`${h}.service`, `${h}-edge.service`]);
437
438
  try {
438
- otherBrandService = readdirSync(systemdUserDir).some(f => f.endsWith(".service") && f !== BRAND.serviceName);
439
+ const files = new Set(readdirSync(systemdUserDir));
440
+ otherBrandService = peerUnits.some((unit) => files.has(unit));
439
441
  }
440
442
  catch { /* directory may not exist on a fresh device — not an error */ }
441
443
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy",
3
- "version": "1.0.705",
3
+ "version": "1.0.706",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy": "./dist/index.js"
@@ -9,8 +9,8 @@
9
9
  "scripts": {
10
10
  "build": "tsc",
11
11
  "bundle": "node scripts/bundle.js",
12
- "test": "npm run build && node --test dist/__tests__/port-canonicalisation.test.js",
13
- "prepublishOnly": "node ../../platform/ui/scripts/check-route-wiring.mjs && node ../../platform/ui/scripts/check-edge-admin-routes.mjs && npm run build && chmod +x dist/index.js && npm run bundle && node ../../platform/ui/scripts/check-bundle-node-imports.mjs --dir=./payload/server/public/assets"
12
+ "test": "npm run build && node --test 'dist/__tests__/*.test.js'",
13
+ "prepublishOnly": "node ../../platform/ui/scripts/check-route-wiring.mjs && node ../../platform/ui/scripts/check-edge-admin-routes.mjs && npm run build && node --test 'dist/__tests__/*.test.js' && chmod +x dist/index.js && npm run bundle && node ../../platform/ui/scripts/check-bundle-node-imports.mjs --dir=./payload/server/public/assets"
14
14
  },
15
15
  "files": [
16
16
  "dist",
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=schema-cypher-parser.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-cypher-parser.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/schema-cypher-parser.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const node_test_1 = __importDefault(require("node:test"));
7
+ const strict_1 = __importDefault(require("node:assert/strict"));
8
+ const schema_cypher_parser_js_1 = require("../schema-cypher-parser.js");
9
+ (0, node_test_1.default)("parseLabelsFromSchemaCypher: extracts labels from constraint and index forms", () => {
10
+ const text = `
11
+ CREATE CONSTRAINT person_email IF NOT EXISTS
12
+ FOR (p:Person) REQUIRE p.email IS UNIQUE;
13
+
14
+ CREATE CONSTRAINT business_account IF NOT EXISTS
15
+ FOR (b:LocalBusiness) REQUIRE b.accountId IS UNIQUE;
16
+
17
+ CREATE INDEX admin_user_account IF NOT EXISTS
18
+ FOR (au:AdminUser) ON (au.accountId);
19
+
20
+ CREATE INDEX knowledge_doc IF NOT EXISTS
21
+ FOR (k:KnowledgeDocument) ON (k.embedding) OPTIONS { ... };
22
+ `;
23
+ const labels = (0, schema_cypher_parser_js_1.parseLabelsFromSchemaCypher)(text);
24
+ strict_1.default.deepEqual(labels, [
25
+ "AdminUser",
26
+ "KnowledgeDocument",
27
+ "LocalBusiness",
28
+ "Person",
29
+ ]);
30
+ });
31
+ (0, node_test_1.default)("parseLabelsFromSchemaCypher: deduplicates labels declared multiple times", () => {
32
+ const text = `
33
+ FOR (p:Person) REQUIRE p.email IS UNIQUE;
34
+ FOR (p:Person) REQUIRE p.telephone IS UNIQUE;
35
+ FOR (p:Person) ON (p.status);
36
+ `;
37
+ strict_1.default.deepEqual((0, schema_cypher_parser_js_1.parseLabelsFromSchemaCypher)(text), ["Person"]);
38
+ });
39
+ (0, node_test_1.default)("parseLabelsFromSchemaCypher: returns sorted output", () => {
40
+ const text = `FOR (z:Zebra) ON (z.x); FOR (a:Aardvark) ON (a.x); FOR (m:Mongoose) ON (m.x);`;
41
+ strict_1.default.deepEqual((0, schema_cypher_parser_js_1.parseLabelsFromSchemaCypher)(text), [
42
+ "Aardvark",
43
+ "Mongoose",
44
+ "Zebra",
45
+ ]);
46
+ });
47
+ (0, node_test_1.default)("parseLabelsFromSchemaCypher: ignores non-constraint cypher", () => {
48
+ const text = `
49
+ MATCH (n:Person) RETURN n;
50
+ CREATE (b:Business { name: 'X' });
51
+ FOR (p:Person) REQUIRE p.email IS UNIQUE;
52
+ `;
53
+ // MATCH/CREATE patterns don't include FOR, so they're ignored.
54
+ strict_1.default.deepEqual((0, schema_cypher_parser_js_1.parseLabelsFromSchemaCypher)(text), ["Person"]);
55
+ });
56
+ (0, node_test_1.default)("parseLabelsFromSchemaCypher: empty input returns empty array", () => {
57
+ strict_1.default.deepEqual((0, schema_cypher_parser_js_1.parseLabelsFromSchemaCypher)(""), []);
58
+ });
59
+ (0, node_test_1.default)("levenshtein: identical strings", () => {
60
+ strict_1.default.equal((0, schema_cypher_parser_js_1.levenshtein)("Person", "Person"), 0);
61
+ });
62
+ (0, node_test_1.default)("levenshtein: substitution distance", () => {
63
+ // LocaIBusiness vs LocalBusiness — capital-I-as-l typo from the incident.
64
+ strict_1.default.equal((0, schema_cypher_parser_js_1.levenshtein)("LocaIBusiness", "LocalBusiness"), 1);
65
+ });
66
+ (0, node_test_1.default)("levenshtein: insertion + deletion", () => {
67
+ strict_1.default.equal((0, schema_cypher_parser_js_1.levenshtein)("Person", "Persons"), 1);
68
+ strict_1.default.equal((0, schema_cypher_parser_js_1.levenshtein)("Persons", "Person"), 1);
69
+ });
70
+ (0, node_test_1.default)("levenshtein: handles empty strings", () => {
71
+ strict_1.default.equal((0, schema_cypher_parser_js_1.levenshtein)("", "abc"), 3);
72
+ strict_1.default.equal((0, schema_cypher_parser_js_1.levenshtein)("abc", ""), 3);
73
+ strict_1.default.equal((0, schema_cypher_parser_js_1.levenshtein)("", ""), 0);
74
+ });
75
+ (0, node_test_1.default)("nearestLabel: finds the closest match", () => {
76
+ const candidates = ["Person", "LocalBusiness", "AdminUser"];
77
+ strict_1.default.equal((0, schema_cypher_parser_js_1.nearestLabel)("LocaIBusiness", candidates), "LocalBusiness");
78
+ strict_1.default.equal((0, schema_cypher_parser_js_1.nearestLabel)("AdminUsr", candidates), "AdminUser");
79
+ });
80
+ (0, node_test_1.default)("nearestLabel: returns null when no match within maxDistance", () => {
81
+ const candidates = ["Person", "LocalBusiness"];
82
+ strict_1.default.equal((0, schema_cypher_parser_js_1.nearestLabel)("Quokka", candidates), null);
83
+ // Default maxDistance is 3; "abcde" is too far from any candidate.
84
+ strict_1.default.equal((0, schema_cypher_parser_js_1.nearestLabel)("abcde", candidates, 1), null);
85
+ });
86
+ (0, node_test_1.default)("nearestLabel: empty candidates returns null", () => {
87
+ strict_1.default.equal((0, schema_cypher_parser_js_1.nearestLabel)("Person", []), null);
88
+ });
89
+ //# sourceMappingURL=schema-cypher-parser.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-cypher-parser.test.js","sourceRoot":"","sources":["../../src/__tests__/schema-cypher-parser.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Shared parser for `platform/neo4j/schema.cypher`.
3
+ *
4
+ * Two consumers today:
5
+ * 1. The admin SCHEMA prompt block (`platform/ui/app/lib/admin-schema-block.ts`,
6
+ * Task 703) renders labels declared in schema.cypher into the agent's
7
+ * system prompt.
8
+ * 2. The memory-plugin schema validator (Task 736) treats schema.cypher
9
+ * declarations as the "promise" half of the recognised-label set —
10
+ * labels that exist as constraints/indexes but may not yet have a node
11
+ * in the graph (fresh-install bootstrap).
12
+ *
13
+ * The previous home was admin-schema-block.ts; that owner was wrong as
14
+ * soon as a second consumer arrived. Lifting here keeps a single source of
15
+ * truth and removes the cross-package shape coupling.
16
+ *
17
+ * Also exports a compact `levenshtein` for "did you mean?" suggestions on
18
+ * unknown labels — used by the memory-plugin validator. The graph-mcp
19
+ * cypher validator has its own copy in schema-cache.ts (private to the
20
+ * stale-miss heuristic); the duplication is intentional, since this
21
+ * exported one targets human-readable error UX, not refresh debouncing.
22
+ */
23
+ /**
24
+ * Extract every Neo4j label declared in a `schema.cypher` file by scanning
25
+ * `FOR (alias:Label)` constraint and index forms. Returns a sorted, de-duped
26
+ * list. Pure function — no I/O.
27
+ */
28
+ export declare function parseLabelsFromSchemaCypher(schemaCypher: string): string[];
29
+ /**
30
+ * Standard iterative-DP Levenshtein. Used to suggest a near-match when an
31
+ * agent submits an unknown label — small alphabets, short strings, runs in
32
+ * microseconds. Identical algorithm to the private copy in schema-cache.ts;
33
+ * exported here so callers outside graph-mcp don't have to roll their own.
34
+ */
35
+ export declare function levenshtein(a: string, b: string): number;
36
+ /**
37
+ * Find the closest label in `candidates` to `unknown` by edit distance.
38
+ * Returns null when the closest match is further than `maxDistance` (default
39
+ * 3) — beyond that a suggestion is more confusing than helpful.
40
+ */
41
+ export declare function nearestLabel(unknown: string, candidates: Iterable<string>, maxDistance?: number): string | null;
42
+ //# sourceMappingURL=schema-cypher-parser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-cypher-parser.d.ts","sourceRoot":"","sources":["../src/schema-cypher-parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAIH;;;;GAIG;AACH,wBAAgB,2BAA2B,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,EAAE,CAM1E;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAgBxD;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,EAC5B,WAAW,SAAI,GACd,MAAM,GAAG,IAAI,CAYf"}