@clearlysh/beehaven 0.8.16

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 (171) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +201 -0
  3. package/dist/__tests__/_cli-switch.js +105 -0
  4. package/dist/__tests__/agent-wrap-probe.js +136 -0
  5. package/dist/agent-attribution.js +192 -0
  6. package/dist/agent-identity-daemon.js +135 -0
  7. package/dist/agent-onboard.js +156 -0
  8. package/dist/agent-scope.js +79 -0
  9. package/dist/agent-sessions.js +370 -0
  10. package/dist/agent-state.js +482 -0
  11. package/dist/app-manifest.js +221 -0
  12. package/dist/app-signer.js +54 -0
  13. package/dist/app-signing.js +127 -0
  14. package/dist/audio-play.js +98 -0
  15. package/dist/avatar-protocol.js +25 -0
  16. package/dist/avatar-route.js +79 -0
  17. package/dist/bee/cloud-proxy.js +130 -0
  18. package/dist/blocked-actions.js +120 -0
  19. package/dist/build-channel.js +101 -0
  20. package/dist/buildings/daemon-workspace.js +475 -0
  21. package/dist/claude-bridge.js +310 -0
  22. package/dist/claude-credential-store.js +633 -0
  23. package/dist/clearly-sync.js +518 -0
  24. package/dist/clearly.js +616 -0
  25. package/dist/cli-state.js +334 -0
  26. package/dist/cli.js +6973 -0
  27. package/dist/commands/agent-skills.js +302 -0
  28. package/dist/commands/agent.js +1310 -0
  29. package/dist/commands/app.js +1123 -0
  30. package/dist/commands/auth.js +171 -0
  31. package/dist/commands/building-shell.js +392 -0
  32. package/dist/commands/canvas.js +932 -0
  33. package/dist/commands/claude.js +218 -0
  34. package/dist/commands/compliance.js +387 -0
  35. package/dist/commands/compose.js +507 -0
  36. package/dist/commands/composition.js +95 -0
  37. package/dist/commands/connect-session.js +409 -0
  38. package/dist/commands/conversations.js +160 -0
  39. package/dist/commands/costs.js +443 -0
  40. package/dist/commands/d1.js +253 -0
  41. package/dist/commands/daemon-cmd.js +162 -0
  42. package/dist/commands/deploy.js +164 -0
  43. package/dist/commands/design.js +617 -0
  44. package/dist/commands/dev-generate.js +125 -0
  45. package/dist/commands/dev.js +109 -0
  46. package/dist/commands/devices.js +135 -0
  47. package/dist/commands/explain-router.js +24 -0
  48. package/dist/commands/explain.js +882 -0
  49. package/dist/commands/explore.js +264 -0
  50. package/dist/commands/help.js +2418 -0
  51. package/dist/commands/init.js +408 -0
  52. package/dist/commands/install.js +129 -0
  53. package/dist/commands/library.js +229 -0
  54. package/dist/commands/marketplace.js +138 -0
  55. package/dist/commands/mcp-install.js +476 -0
  56. package/dist/commands/mention.js +257 -0
  57. package/dist/commands/omni.js +529 -0
  58. package/dist/commands/secrets.js +323 -0
  59. package/dist/commands/self-update.js +206 -0
  60. package/dist/commands/sheet.js +264 -0
  61. package/dist/commands/shell-output.js +309 -0
  62. package/dist/commands/shell.js +1048 -0
  63. package/dist/commands/shop.js +368 -0
  64. package/dist/commands/skills.js +311 -0
  65. package/dist/commands/slides.js +418 -0
  66. package/dist/commands/swarm.js +341 -0
  67. package/dist/commands/ticket.js +1301 -0
  68. package/dist/commands/tool.js +312 -0
  69. package/dist/commands/trash.js +106 -0
  70. package/dist/commands/treehouse.js +120 -0
  71. package/dist/commands/user.js +240 -0
  72. package/dist/commands/voice-tts.js +319 -0
  73. package/dist/commands/voice.js +166 -0
  74. package/dist/commands/watch-git.js +401 -0
  75. package/dist/commands/webhook.js +159 -0
  76. package/dist/commands/workspace.js +3354 -0
  77. package/dist/composition-repos.js +91 -0
  78. package/dist/daemon-avatar.js +130 -0
  79. package/dist/daemon-claude-code-auto-install.js +52 -0
  80. package/dist/daemon-client.js +363 -0
  81. package/dist/daemon-codex.js +349 -0
  82. package/dist/daemon-entry.js +101 -0
  83. package/dist/daemon-fetch-allowlist.js +194 -0
  84. package/dist/daemon-gemini.js +224 -0
  85. package/dist/daemon-mcp-clearly.js +353 -0
  86. package/dist/daemon-mcp-sandbox.js +127 -0
  87. package/dist/daemon-omni-company.js +338 -0
  88. package/dist/daemon-omni-hooks.js +375 -0
  89. package/dist/daemon-omni-identity.js +229 -0
  90. package/dist/daemon-omni-pipeline.js +251 -0
  91. package/dist/daemon-omni-policies.js +346 -0
  92. package/dist/daemon-omni-principal.js +580 -0
  93. package/dist/daemon-omni-tool-gate.js +116 -0
  94. package/dist/daemon-omni.js +1442 -0
  95. package/dist/daemon-path-transform.js +120 -0
  96. package/dist/daemon-write-sandbox.js +191 -0
  97. package/dist/daemon.js +4471 -0
  98. package/dist/dashboard.js +846 -0
  99. package/dist/dep-installer.js +286 -0
  100. package/dist/dev-mode.js +67 -0
  101. package/dist/doc-indexer.js +169 -0
  102. package/dist/e2e-crypto.js +94 -0
  103. package/dist/feature-flags.js +154 -0
  104. package/dist/file-browser.js +88 -0
  105. package/dist/first-party-hives.js +211 -0
  106. package/dist/folder-sync.js +226 -0
  107. package/dist/health.js +56 -0
  108. package/dist/ipc-auth.js +299 -0
  109. package/dist/ipc-client.js +495 -0
  110. package/dist/ipc-server.js +684 -0
  111. package/dist/keychain.js +224 -0
  112. package/dist/lib/admin-client.js +73 -0
  113. package/dist/lib/mobile-build-parser.js +156 -0
  114. package/dist/local-app-manager.js +660 -0
  115. package/dist/log-redaction.js +89 -0
  116. package/dist/logger.js +108 -0
  117. package/dist/manifest.js +143 -0
  118. package/dist/marketplace.js +793 -0
  119. package/dist/min-version-copy.js +51 -0
  120. package/dist/min-version.js +214 -0
  121. package/dist/public-surface.js +23 -0
  122. package/dist/relay-presets.js +89 -0
  123. package/dist/relay.js +2723 -0
  124. package/dist/repl.js +619 -0
  125. package/dist/setup-relay.js +363 -0
  126. package/dist/setup-service.js +536 -0
  127. package/dist/shared/bee-memory/index.js +6 -0
  128. package/dist/shared/bee-memory/types.js +682 -0
  129. package/dist/shared/claude-code/daemon-types.js +112 -0
  130. package/dist/shared/claude-code/index.js +2 -0
  131. package/dist/shared/claude-code/types.js +1 -0
  132. package/dist/shared/status/schema.js +133 -0
  133. package/dist/shell-session.js +198 -0
  134. package/dist/speak-lock.js +97 -0
  135. package/dist/spinner.js +57 -0
  136. package/dist/telemetry.js +166 -0
  137. package/dist/terminal/block-parser.js +207 -0
  138. package/dist/terminal/shell-integration.js +108 -0
  139. package/dist/transcript-tailer.js +215 -0
  140. package/dist/tts-qwen3.js +547 -0
  141. package/dist/types.js +4 -0
  142. package/dist/utm.js +38 -0
  143. package/dist/verb-surface.js +331 -0
  144. package/dist/version-utils.js +85 -0
  145. package/dist/watcher.js +207 -0
  146. package/package.json +88 -0
  147. package/python/qwen3_tts.py +230 -0
  148. package/scripts/stamp-build-channel.mjs +115 -0
  149. package/scripts/sync-installed.mjs +159 -0
  150. package/scripts/sync-skills.mjs +112 -0
  151. package/skills/README.md +24 -0
  152. package/skills/agent-doodle-space/SKILL.md +118 -0
  153. package/skills/agent-home/SKILL.md +98 -0
  154. package/skills/beehaven-cli/SKILL.md +144 -0
  155. package/skills/claude-shopper/README.md +90 -0
  156. package/skills/claude-shopper/SKILL.md +151 -0
  157. package/skills/clearly-agent/SKILL.md +183 -0
  158. package/skills/clearly-canvas/SKILL.md +222 -0
  159. package/skills/clearly-canvas/authoring.md +160 -0
  160. package/skills/clearly-canvas/communications.md +32 -0
  161. package/skills/clearly-canvas/composition-patterns.md +84 -0
  162. package/skills/clearly-canvas/example.mjs +78 -0
  163. package/skills/clearly-canvas/project-plans.md +156 -0
  164. package/skills/clearly-canvas/review.mjs +68 -0
  165. package/skills/clearly-workspace/SKILL.md +79 -0
  166. package/skills/clearly-workspace/survey.mjs +101 -0
  167. package/skills/codebase-map/SKILL.md +142 -0
  168. package/skills/design-craft/SKILL.md +300 -0
  169. package/skills/index.json +79 -0
  170. package/skills/layout-systems/SKILL.md +184 -0
  171. package/skills/pair-on-canvas/SKILL.md +92 -0
