@hybridlabor-api/aos 4.16.0 → 4.18.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 (147) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/.claude/hooks/aos-bus.mjs +8 -4
  3. package/.claude/hooks/env-file-protection.mjs +19 -1
  4. package/.claude/hooks/go-gate.mjs +788 -69
  5. package/.claude/hooks/go-grant.mjs +88 -0
  6. package/.claude/hooks/go-token.mjs +17 -2
  7. package/.claude/hooks/memb-inject.mjs +61 -37
  8. package/.claude/settings.json +8 -0
  9. package/.codex-plugin/plugin.json +36 -6
  10. package/.opencode/commands/bdb-aos-brainstorm.md +5 -0
  11. package/.opencode/commands/bdb-aos-doctor.md +5 -0
  12. package/.opencode/commands/bdb-aos-graph.md +5 -0
  13. package/.opencode/commands/bdb-aos-init.md +5 -0
  14. package/.opencode/commands/bdb-aos-loop.md +5 -0
  15. package/.opencode/commands/bdb-aos-mastersession.md +5 -0
  16. package/.opencode/commands/bdb-aos-memb.md +5 -0
  17. package/.opencode/commands/bdb-aos-orchestrator.md +5 -0
  18. package/.opencode/commands/bdb-aos-plan.md +5 -0
  19. package/.opencode/commands/bdb-aos-playbooks.md +5 -0
  20. package/.opencode/commands/bdb-aos-setup.md +5 -0
  21. package/.opencode/commands/bdb-aos-shipping.md +5 -0
  22. package/.opencode/commands/bdb-aos-startproject.md +5 -0
  23. package/.opencode/commands/bdb-aos-store.md +5 -0
  24. package/.opencode/plugins/bdb-aos.js +54 -10
  25. package/README.md +5 -4
  26. package/THIRD_PARTY_NOTICES.md +2 -2
  27. package/agy-commands/loop.md +5 -0
  28. package/bin/aos-acp.mjs +27 -1
  29. package/bin/aos-doctor.mjs +41 -4
  30. package/bin/aos-uninstall.mjs +41 -3
  31. package/bin/go-check.mjs +79 -0
  32. package/bin/guarded-patterns.json +106 -0
  33. package/commands/brainstorm.md +5 -0
  34. package/commands/doctor.md +5 -0
  35. package/commands/graph.md +5 -0
  36. package/commands/init.md +5 -0
  37. package/commands/loop.md +5 -0
  38. package/commands/mastersession.md +5 -0
  39. package/commands/memb.md +5 -0
  40. package/commands/orchestrator.md +5 -0
  41. package/commands/plan.md +5 -0
  42. package/commands/playbooks.md +5 -0
  43. package/commands/setup.md +5 -0
  44. package/commands/shipping.md +5 -0
  45. package/commands/startproject.md +5 -0
  46. package/commands/store.md +5 -0
  47. package/docs/codenotch.md +44 -0
  48. package/docs/codex-agy-setup.md +16 -0
  49. package/docs/codex-gate-smoke.md +43 -0
  50. package/docs/delegation-routing.md +32 -0
  51. package/docs/go-check.md +60 -0
  52. package/docs/master-session-acp.md +2 -0
  53. package/docs/opencode-setup.md +54 -0
  54. package/docs/plugin-migration.md +61 -0
  55. package/installer.js +351 -121
  56. package/lib/codenotch.js +389 -0
  57. package/lib/plugin-migration.js +462 -0
  58. package/lib/retired-skills.js +101 -0
  59. package/lib/store-ui/index.html +9 -1
  60. package/lib/store-ui/server.mjs +2 -0
  61. package/mcps/mcsc/README.md +1 -1
  62. package/mcps/mcsc/packages/core/src/adapters/agy.js +3 -1
  63. package/mcps/mcsc/packages/core/src/adapters/codex.js +2 -1
  64. package/mcps/mcsc/packages/core/src/adapters/opencode.js +2 -1
  65. package/mcps/mcsc/packages/core/src/depth.js +16 -0
  66. package/mcps/mcsc/packages/mcp/server.js +15 -2
  67. package/package.json +7 -2
  68. package/plugin-commands.json +143 -0
  69. package/plugin.json +299 -0
  70. package/plugins/bdb-aos-codex/.codex-plugin/plugin.json +38 -0
  71. package/plugins/bdb-aos-codex/skills/brainstorm/SKILL.md +6 -0
  72. package/plugins/bdb-aos-codex/skills/doctor/SKILL.md +6 -0
  73. package/plugins/bdb-aos-codex/skills/graph/SKILL.md +6 -0
  74. package/plugins/bdb-aos-codex/skills/init/SKILL.md +6 -0
  75. package/plugins/bdb-aos-codex/skills/loop/SKILL.md +6 -0
  76. package/plugins/bdb-aos-codex/skills/mastersession/SKILL.md +6 -0
  77. package/plugins/bdb-aos-codex/skills/memb/SKILL.md +6 -0
  78. package/plugins/bdb-aos-codex/skills/orchestrator/SKILL.md +6 -0
  79. package/plugins/bdb-aos-codex/skills/plan/SKILL.md +6 -0
  80. package/plugins/bdb-aos-codex/skills/playbooks/SKILL.md +6 -0
  81. package/plugins/bdb-aos-codex/skills/setup/SKILL.md +6 -0
  82. package/plugins/bdb-aos-codex/skills/shipping/SKILL.md +6 -0
  83. package/plugins/bdb-aos-codex/skills/startproject/SKILL.md +6 -0
  84. package/plugins/bdb-aos-codex/skills/store/SKILL.md +6 -0
  85. package/scripts/build-plugin-manifest.mjs +202 -3
  86. package/scripts/codex-gate-smoke.mjs +73 -0
  87. package/skills/basic/master-session/SKILL.md +11 -0
  88. package/skills/bdb-aos/scripts/list-playbooks.mjs +72 -0
  89. package/skills/global_config/agenttrail/SKILL.md +3 -1
  90. package/skills/global_config/agenttrail/bin/agenttrail.mjs +255 -117
  91. package/skills/global_config/agenttrail/bin/ensure.mjs +60 -40
  92. package/skills/global_config/agenttrail/bin/repoid.mjs +70 -0
  93. package/skills/global_config/agenttrail/public/index.html +9 -1
  94. package/skills/global_config/aos-setup/scripts/aos-doctor.mjs +1 -1
  95. package/skills/global_config/bdb-memb-mcp/SKILL.md +5 -4
  96. package/skills/global_config/bdb-visual-edit/SKILL.md +28 -32
  97. package/skills/global_config/bdb-visual-edit/references/vite-react-source-attr.md +2 -2
  98. package/skills/global_config/bdb-visual-edit/scripts/locate-source.mjs +135 -0
  99. package/skills/global_config/bdb-visual-edit/scripts/sanitize-element.mjs +30 -2
  100. package/skills/global_config/gogate/SKILL.md +123 -0
  101. package/skills/global_config/loop-templates/SKILL.md +27 -0
  102. package/skills/global_config/loop-templates/references/ci-until-green.md +27 -0
  103. package/skills/global_config/loop-templates/references/daily-summary.md +27 -0
  104. package/skills/global_config/loop-templates/references/pr-to-merge.md +27 -0
  105. package/skills/global_config/loop-templates/references/review-rounds.md +27 -0
  106. package/skills/global_config/mcsc/SKILL.md +9 -1
  107. package/skills/global_config/plan-arbiter/SKILL.md +1 -1
  108. package/skills/global_config/plan-canvas/SKILL.md +42 -4
  109. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/README.md +1 -1
  110. package/skills/global_config/plan-canvas/scripts/lib/plan-builder/render.js +2 -2
  111. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/annotate-client/geometry.js +76 -0
  112. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/annotate-client/index.js +596 -0
  113. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/annotate-client/model.js +192 -0
  114. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/annotate-client/toolbar.js +99 -0
  115. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/annotate-server.js +282 -0
  116. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/annotation-schema.js +210 -0
  117. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/route.js +10 -0
  118. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/sdk.js +6 -230
  119. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/server.js +45 -4
  120. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/sessions.js +60 -21
  121. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/trail-on-approve.js +103 -0
  122. package/skills/global_config/plan-canvas/scripts/lib/plan-canvas/ui.js +19 -7
  123. package/skills/global_config/plan-canvas/scripts/plan-canvas.js +118 -20
  124. package/skills/global_config/subagent-setup/SKILL.md +6 -0
  125. package/skills/global_config/subagent-setup/scripts/setup-subagents.mjs +20 -1
  126. package/skills/playbooks/pb-idea-to-launch/SKILL.md +2 -2
  127. package/skills/playbooks/pb-redesign-app/SKILL.md +3 -3
  128. package/skills/playbooks/pb-release-aos/SKILL.md +2 -2
  129. package/skills/playbooks/pb-ship/SKILL.md +2 -2
  130. package/skills/playbooks/pb-worktrees-land/SKILL.md +2 -2
  131. package/.codex-plugin/marketplace.json +0 -11
  132. package/skills/global_config/bdb-visual-edit/scripts/pick-snippet.js +0 -27
  133. package/skills/global_config/visual-edit/README.md +0 -96
  134. package/skills/global_config/visual-edit/SKILL.md +0 -615
  135. package/skills/global_config/visual-plan/README.md +0 -93
  136. package/skills/global_config/visual-plan/SKILL.md +0 -544
  137. package/skills/global_config/visual-plan/references/canvas.md +0 -139
  138. package/skills/global_config/visual-plan/references/connection.md +0 -51
  139. package/skills/global_config/visual-plan/references/document-quality.md +0 -186
  140. package/skills/global_config/visual-plan/references/exemplar.md +0 -62
  141. package/skills/global_config/visual-plan/references/local-files.md +0 -99
  142. package/skills/global_config/visual-plan/references/wireframe.md +0 -319
  143. package/skills/global_config/visual-recap/README.md +0 -103
  144. package/skills/global_config/visual-recap/SKILL.md +0 -560
  145. package/skills/global_config/visual-recap/references/connection.md +0 -51
  146. package/skills/global_config/visual-recap/references/local-files.md +0 -99
  147. package/skills/global_config/visual-recap/references/wireframe.md +0 -319
@@ -0,0 +1,389 @@
1
+ 'use strict';
2
+ // macOS-only opt-in install of BDB AO Codenotch from the public releases repo.
3
+ // Every side effect (network, hdiutil, xattr, plutil, copy, prompts, roots) is injectable.
4
+ const fs = require('fs');
5
+ const os = require('os');
6
+ const path = require('path');
7
+ const crypto = require('crypto');
8
+ const { execFileSync } = require('child_process');
9
+
10
+ // The list endpoint, not /releases/latest: the newest release may lack this platform's assets.
11
+ const RELEASES_URL = 'https://api.github.com/repos/hybridlabor-api/bdb-ao-codenotch/releases?per_page=30';
12
+ const stateFilePath = (home = os.homedir()) => path.join(home, '.agents', '.bdb-codenotch.json');
13
+
14
+ // 'off' wins over 'force'; otherwise macOS installs default on.
15
+ function codenotchMode(argv = process.argv, env = process.env) {
16
+ if (argv.includes('--no-codenotch') || env.AOS_CODENOTCH === '0') return 'off';
17
+ if (argv.includes('--codenotch') || env.AOS_CODENOTCH === '1') return 'force';
18
+ return 'default';
19
+ }
20
+
21
+ function compareVersions(a, b) {
22
+ const parts = (v) => String(v).replace(/^v/i, '').split(/[^0-9]+/).filter(Boolean).map(Number);
23
+ const x = parts(a), y = parts(b);
24
+ for (let i = 0; i < Math.max(x.length, y.length); i++) {
25
+ const d = (x[i] || 0) - (y[i] || 0);
26
+ if (d) return d < 0 ? -1 : 1;
27
+ }
28
+ return 0;
29
+ }
30
+
31
+ const run = (cmd) => (args) => execFileSync(cmd, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
32
+ const defaultDeps = () => ({
33
+ fetch: (...a) => fetch(...a),
34
+ hdiutil: run('hdiutil'),
35
+ xattr: run('xattr'),
36
+ plutil: run('plutil'),
37
+ copyApp: (src, dest) => execFileSync('ditto', [src, dest]),
38
+ reg: run('reg'),
39
+ runInstaller: (exe, args, timeoutMs) => runWinProgram(require('child_process').spawnSync, exe, args, timeoutMs),
40
+ });
41
+
42
+ function readPlist(plutil, appPath) {
43
+ const plist = path.join(appPath, 'Contents', 'Info.plist');
44
+ if (!fs.existsSync(plist)) return null;
45
+ const get = (key) => {
46
+ try { return String(plutil(['-extract', key, 'raw', '-o', '-', plist])).trim() || null; } catch { return null; }
47
+ };
48
+ const version = get('CFBundleShortVersionString');
49
+ return version ? { version, bundleId: get('CFBundleIdentifier') } : null;
50
+ }
51
+
52
+ const readState = (file) => {
53
+ try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; }
54
+ };
55
+
56
+ const isWritableDir = (dir) => {
57
+ try { if (!fs.statSync(dir).isDirectory()) return false; fs.accessSync(dir, fs.constants.W_OK); return true; } catch { return false; }
58
+ };
59
+
60
+ async function fetchLatestRelease(fetchFn, platform) {
61
+ let res;
62
+ try {
63
+ res = await fetchFn(RELEASES_URL, { headers: { Accept: 'application/vnd.github+json', 'User-Agent': 'aos-installer' } });
64
+ } catch (e) {
65
+ return { error: `could not reach GitHub (${e.message}); try again later` };
66
+ }
67
+ if (res.status === 404) return { error: 'no public Codenotch release found yet (hybridlabor-api/bdb-ao-codenotch); try again later' };
68
+ const remaining = res.headers && res.headers.get && res.headers.get('x-ratelimit-remaining');
69
+ if (res.status === 429 || (res.status === 403 && remaining === '0')) return { error: 'GitHub API rate limit reached for unauthenticated requests; try again in about an hour' };
70
+ if (!res.ok) return { error: `GitHub releases API answered HTTP ${res.status}` };
71
+ const data = await res.json();
72
+ const release = selectRelease(Array.isArray(data) ? data : [data], platform);
73
+ return release ? { release } : { error: `no public Codenotch release found yet with a ${platform === 'win32' ? 'Windows Setup .exe' : '.dmg'} and a .sha256 asset; try again later` };
74
+ }
75
+
76
+ function pickMacAssets(assets) {
77
+ const dmg = assets.find((a) => /\.dmg$/i.test(a.name));
78
+ const sha = dmg && (assets.find((a) => a.name === `${dmg.name}.sha256`) || assets.find((a) => /\.sha256$/i.test(a.name)));
79
+ return dmg && sha ? { dmg, sha } : null;
80
+ }
81
+
82
+ // First (newest) non-draft, non-prerelease release that carries this platform's asset and a checksum.
83
+ function selectRelease(releases, platform) {
84
+ return releases.find((r) => {
85
+ if (!r || r.draft || r.prerelease) return false;
86
+ const tag = String(r.tag_name || '').replace(/^v/i, '');
87
+ return tag && !(platform === 'win32' ? pickWinAssets(r.assets || [], tag).error : !pickMacAssets(r.assets || []));
88
+ }) || null;
89
+ }
90
+
91
+ async function download(fetchFn, url) {
92
+ const res = await fetchFn(url, { headers: { 'User-Agent': 'aos-installer' }, redirect: 'follow' });
93
+ if (!res.ok) throw new Error(`download failed: HTTP ${res.status}`);
94
+ return Buffer.from(await res.arrayBuffer());
95
+ }
96
+
97
+ function findInstalled(state, roots, plutil) {
98
+ const candidates = [state && state.path, ...[roots.applications, roots.userApplications].map((r) => path.join(r, 'Codenotch.app'))].filter(Boolean);
99
+ for (const appPath of candidates) {
100
+ if (!fs.existsSync(appPath)) continue;
101
+ const info = readPlist(plutil, appPath);
102
+ const ours = !!(state && state.path === appPath && info && info.bundleId === state.bundleId && info.version === state.version);
103
+ return { appPath, info, ours };
104
+ }
105
+ return null;
106
+ }
107
+
108
+ const UNINSTALL_ROOT = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall';
109
+ // UNVERIFIED: key name, DisplayVersion and UninstallString come from Tauri's stock NSIS template, not a real
110
+ // build; check on Windows with the first CI-built installer. The DisplayName scan is the fallback.
111
+ const NSIS_KEY = 'Codenotch';
112
+ const INSTALLER_TIMEOUT_MS = 5 * 60 * 1000;
113
+
114
+ function regValues(out) {
115
+ const vals = {};
116
+ for (const line of String(out).split(/\r?\n/)) {
117
+ const m = line.match(/^\s+(\S+)\s+REG_\w+\s+(.*?)\s*$/);
118
+ if (m) vals[m[1]] = m[2];
119
+ }
120
+ return vals;
121
+ }
122
+
123
+ function uninstallExeFrom(vals) {
124
+ const cmd = vals.QuietUninstallString || vals.UninstallString || '';
125
+ const m = cmd.match(/^\s*"([^"]+)"/) || cmd.match(/^\s*(.+?\.exe)/i);
126
+ if (m) return m[1];
127
+ return vals.InstallLocation ? path.win32.join(vals.InstallLocation, 'uninstall.exe') : null;
128
+ }
129
+
130
+ // Returns { version, uninstallPath } or null (not installed, key missing, or reg failing).
131
+ function readWinInstall(reg) {
132
+ const read = (key) => { try { return regValues(reg(['query', key])); } catch { return null; } };
133
+ let vals = read(`${UNINSTALL_ROOT}\\${NSIS_KEY}`);
134
+ if (!vals || !vals.DisplayVersion) {
135
+ vals = null;
136
+ let listing = '';
137
+ try { listing = reg(['query', UNINSTALL_ROOT]); } catch { return null; }
138
+ for (const sub of String(listing).split(/\r?\n/).map((l) => l.trim()).filter((l) => /^HKEY_/i.test(l))) {
139
+ const v = read(sub);
140
+ if (v && /^codenotch$/i.test(v.DisplayName || '') && v.DisplayVersion) { vals = v; break; }
141
+ }
142
+ }
143
+ return vals ? { version: vals.DisplayVersion, uninstallPath: uninstallExeFrom(vals) } : null;
144
+ }
145
+
146
+ function runWinProgram(spawnSyncFn, exe, args, timeoutMs) {
147
+ const r = spawnSyncFn(exe, args, { timeout: timeoutMs, windowsHide: true, stdio: 'ignore' });
148
+ if (r.error) throw new Error(r.error.code === 'ETIMEDOUT' ? `timed out after ${Math.round(timeoutMs / 1000)}s` : r.error.message);
149
+ if (r.status !== 0) throw new Error(`exited with code ${r.status}`);
150
+ }
151
+
152
+ function pickWinAssets(assets, latest) {
153
+ const exact = assets.find((a) => a.name === `Codenotch-Setup-${latest}.exe`);
154
+ const exes = exact ? [exact] : assets.filter((a) => /Setup.*\.exe$/i.test(a.name));
155
+ if (exes.length !== 1) return { error: exes.length ? 'latest release has several Setup .exe assets; cannot pick one' : 'latest release has no Windows Setup .exe asset' };
156
+ const sha = assets.find((a) => a.name === `${exes[0].name}.sha256`);
157
+ return sha ? { exe: exes[0], sha } : { error: 'latest release has no matching .sha256 asset; refusing to run an unverified installer' };
158
+ }
159
+
160
+ async function installCodenotchWin(o, log) {
161
+ const home = o.home || os.homedir();
162
+ const tmp = (o.roots && o.roots.tmp) || os.tmpdir();
163
+ const stateFile = o.stateFile || stateFilePath(home);
164
+ const interactive = !!o.interactive;
165
+ const confirm = o.confirm || (async () => false);
166
+ const fail = (status, message) => { log.warn(`Codenotch: ${message}`); return { status, message }; };
167
+
168
+ const { release, error } = await fetchLatestRelease(o.fetch, 'win32');
169
+ if (error) return fail(/rate limit/.test(error) ? 'rate-limited' : /no public/.test(error) ? 'not-found' : 'error', error);
170
+ const latest = String(release.tag_name || '').replace(/^v/i, '');
171
+ const picked = pickWinAssets(release.assets || [], latest);
172
+ if (!latest || picked.error) return fail('error', picked.error || 'latest release has no tag');
173
+
174
+ const state = readState(stateFile);
175
+ const installed = readWinInstall(o.reg);
176
+ let replacing = false;
177
+ if (installed) {
178
+ if (compareVersions(installed.version, latest) >= 0) {
179
+ const message = `already installed (${installed.version})${installed.uninstallPath ? ` at ${installed.uninstallPath}` : ''}`;
180
+ log.info(`Codenotch: ${message}`);
181
+ return { status: 'up-to-date', message };
182
+ }
183
+ const ours = !!(state && state.platform === 'win32' && state.version === installed.version && state.uninstallPath === installed.uninstallPath);
184
+ if (!ours && !interactive) {
185
+ const message = `an existing Codenotch ${installed.version} was not installed by AOS; keeping it (latest ${latest})`;
186
+ log.info(`Codenotch: ${message}`);
187
+ return { status: 'kept-foreign', message };
188
+ }
189
+ if (interactive && !(await confirm(`Replace Codenotch ${installed.version} with ${latest}?`))) {
190
+ return { status: 'declined', message: 'kept the installed Codenotch' };
191
+ }
192
+ replacing = true;
193
+ }
194
+
195
+ const work = fs.mkdtempSync(path.join(tmp, 'aos-codenotch-'));
196
+ try {
197
+ const buf = await download(o.fetch, picked.exe.browser_download_url);
198
+ const expected = ((await download(o.fetch, picked.sha.browser_download_url)).toString('utf8').match(/[0-9a-f]{64}/i) || [])[0];
199
+ if (!expected || expected.toLowerCase() !== crypto.createHash('sha256').update(buf).digest('hex')) {
200
+ return fail('checksum-mismatch', `sha256 mismatch for ${picked.exe.name}; refusing to run it`);
201
+ }
202
+ const exePath = path.join(work, picked.exe.name);
203
+ fs.writeFileSync(exePath, buf);
204
+
205
+ try { o.runInstaller(exePath, ['/S'], o.timeoutMs || INSTALLER_TIMEOUT_MS); }
206
+ catch (e) { return fail('error', `silent installer failed: ${e.message}`); }
207
+
208
+ const after = readWinInstall(o.reg);
209
+ if (!after || compareVersions(after.version, latest) < 0) return fail('error', `installer finished but the registry shows ${after ? after.version : 'no Codenotch'}; not recorded`);
210
+ fs.mkdirSync(path.dirname(stateFile), { recursive: true });
211
+ fs.writeFileSync(stateFile, JSON.stringify({ platform: 'win32', uninstallPath: after.uninstallPath, version: after.version, installedAt: new Date().toISOString() }, null, 2));
212
+ const message = `${replacing ? 'updated' : 'installed'} Codenotch ${after.version}${after.uninstallPath ? ` (uninstaller: ${after.uninstallPath})` : ''}`;
213
+ log.success(`Codenotch: ${message}`);
214
+ return { status: replacing ? 'updated' : 'installed', message, path: after.uninstallPath, version: after.version };
215
+ } catch (e) {
216
+ return fail('error', e.message);
217
+ } finally {
218
+ fs.rmSync(work, { recursive: true, force: true });
219
+ }
220
+ }
221
+
222
+ // Returns { status, message }. Never throws, never elevates.
223
+ async function installCodenotch(opts = {}) {
224
+ const o = { ...defaultDeps(), ...opts };
225
+ const platform = o.platform || process.platform;
226
+ const log = { info() {}, warn() {}, success() {}, ...(o.log || {}) };
227
+ if (platform === 'win32') return installCodenotchWin(o, log);
228
+ if (platform !== 'darwin') return { status: 'skipped-platform' };
229
+
230
+ const home = o.home || os.homedir();
231
+ const roots = { applications: '/Applications', userApplications: path.join(home, 'Applications'), tmp: os.tmpdir(), ...(o.roots || {}) };
232
+ const stateFile = o.stateFile || stateFilePath(home);
233
+ const interactive = !!o.interactive;
234
+ const confirm = o.confirm || (async () => false);
235
+ const fail = (status, message) => { log.warn(`Codenotch: ${message}`); return { status, message }; };
236
+
237
+ const { release, error } = await fetchLatestRelease(o.fetch, 'darwin');
238
+ if (error) return fail(/rate limit/.test(error) ? 'rate-limited' : /no public/.test(error) ? 'not-found' : 'error', error);
239
+
240
+ const latest = String(release.tag_name || '').replace(/^v/i, '');
241
+ const { dmg, sha: shaAsset } = pickMacAssets(release.assets || []);
242
+
243
+ const state = readState(stateFile);
244
+ const installed = findInstalled(state, roots, o.plutil);
245
+ let destDir = null, replacing = null;
246
+ if (installed) {
247
+ const cur = installed.info && installed.info.version;
248
+ if (cur && compareVersions(cur, latest) >= 0) {
249
+ const message = `already installed (${cur}) at ${installed.appPath}`;
250
+ log.info(`Codenotch: ${message}`);
251
+ return { status: 'up-to-date', message };
252
+ }
253
+ if (!installed.ours && !interactive) {
254
+ const message = `${installed.appPath} was not installed by AOS; keeping it (installed ${cur || 'unknown version'}, latest ${latest})`;
255
+ log.info(`Codenotch: ${message}`);
256
+ return { status: 'kept-foreign', message };
257
+ }
258
+ if (interactive && !(await confirm(`Replace Codenotch ${cur || '(unknown version)'} at ${installed.appPath} with ${latest}?`))) {
259
+ return { status: 'declined', message: 'kept the installed Codenotch' };
260
+ }
261
+ replacing = installed.appPath;
262
+ destDir = path.dirname(replacing);
263
+ if (!isWritableDir(destDir)) return fail('error', `${destDir} is not writable; update Codenotch manually`);
264
+ }
265
+
266
+ const work = fs.mkdtempSync(path.join(roots.tmp, 'aos-codenotch-'));
267
+ const mountPoint = path.join(work, 'mnt');
268
+ let mounted = false;
269
+ try {
270
+ const dmgBuf = await download(o.fetch, dmg.browser_download_url);
271
+ const expected = ((await download(o.fetch, shaAsset.browser_download_url)).toString('utf8').match(/[0-9a-f]{64}/i) || [])[0];
272
+ const actual = crypto.createHash('sha256').update(dmgBuf).digest('hex');
273
+ if (!expected || expected.toLowerCase() !== actual) return fail('checksum-mismatch', `sha256 mismatch for ${dmg.name}; refusing to mount it`);
274
+ const dmgPath = path.join(work, dmg.name);
275
+ fs.writeFileSync(dmgPath, dmgBuf);
276
+
277
+ fs.mkdirSync(mountPoint);
278
+ o.hdiutil(['attach', '-nobrowse', '-readonly', '-mountpoint', mountPoint, dmgPath]);
279
+ mounted = true;
280
+
281
+ const apps = fs.readdirSync(mountPoint, { withFileTypes: true }).filter((e) => e.isDirectory() && e.name.endsWith('.app'));
282
+ if (apps.length !== 1) return fail('error', `expected exactly one .app in the DMG, found ${apps.length}`);
283
+ const appName = apps[0].name;
284
+ const src = path.join(mountPoint, appName);
285
+
286
+ let dest;
287
+ if (replacing) dest = replacing;
288
+ else {
289
+ destDir = isWritableDir(roots.applications) ? roots.applications : roots.userApplications;
290
+ fs.mkdirSync(destDir, { recursive: true });
291
+ dest = path.join(destDir, appName);
292
+ if (fs.existsSync(dest)) {
293
+ const cur = readPlist(o.plutil, dest);
294
+ if (!interactive || !(cur && compareVersions(cur.version, latest) < 0) || !(await confirm(`Replace ${dest} (not installed by AOS) with Codenotch ${latest}?`))) {
295
+ return { status: 'kept-foreign', message: `${dest} exists and was not installed by AOS; keeping it` };
296
+ }
297
+ replacing = dest;
298
+ }
299
+ }
300
+
301
+ const backup = replacing ? `${dest}.aos-old` : null;
302
+ if (backup) fs.renameSync(dest, backup);
303
+ try {
304
+ o.copyApp(src, dest);
305
+ o.xattr(['-dr', 'com.apple.quarantine', dest]);
306
+ } catch (e) {
307
+ fs.rmSync(dest, { recursive: true, force: true });
308
+ if (backup) fs.renameSync(backup, dest);
309
+ throw e;
310
+ }
311
+ if (backup) fs.rmSync(backup, { recursive: true, force: true });
312
+
313
+ const info = readPlist(o.plutil, dest) || { version: latest, bundleId: null };
314
+ fs.mkdirSync(path.dirname(stateFile), { recursive: true });
315
+ fs.writeFileSync(stateFile, JSON.stringify({ path: dest, version: info.version, bundleId: info.bundleId, installedAt: new Date().toISOString() }, null, 2));
316
+ const message = `${replacing ? 'updated' : 'installed'} Codenotch ${info.version} at ${dest} (ad-hoc signed; quarantine removed)`;
317
+ log.success(`Codenotch: ${message}`);
318
+ return { status: replacing ? 'updated' : 'installed', message, path: dest, version: info.version };
319
+ } catch (e) {
320
+ return fail('error', e.message);
321
+ } finally {
322
+ if (mounted) {
323
+ try { o.hdiutil(['detach', mountPoint]); } catch {
324
+ try { o.hdiutil(['detach', mountPoint, '-force']); } catch { /* leave to the OS */ }
325
+ }
326
+ }
327
+ fs.rmSync(work, { recursive: true, force: true });
328
+ }
329
+ }
330
+
331
+ // Pure planning for aos-uninstall: only the recorded install, only if still the recorded build.
332
+ function planCodenotchUninstall({ stateFile = stateFilePath(), plutil = run('plutil'), reg = run('reg'), exists = fs.existsSync } = {}) {
333
+ const state = readState(stateFile);
334
+ if (!state) return null;
335
+ if (state.platform === 'win32') {
336
+ if (!state.uninstallPath) return null;
337
+ if (!exists(state.uninstallPath)) return { state, action: 'gone' };
338
+ const cur = readWinInstall(reg);
339
+ return { state, action: cur && cur.version === state.version && cur.uninstallPath === state.uninstallPath ? 'remove' : 'keep' };
340
+ }
341
+ if (!state.path) return null;
342
+ if (!fs.existsSync(state.path)) return { state, action: 'gone' };
343
+ const info = readPlist(plutil, state.path);
344
+ const same = info && info.bundleId === state.bundleId && info.version === state.version;
345
+ return { state, action: same ? 'remove' : 'keep' };
346
+ }
347
+
348
+ function uninstallCodenotch(plan, { stateFile = stateFilePath(), runUninstaller = (exe, args, t) => runWinProgram(require('child_process').spawnSync, exe, args, t) } = {}) {
349
+ if (!plan) return 'none';
350
+ if (plan.action === 'remove') {
351
+ if (plan.state.platform === 'win32') {
352
+ try { runUninstaller(plan.state.uninstallPath, ['/S'], INSTALLER_TIMEOUT_MS); } catch { return 'error'; }
353
+ } else fs.rmSync(plan.state.path, { recursive: true, force: true });
354
+ }
355
+ if (plan.action !== 'keep') fs.rmSync(stateFile, { force: true });
356
+ return plan.action;
357
+ }
358
+
359
+ // Never throws and never affects the caller's exit code. Returns a result or null (not run).
360
+ async function runCodenotchStep({ platform = process.platform, argv = process.argv, env = process.env, interactive, dryRun, ask, log = {}, install = installCodenotch, ...rest } = {}) {
361
+ if (platform !== 'darwin' && platform !== 'win32') return null;
362
+ try {
363
+ const mode = codenotchMode(argv, env);
364
+ if (mode === 'off') return { status: 'opted-out' };
365
+ if (interactive && mode === 'default' && ask && !(await ask('Install BDB AO Codenotch? (SHA-256 verified)'))) return { status: 'declined' };
366
+ if (dryRun) { if (log.info) log.info('[dry-run] would install BDB AO Codenotch'); return null; }
367
+ return await install({ platform, interactive, confirm: ask && ((m) => ask(m, false)), log, ...rest });
368
+ } catch (e) {
369
+ if (log.warn) log.warn(`Codenotch: ${e.message}`);
370
+ return { status: 'error', message: e.message };
371
+ }
372
+ }
373
+
374
+ function codenotchSummaryLine(r) {
375
+ if (!r || r.status === 'skipped-platform') return null;
376
+ const how = (p) => `remove with aos-uninstall${p ? ` (app: ${p})` : ''}`;
377
+ switch (r.status) {
378
+ case 'installed': case 'updated': return `Codenotch: ${r.status} ${r.version || ''} (${r.path}); ${how(r.path)}`.replace(' ', ' ');
379
+ case 'up-to-date': case 'kept-foreign': return `Codenotch: skipped (${r.message}); ${how()}`;
380
+ case 'opted-out': return 'Codenotch: skipped (opted out)';
381
+ case 'declined': return 'Codenotch: skipped (declined)';
382
+ default: return `Codenotch: FAILED (${r.message || r.status}); AOS install is unaffected, rerun later or use --no-codenotch to opt out`;
383
+ }
384
+ }
385
+
386
+ module.exports = {
387
+ RELEASES_URL, selectRelease, stateFilePath, readWinInstall, runWinProgram, uninstallExeFrom, codenotchMode, runCodenotchStep, codenotchSummaryLine, compareVersions, readPlist,
388
+ installCodenotch, planCodenotchUninstall, uninstallCodenotch,
389
+ };