@@ -0,0 +1,323 @@
1
+ /**
2
+ * beehaven secrets — Cloudflare Workers secrets wrapper
3
+ *
4
+ * Defaults to setting on EVERY declared env (the parity-enforcement the root
5
+ * CLAUDE.md explicitly calls out as a recurring bug source). Detects envs from
6
+ * `[env.<name>]` blocks in wrangler.toml/wrangler.jsonc.
7
+ *
8
+ * Subcommands:
9
+ * list [--env <name>|all] List secret names per env
10
+ * set <NAME> [--env <name>|all] Set on all envs by default (parity)
11
+ * delete <NAME> [--env <name>|all] Delete on all envs by default
12
+ * diff Show secrets present on one env but not the other
13
+ * check Verify required secrets (from beehaven.json) are set everywhere
14
+ */
15
+ import { existsSync, readFileSync } from 'fs';
16
+ import { dirname, join, resolve } from 'path';
17
+ import { spawn } from 'child_process';
18
+ import { createInterface } from 'readline';
19
+ import { color, spinner, table } from '../spinner.js';
20
+ function findWranglerConfig(start = process.cwd()) {
21
+ let dir = resolve(start);
22
+ while (true) {
23
+ for (const name of ['wrangler.toml', 'wrangler.jsonc', 'wrangler.json']) {
24
+ const p = join(dir, name);
25
+ if (existsSync(p))
26
+ return { path: p, envs: parseEnvs(p) };
27
+ }
28
+ const parent = dirname(dir);
29
+ if (parent === dir)
30
+ return null;
31
+ dir = parent;
32
+ }
33
+ }
34
+ function parseEnvs(configPath) {
35
+ const text = readFileSync(configPath, 'utf-8');
36
+ if (configPath.endsWith('.toml')) {
37
+ const envs = new Set();
38
+ const re = /^\[env\.([a-zA-Z0-9_-]+)/gm;
39
+ let m;
40
+ while ((m = re.exec(text)) !== null)
41
+ envs.add(m[1]);
42
+ return Array.from(envs);
43
+ }
44
+ const stripped = text.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
45
+ const cfg = JSON.parse(stripped);
46
+ return Object.keys(cfg.env ?? {});
47
+ }
48
+ function readRequiredSecrets(cwd) {
49
+ const candidates = [join(cwd, 'beehaven.json'), join(dirname(cwd), 'beehaven.json')];
50
+ for (const p of candidates) {
51
+ if (!existsSync(p))
52
+ continue;
53
+ try {
54
+ const cfg = JSON.parse(readFileSync(p, 'utf-8'));
55
+ if (Array.isArray(cfg.requiredSecrets))
56
+ return cfg.requiredSecrets;
57
+ }
58
+ catch {
59
+ /* ignore */
60
+ }
61
+ }
62
+ return [];
63
+ }
64
+ function runWranglerCapture(args, cwd) {
65
+ return new Promise((resolveP) => {
66
+ const proc = spawn('npx', ['wrangler', ...args], {
67
+ cwd,
68
+ stdio: ['ignore', 'pipe', 'pipe'],
69
+ env: { ...process.env },
70
+ });
71
+ let stdout = '';
72
+ let stderr = '';
73
+ proc.stdout.on('data', (d) => (stdout += d.toString()));
74
+ proc.stderr.on('data', (d) => (stderr += d.toString()));
75
+ proc.on('close', (code) => resolveP({ stdout, stderr, code: code ?? 1 }));
76
+ proc.on('error', () => resolveP({ stdout, stderr, code: 1 }));
77
+ });
78
+ }
79
+ function runWranglerWithStdin(args, cwd, stdinValue) {
80
+ return new Promise((resolveP) => {
81
+ const proc = spawn('npx', ['wrangler', ...args], {
82
+ cwd,
83
+ stdio: ['pipe', 'inherit', 'pipe'],
84
+ env: { ...process.env },
85
+ });
86
+ let stderr = '';
87
+ proc.stderr?.on('data', (d) => (stderr += d.toString()));
88
+ proc.stdin.write(stdinValue);
89
+ proc.stdin.end();
90
+ proc.on('close', (code) => resolveP({ code: code ?? 1, stderr }));
91
+ proc.on('error', () => resolveP({ code: 1, stderr }));
92
+ });
93
+ }
94
+ function extractJson(stdout) {
95
+ const idx = stdout.indexOf('[');
96
+ if (idx === -1) {
97
+ const objIdx = stdout.indexOf('{');
98
+ if (objIdx === -1)
99
+ return null;
100
+ try {
101
+ return JSON.parse(stdout.slice(objIdx));
102
+ }
103
+ catch {
104
+ return null;
105
+ }
106
+ }
107
+ try {
108
+ return JSON.parse(stdout.slice(idx));
109
+ }
110
+ catch {
111
+ return null;
112
+ }
113
+ }
114
+ async function listSecrets(cwd, env) {
115
+ const args = ['secret', 'list'];
116
+ if (env)
117
+ args.push('--env', env);
118
+ const { stdout, code } = await runWranglerCapture(args, cwd);
119
+ if (code !== 0)
120
+ return [];
121
+ const parsed = extractJson(stdout);
122
+ if (!Array.isArray(parsed))
123
+ return [];
124
+ return parsed
125
+ .map((s) => s.name)
126
+ .filter(Boolean)
127
+ .sort();
128
+ }
129
+ function targetEnvs(cfg, requested) {
130
+ // null = the unnamed default env (top-level wrangler.toml)
131
+ if (requested && requested !== 'all')
132
+ return [requested];
133
+ if (cfg.envs.length === 0)
134
+ return [null];
135
+ return [null, ...cfg.envs];
136
+ }
137
+ function envLabel(env) {
138
+ return env ?? 'prod';
139
+ }
140
+ function promptHidden(prompt) {
141
+ return new Promise((resolveP) => {
142
+ const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
143
+ process.stdout.write(prompt);
144
+ const stdin = process.stdin;
145
+ const wasRaw = stdin.isRaw;
146
+ if (stdin.setRawMode)
147
+ stdin.setRawMode(true);
148
+ let value = '';
149
+ const onData = (buf) => {
150
+ const ch = buf.toString();
151
+ if (ch === '\n' || ch === '\r' || ch === '') {
152
+ if (stdin.setRawMode)
153
+ stdin.setRawMode(!!wasRaw);
154
+ stdin.removeListener('data', onData);
155
+ process.stdout.write('\n');
156
+ rl.close();
157
+ resolveP(value);
158
+ return;
159
+ }
160
+ if (ch === '') {
161
+ process.exit(130);
162
+ }
163
+ if (ch === '\b' || ch === '\x7f') {
164
+ value = value.slice(0, -1);
165
+ return;
166
+ }
167
+ value += ch;
168
+ };
169
+ stdin.on('data', onData);
170
+ });
171
+ }
172
+ export async function secrets(opts) {
173
+ const cfg = findWranglerConfig();
174
+ if (!cfg) {
175
+ console.error(color.red('No wrangler.toml/wrangler.jsonc found in cwd or any parent.'));
176
+ process.exit(1);
177
+ }
178
+ const cwd = dirname(cfg.path);
179
+ const sub = opts.subcommand ?? 'list';
180
+ switch (sub) {
181
+ case 'list': {
182
+ const envs = targetEnvs(cfg, opts.env);
183
+ const byEnv = {};
184
+ for (const env of envs) {
185
+ const s = spinner(`Listing secrets on ${envLabel(env)}…`).start();
186
+ const names = await listSecrets(cwd, env);
187
+ s.succeed(`${envLabel(env)}: ${names.length} secrets`);
188
+ byEnv[envLabel(env)] = names;
189
+ }
190
+ if (opts.json) {
191
+ console.log(JSON.stringify(byEnv, null, 2));
192
+ return;
193
+ }
194
+ console.log('');
195
+ const allNames = Array.from(new Set(Object.values(byEnv).flat())).sort();
196
+ const envNames = Object.keys(byEnv);
197
+ const rows = allNames.map((name) => [
198
+ color.cyan(name),
199
+ ...envNames.map((e) => (byEnv[e].includes(name) ? color.green('✓') : color.dim('—'))),
200
+ ]);
201
+ console.log(table(['Secret', ...envNames], rows));
202
+ return;
203
+ }
204
+ case 'set': {
205
+ const name = opts.positionals[0];
206
+ if (!name) {
207
+ console.error('Usage: beehaven secrets set <NAME> [--env <name>|all] [--yes]');
208
+ process.exit(1);
209
+ }
210
+ const envs = targetEnvs(cfg, opts.env);
211
+ const value = await promptHidden(`Value for ${name}: `);
212
+ if (!value) {
213
+ console.error('Empty value — aborting.');
214
+ process.exit(1);
215
+ }
216
+ for (const env of envs) {
217
+ const args = ['secret', 'put', name];
218
+ if (env)
219
+ args.push('--env', env);
220
+ const s = spinner(`Setting ${name} on ${envLabel(env)}…`).start();
221
+ const { code, stderr } = await runWranglerWithStdin(args, cwd, value);
222
+ if (code !== 0) {
223
+ s.fail(`Failed on ${envLabel(env)} (exit ${code})`);
224
+ if (stderr)
225
+ console.error(color.dim(stderr));
226
+ }
227
+ else {
228
+ s.succeed(`${name} → ${envLabel(env)}`);
229
+ }
230
+ }
231
+ return;
232
+ }
233
+ case 'delete':
234
+ case 'rm': {
235
+ const name = opts.positionals[0];
236
+ if (!name) {
237
+ console.error('Usage: beehaven secrets delete <NAME> [--env <name>|all] [--yes]');
238
+ process.exit(1);
239
+ }
240
+ const envs = targetEnvs(cfg, opts.env);
241
+ if (!opts.yes) {
242
+ console.error(color.yellow(`About to delete ${name} from: ${envs.map(envLabel).join(', ')}`));
243
+ console.error(color.dim('Re-run with --yes to confirm.'));
244
+ process.exit(1);
245
+ }
246
+ for (const env of envs) {
247
+ const args = ['secret', 'delete', name];
248
+ if (env)
249
+ args.push('--env', env);
250
+ const s = spinner(`Deleting ${name} on ${envLabel(env)}…`).start();
251
+ const { code } = await runWranglerCapture([...args, '--force'], cwd);
252
+ if (code !== 0)
253
+ s.fail(`Failed on ${envLabel(env)}`);
254
+ else
255
+ s.succeed(`${name} ✕ ${envLabel(env)}`);
256
+ }
257
+ return;
258
+ }
259
+ case 'diff': {
260
+ if (cfg.envs.length === 0) {
261
+ console.log(color.dim('Only one env declared in wrangler.toml — nothing to diff.'));
262
+ return;
263
+ }
264
+ const envs = targetEnvs(cfg, 'all');
265
+ const byEnv = {};
266
+ for (const env of envs) {
267
+ const s = spinner(`Listing ${envLabel(env)}…`).start();
268
+ byEnv[envLabel(env)] = new Set(await listSecrets(cwd, env));
269
+ s.succeed(envLabel(env));
270
+ }
271
+ const envNames = Object.keys(byEnv);
272
+ const allNames = Array.from(new Set(envNames.flatMap((e) => Array.from(byEnv[e])))).sort();
273
+ const missing = allNames.filter((n) => envNames.some((e) => !byEnv[e].has(n)));
274
+ console.log('');
275
+ if (missing.length === 0) {
276
+ console.log(color.green('✓ All secrets present on every env.'));
277
+ return;
278
+ }
279
+ console.log(color.yellow(`${missing.length} secrets out of parity:`));
280
+ const rows = missing.map((name) => [
281
+ color.cyan(name),
282
+ ...envNames.map((e) => (byEnv[e].has(name) ? color.green('✓') : color.red('✗'))),
283
+ ]);
284
+ console.log(table(['Secret', ...envNames], rows));
285
+ console.log('');
286
+ console.log(color.dim('Fix with: beehaven secrets set <NAME> (defaults to all envs)'));
287
+ process.exit(1);
288
+ }
289
+ case 'check': {
290
+ const required = readRequiredSecrets(cwd);
291
+ if (required.length === 0) {
292
+ console.log(color.yellow('No requiredSecrets declared in beehaven.json.'));
293
+ console.log(color.dim('Add e.g.: { "requiredSecrets": ["GCP_SERVICE_ACCOUNT_JSON", "..."] }'));
294
+ return;
295
+ }
296
+ const envs = targetEnvs(cfg, opts.env);
297
+ const byEnv = {};
298
+ for (const env of envs) {
299
+ byEnv[envLabel(env)] = new Set(await listSecrets(cwd, env));
300
+ }
301
+ const envNames = Object.keys(byEnv);
302
+ const rows = required.map((name) => [
303
+ color.cyan(name),
304
+ ...envNames.map((e) => (byEnv[e].has(name) ? color.green('✓') : color.red('MISSING'))),
305
+ ]);
306
+ console.log('');
307
+ console.log(table(['Required Secret', ...envNames], rows));
308
+ const missing = required.filter((n) => envNames.some((e) => !byEnv[e].has(n)));
309
+ if (missing.length > 0) {
310
+ console.log('');
311
+ console.log(color.red(`✗ ${missing.length} required secrets missing on at least one env.`));
312
+ process.exit(1);
313
+ }
314
+ console.log('');
315
+ console.log(color.green('✓ All required secrets set on every env.'));
316
+ return;
317
+ }
318
+ default:
319
+ console.error(`Unknown secrets subcommand: ${sub}`);
320
+ console.error('Use: list | set | delete | diff | check');
321
+ process.exit(1);
322
+ }
323
+ }
@@ -0,0 +1,206 @@
1
+ /**
2
+ * self-update — where this `beehaven` came from, and how (or whether) it updates.
3
+ *
4
+ * ## ⚠⚠ THE OLD NOTIFIER GAVE ADVICE THAT BREAKS THE MAC APP'S USERS
5
+ *
6
+ * `checkForUpdates` printed, unconditionally:
7
+ *
8
+ * Update available: 0.7.6 → 0.7.8
9
+ * Run curl -fsSL https://clearly.sh/install.sh | sh to update
10
+ *
11
+ * For anyone whose `beehaven` comes from the Mac app — which is EVERY ordinary user, since the
12
+ * app bundles the CLI and installs the wrapper — following that instruction used to be actively
13
+ * harmful. The wrapper resolved `$BEEHAVEN_CLI` → `~/.beehaven/cli/dist/cli.js` → the bundled
14
+ * copy, in that fixed order, so a curl install created a SECOND CLI that permanently shadowed the
15
+ * app's.
16
+ *
17
+ * ⚠⚠ AND IT WAS PERMANENT IN BOTH DIRECTIONS, WHICH IS WORSE THAN THIS PARAGRAPH ORIGINALLY SAID.
18
+ * The shadowed user copy had no updater either: the app spawns its daemon with
19
+ * `BEEHAVEN_MANAGED_BY`, so `checkAndAutoUpdate` (daemon.ts) SKIPS — correctly, since the app owns
20
+ * the bundle's version. So on a machine with both, the app auto-updated a bundle nothing could
21
+ * reach while `~/.beehaven/cli` froze at whatever was curl-installed, and the terminal was served
22
+ * by neither channel.
23
+ *
24
+ * ✅ FIXED 2026-09-09 IN THE WRAPPER, NOT HERE: `buildWrapperContent` now execs whichever copy
25
+ * reports the higher version, so an app update and a `beehaven update` both reach the shell. The
26
+ * hint below is still per-install, because "how do I update this" still has three answers.
27
+ *
28
+ * ⚠ IT WAS ALSO THE REASON A DEVELOPER NEVER SAW THE BUNDLED CLI GO STALE — their curl install
29
+ * won unconditionally, so they were testing a different binary from the one users get. Under the
30
+ * version comparison they still win whenever their build is newer, which is the normal case.
31
+ *
32
+ * ## The three installs, and what "update" means for each
33
+ *
34
+ * bundled — inside `Clearly*.app/Contents/Resources/beehaven/`. The APP owns it and the app
35
+ * auto-updates (electron-updater), so this CLI already updates; it just does not
36
+ * update ITSELF. Telling this user to curl is the bug above.
37
+ * user — `~/.beehaven/cli/`, from install.sh. A CLI-owned standalone daemon checks hourly;
38
+ * without that daemon, `beehaven update` performs the update on demand.
39
+ * dev-bundled — Clearly Dev.app has no release updater; rebuild and reinstall it.
40
+ * linked — a dev checkout via `npm link`. Never touch it; the developer's build IS the point.
41
+ */
42
+ import { execFileSync } from 'node:child_process';
43
+ import { createHash } from 'node:crypto';
44
+ import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
45
+ import { homedir, tmpdir } from 'node:os';
46
+ import { join, sep } from 'node:path';
47
+ import { fileURLToPath } from 'node:url';
48
+ import { usableUpdateDigest } from '../version-utils.js';
49
+ /**
50
+ * Which install this process is.
51
+ *
52
+ * ⚠ DECIDED FROM THIS RUNNING MODULE'S RESOLVED PATH, not the binary symlink or from what
53
+ * happens to exist on disk. npm global binaries are often symlinks outside node_modules;
54
+ * process.argv[1] would misclassify them as linked development builds. Both a bundled
55
+ * CLI and a user install can be present at once — that is exactly the shadowing case above — so
56
+ * "does `~/.beehaven/cli` exist" answers the wrong question. `process.argv[1]` is the module that
57
+ * was actually loaded.
58
+ */
59
+ export function installKind(entry = fileURLToPath(import.meta.url)) {
60
+ if (/(?:Clearly|Hive) Dev\.app[\\/]Contents[\\/]Resources[\\/]/.test(entry))
61
+ return 'dev-bundled';
62
+ if (entry.includes(`.app${sep}Contents${sep}Resources${sep}`))
63
+ return 'bundled';
64
+ const prefixes = [join(homedir(), '.beehaven'), process.env.BEEHAVEN_PREFIX].filter((p) => Boolean(p));
65
+ if (prefixes.some((prefix) => entry.startsWith(join(prefix, 'cli') + sep)))
66
+ return 'user';
67
+ if (entry.includes(`${sep}_npx${sep}`) && entry.includes(`${sep}node_modules${sep}`))
68
+ return 'npx';
69
+ if (entry.includes(`${sep}node_modules${sep}`))
70
+ return 'npm';
71
+ return 'linked';
72
+ }
73
+ /** The one-line instruction to print when a newer version exists. Per install, because the right
74
+ * answer genuinely differs and a single string is what caused the defect above. */
75
+ export function updateHint(kind = installKind()) {
76
+ if (kind === 'dev-bundled')
77
+ return 'This is a development app build — rebuild and reinstall Clearly Dev.app from the repo.';
78
+ if (kind === 'bundled') {
79
+ /* ⚠ NOT "run the installer". The app updates this copy; a curl install would shadow it. */
80
+ return 'The Clearly app ships this CLI and updates it — open the app, or Tray → Check for updates.';
81
+ }
82
+ if (kind === 'user')
83
+ return 'Run \x1b[1mbeehaven update\x1b[0m';
84
+ if (kind === 'npx')
85
+ return 'Run `npx @clearlysh/beehaven@latest` for the current npm release.';
86
+ if (kind === 'npm')
87
+ return 'Run `npm install -g @clearlysh/beehaven@latest` to update this npm install.';
88
+ return 'This is a linked dev build — `npm run build` in beehaven_sdk/packages/cli.';
89
+ }
90
+ /**
91
+ * Perform the update, for a user install only.
92
+ *
93
+ * ⚠⚠ IT RE-RUNS THE PUBLISHED INSTALLER RATHER THAN REIMPLEMENTING IT. `install.sh` already
94
+ * stages into a temp dir, runs `npm install` to resolve the NATIVE dependencies (better-sqlite3,
95
+ * node-pty — per-platform, per-ABI prebuilds), and only then swaps the directory into place. A
96
+ * hand-rolled updater here would have to reproduce all of that, and would be the copy that rots.
97
+ *
98
+ * ⚠ REPLACING A RUNNING PROGRAM IS SAFE HERE ONLY BECAUSE OF THAT STAGING. The swap is a
99
+ * directory rename after the download and build have both succeeded; this process is already
100
+ * resident in memory and exits normally. An in-place overwrite would not be safe.
101
+ *
102
+ * ⚠ IT REFUSES THE OTHER TWO KINDS RATHER THAN DOING SOMETHING PLAUSIBLE. A bundled install that
103
+ * "updated" itself would create the shadowing copy this whole module exists to prevent, and a
104
+ * linked one would overwrite a developer's working tree.
105
+ */
106
+ export function runSelfUpdate(kind = installKind()) {
107
+ if (kind === 'dev-bundled') {
108
+ return { ok: false, message: 'This CLI is bundled with Clearly Dev.app. Rebuild and reinstall the development app from the repo.' };
109
+ }
110
+ if (kind === 'bundled') {
111
+ return { ok: false, message: 'This CLI is part of the Clearly app and updates with it.\n Open the app, or use Tray → Check for updates.' };
112
+ }
113
+ if (kind === 'linked') {
114
+ return { ok: false, message: 'This is a linked development build — refusing to overwrite it.\n cd beehaven_sdk/packages/cli && npm run build' };
115
+ }
116
+ if (kind === 'npm' || kind === 'npx') {
117
+ return { ok: false, message: updateHint(kind) };
118
+ }
119
+ const feed = (process.env.BEEHAVEN_CLI_FEED || 'https://downloads.clearly.sh/cli/').replace(/\/+$/, '');
120
+ const installerUrl = process.env.BEEHAVEN_INSTALL_URL || 'https://clearly.sh/install.sh';
121
+ const temp = mkdtempSync(join(tmpdir(), 'beehaven-self-update-'));
122
+ try {
123
+ const manifestPath = join(temp, 'latest.json');
124
+ execFileSync('curl', ['-fsSL', '--max-time', '30', '-o', manifestPath, `${feed}/latest.json`]);
125
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
126
+ const version = manifest.version;
127
+ const digest = usableUpdateDigest(manifest);
128
+ if (typeof version !== 'string' || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version) || !digest) {
129
+ return { ok: false, message: 'Update failed: the release manifest has no valid version and sha256.' };
130
+ }
131
+ const tarball = join(temp, 'beehaven.tgz');
132
+ execFileSync('curl', ['-fsSL', '--max-time', '120', '-o', tarball, `${feed}/beehaven-${version}.tgz`]);
133
+ const actual = createHash('sha256').update(readFileSync(tarball)).digest('hex');
134
+ if (actual !== digest)
135
+ return { ok: false, message: 'Update failed: release sha256 mismatch.' };
136
+ // Download the installer as a file. A `curl | sh` pipeline reports sh's success even when
137
+ // curl fails, so it can say "Updated" without ever running an installer.
138
+ const installer = join(temp, 'install.sh');
139
+ execFileSync('curl', ['-fsSL', '--max-time', '30', '-o', installer, installerUrl]);
140
+ execFileSync('/bin/sh', [installer], {
141
+ stdio: 'inherit',
142
+ timeout: 10 * 60_000,
143
+ env: { ...process.env, BEEHAVEN_TARBALL: `file://${tarball}` },
144
+ });
145
+ const installed = JSON.parse(readFileSync(join(process.env.BEEHAVEN_PREFIX || join(homedir(), '.beehaven'), 'cli', 'package.json'), 'utf8'));
146
+ if (installed.version !== version)
147
+ return { ok: false, message: `Update failed: installed ${installed.version ?? 'unknown'}, expected ${version}.` };
148
+ return { ok: true, message: `Updated to ${version}.` };
149
+ }
150
+ catch (e) {
151
+ return { ok: false, message: `Update failed: ${String(e?.message ?? e).slice(0, 200)}` };
152
+ }
153
+ finally {
154
+ rmSync(temp, { recursive: true, force: true });
155
+ }
156
+ }
157
+ /** True when this install can update itself at all — what the notifier uses to pick its wording. */
158
+ export function canSelfUpdate(kind = installKind()) {
159
+ return kind === 'user';
160
+ }
161
+ /**
162
+ * Is a bundled CLI present that the shell can NEVER reach, whatever its version?
163
+ *
164
+ * ⚠⚠ THIS REPLACES `bundledCliShadowed`, WHICH BECAME WRONG ADVICE (2026-09-09). That function
165
+ * answered "is a bundle present alongside a user install", and the `update` command turned a
166
+ * `true` into *"this install shadows it — remove ~/.beehaven/cli to go back to the app-managed
167
+ * copy."* Since the app's wrapper started resolving by VERSION rather than by fixed order
168
+ * (`cli-installer.ts:buildWrapperContent`), that is advice to go BACKWARDS: it fired immediately
169
+ * after `beehaven update`, at the exact moment the freshly-installed copy is the newer one and
170
+ * SHOULD win, and told the user to delete it.
171
+ *
172
+ * ⚠ RENAMED RATHER THAN RE-POINTED, because the question it answers is genuinely different. A
173
+ * name that still says "shadowed" over a body that means "unreachable" is how the next reader
174
+ * restores the old warning.
175
+ *
176
+ * The one configuration that is still a real problem: a wrapper on PATH that the app does NOT
177
+ * own. `install.sh` writes an unmarked shim (`exec node <user cli>`) with no chain in it at all,
178
+ * and `cli-installer` correctly refuses to take a path it did not write — so on that machine the
179
+ * bundle is unreachable REGARDLESS of which copy is newer, and an app update can never arrive.
180
+ * That is worth saying; "both are installed" is not.
181
+ *
182
+ * ⚠ THE MAGIC HEADER IS THE TEST, not the file's existence. It is the app's own claim of
183
+ * ownership and the only thing that distinguishes a wrapper that compares versions from one that
184
+ * hardcodes a single path. Kept in step with `WRAPPER_MAGIC` in `cli-installer.ts`.
185
+ */
186
+ export function bundledCliUnreachable(kind = installKind()) {
187
+ if (kind !== 'user')
188
+ return false;
189
+ const bundlePresent = ['/Applications/Clearly.app', '/Applications/Clearly Dev.app', '/Applications/Clearly STG.app']
190
+ .some((a) => existsSync(join(a, 'Contents', 'Resources', 'beehaven', 'cli.js')));
191
+ if (!bundlePresent)
192
+ return false;
193
+ /* No wrapper at all ⇒ the app has not installed one yet; its next launch will, and that one
194
+ compares versions. Nothing to warn about. */
195
+ const wrapper = join(homedir(), '.local', 'bin', 'beehaven');
196
+ if (!existsSync(wrapper))
197
+ return false;
198
+ try {
199
+ return !readFileSync(wrapper, 'utf8').includes('managed-by: clearly-mac-app');
200
+ }
201
+ catch {
202
+ /* Unreadable ⇒ we cannot prove there is a problem, and a false alarm here tells someone to
203
+ delete a working install. Say nothing. */
204
+ return false;
205
+ }
206
+ }