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.
- package/LICENSE +21 -0
- package/README.md +201 -0
- package/dist/__tests__/_cli-switch.js +105 -0
- package/dist/__tests__/agent-wrap-probe.js +136 -0
- package/dist/agent-attribution.js +192 -0
- package/dist/agent-identity-daemon.js +135 -0
- package/dist/agent-onboard.js +156 -0
- package/dist/agent-scope.js +79 -0
- package/dist/agent-sessions.js +370 -0
- package/dist/agent-state.js +482 -0
- package/dist/app-manifest.js +221 -0
- package/dist/app-signer.js +54 -0
- package/dist/app-signing.js +127 -0
- package/dist/audio-play.js +98 -0
- package/dist/avatar-protocol.js +25 -0
- package/dist/avatar-route.js +79 -0
- package/dist/bee/cloud-proxy.js +130 -0
- package/dist/blocked-actions.js +120 -0
- package/dist/build-channel.js +101 -0
- package/dist/buildings/daemon-workspace.js +475 -0
- package/dist/claude-bridge.js +310 -0
- package/dist/claude-credential-store.js +633 -0
- package/dist/clearly-sync.js +518 -0
- package/dist/clearly.js +616 -0
- package/dist/cli-state.js +334 -0
- package/dist/cli.js +6973 -0
- package/dist/commands/agent-skills.js +302 -0
- package/dist/commands/agent.js +1310 -0
- package/dist/commands/app.js +1123 -0
- package/dist/commands/auth.js +171 -0
- package/dist/commands/building-shell.js +392 -0
- package/dist/commands/canvas.js +932 -0
- package/dist/commands/claude.js +218 -0
- package/dist/commands/compliance.js +387 -0
- package/dist/commands/compose.js +507 -0
- package/dist/commands/composition.js +95 -0
- package/dist/commands/connect-session.js +409 -0
- package/dist/commands/conversations.js +160 -0
- package/dist/commands/costs.js +443 -0
- package/dist/commands/d1.js +253 -0
- package/dist/commands/daemon-cmd.js +162 -0
- package/dist/commands/deploy.js +164 -0
- package/dist/commands/design.js +617 -0
- package/dist/commands/dev-generate.js +125 -0
- package/dist/commands/dev.js +109 -0
- package/dist/commands/devices.js +135 -0
- package/dist/commands/explain-router.js +24 -0
- package/dist/commands/explain.js +882 -0
- package/dist/commands/explore.js +264 -0
- package/dist/commands/help.js +2418 -0
- package/dist/commands/init.js +408 -0
- package/dist/commands/install.js +129 -0
- package/dist/commands/library.js +229 -0
- package/dist/commands/marketplace.js +138 -0
- package/dist/commands/mcp-install.js +476 -0
- package/dist/commands/mention.js +257 -0
- package/dist/commands/omni.js +529 -0
- package/dist/commands/secrets.js +323 -0
- package/dist/commands/self-update.js +206 -0
- package/dist/commands/sheet.js +264 -0
- package/dist/commands/shell-output.js +309 -0
- package/dist/commands/shell.js +1048 -0
- package/dist/commands/shop.js +368 -0
- package/dist/commands/skills.js +311 -0
- package/dist/commands/slides.js +418 -0
- package/dist/commands/swarm.js +341 -0
- package/dist/commands/ticket.js +1301 -0
- package/dist/commands/tool.js +312 -0
- package/dist/commands/trash.js +106 -0
- package/dist/commands/treehouse.js +120 -0
- package/dist/commands/user.js +240 -0
- package/dist/commands/voice-tts.js +319 -0
- package/dist/commands/voice.js +166 -0
- package/dist/commands/watch-git.js +401 -0
- package/dist/commands/webhook.js +159 -0
- package/dist/commands/workspace.js +3354 -0
- package/dist/composition-repos.js +91 -0
- package/dist/daemon-avatar.js +130 -0
- package/dist/daemon-claude-code-auto-install.js +52 -0
- package/dist/daemon-client.js +363 -0
- package/dist/daemon-codex.js +349 -0
- package/dist/daemon-entry.js +101 -0
- package/dist/daemon-fetch-allowlist.js +194 -0
- package/dist/daemon-gemini.js +224 -0
- package/dist/daemon-mcp-clearly.js +353 -0
- package/dist/daemon-mcp-sandbox.js +127 -0
- package/dist/daemon-omni-company.js +338 -0
- package/dist/daemon-omni-hooks.js +375 -0
- package/dist/daemon-omni-identity.js +229 -0
- package/dist/daemon-omni-pipeline.js +251 -0
- package/dist/daemon-omni-policies.js +346 -0
- package/dist/daemon-omni-principal.js +580 -0
- package/dist/daemon-omni-tool-gate.js +116 -0
- package/dist/daemon-omni.js +1442 -0
- package/dist/daemon-path-transform.js +120 -0
- package/dist/daemon-write-sandbox.js +191 -0
- package/dist/daemon.js +4471 -0
- package/dist/dashboard.js +846 -0
- package/dist/dep-installer.js +286 -0
- package/dist/dev-mode.js +67 -0
- package/dist/doc-indexer.js +169 -0
- package/dist/e2e-crypto.js +94 -0
- package/dist/feature-flags.js +154 -0
- package/dist/file-browser.js +88 -0
- package/dist/first-party-hives.js +211 -0
- package/dist/folder-sync.js +226 -0
- package/dist/health.js +56 -0
- package/dist/ipc-auth.js +299 -0
- package/dist/ipc-client.js +495 -0
- package/dist/ipc-server.js +684 -0
- package/dist/keychain.js +224 -0
- package/dist/lib/admin-client.js +73 -0
- package/dist/lib/mobile-build-parser.js +156 -0
- package/dist/local-app-manager.js +660 -0
- package/dist/log-redaction.js +89 -0
- package/dist/logger.js +108 -0
- package/dist/manifest.js +143 -0
- package/dist/marketplace.js +793 -0
- package/dist/min-version-copy.js +51 -0
- package/dist/min-version.js +214 -0
- package/dist/public-surface.js +23 -0
- package/dist/relay-presets.js +89 -0
- package/dist/relay.js +2723 -0
- package/dist/repl.js +619 -0
- package/dist/setup-relay.js +363 -0
- package/dist/setup-service.js +536 -0
- package/dist/shared/bee-memory/index.js +6 -0
- package/dist/shared/bee-memory/types.js +682 -0
- package/dist/shared/claude-code/daemon-types.js +112 -0
- package/dist/shared/claude-code/index.js +2 -0
- package/dist/shared/claude-code/types.js +1 -0
- package/dist/shared/status/schema.js +133 -0
- package/dist/shell-session.js +198 -0
- package/dist/speak-lock.js +97 -0
- package/dist/spinner.js +57 -0
- package/dist/telemetry.js +166 -0
- package/dist/terminal/block-parser.js +207 -0
- package/dist/terminal/shell-integration.js +108 -0
- package/dist/transcript-tailer.js +215 -0
- package/dist/tts-qwen3.js +547 -0
- package/dist/types.js +4 -0
- package/dist/utm.js +38 -0
- package/dist/verb-surface.js +331 -0
- package/dist/version-utils.js +85 -0
- package/dist/watcher.js +207 -0
- package/package.json +88 -0
- package/python/qwen3_tts.py +230 -0
- package/scripts/stamp-build-channel.mjs +115 -0
- package/scripts/sync-installed.mjs +159 -0
- package/scripts/sync-skills.mjs +112 -0
- package/skills/README.md +24 -0
- package/skills/agent-doodle-space/SKILL.md +118 -0
- package/skills/agent-home/SKILL.md +98 -0
- package/skills/beehaven-cli/SKILL.md +144 -0
- package/skills/claude-shopper/README.md +90 -0
- package/skills/claude-shopper/SKILL.md +151 -0
- package/skills/clearly-agent/SKILL.md +183 -0
- package/skills/clearly-canvas/SKILL.md +222 -0
- package/skills/clearly-canvas/authoring.md +160 -0
- package/skills/clearly-canvas/communications.md +32 -0
- package/skills/clearly-canvas/composition-patterns.md +84 -0
- package/skills/clearly-canvas/example.mjs +78 -0
- package/skills/clearly-canvas/project-plans.md +156 -0
- package/skills/clearly-canvas/review.mjs +68 -0
- package/skills/clearly-workspace/SKILL.md +79 -0
- package/skills/clearly-workspace/survey.mjs +101 -0
- package/skills/codebase-map/SKILL.md +142 -0
- package/skills/design-craft/SKILL.md +300 -0
- package/skills/index.json +79 -0
- package/skills/layout-systems/SKILL.md +184 -0
- package/skills/pair-on-canvas/SKILL.md +92 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `beehaven slides` — presentations.
|
|
3
|
+
*
|
|
4
|
+
* ⚠⚠ SEVENTEEN `deck-*` ACTIONS AND NO CLI DOOR. Same shape as `sheet`: a first-class artifact
|
|
5
|
+
* — a deck is a document row, with a key, project filing, trash and search like anything else —
|
|
6
|
+
* reachable only as `beehaven call deck-add-slide '{…}'`. Thirteen of the seventeen also had no
|
|
7
|
+
* input schema until 2026-09-05, so the arguments could only be learned by reading the handler.
|
|
8
|
+
*
|
|
9
|
+
* ⚠ THE TRAPS THIS MAKES UNTYPEABLE:
|
|
10
|
+
*
|
|
11
|
+
* · `deck-move-slide` REQUIRES `to` and refuses without it (`code: "no-target"`) — not something
|
|
12
|
+
* the verb name suggests. This verb takes it positionally, so it cannot be forgotten.
|
|
13
|
+
* · `deck-render` vs `deck-export` are near-synonyms that do different jobs: render is to LOOK
|
|
14
|
+
* at a deck, export is to take the FILE out. Two subcommands, `show` and `export`, named for
|
|
15
|
+
* the job rather than the mechanism.
|
|
16
|
+
* · A layout typed from memory does not error — an unrecognised one silently falls back to
|
|
17
|
+
* `titleBody`. `slides layouts` is one call and is named in the refusal.
|
|
18
|
+
*
|
|
19
|
+
* ⚠ `agentRpc`, NEVER bare `rpcOnce` — see the note in `ticket.ts`.
|
|
20
|
+
*/
|
|
21
|
+
import { agentRpc as rpcOnce } from './workspace.js';
|
|
22
|
+
/** Only `pptx` returns a `filename`; the text formats return bare `content`. Falling back to
|
|
23
|
+
* `${id}.${format}` wrote `PLA-3518.markdown` — an id nobody recognises and an extension no
|
|
24
|
+
* editor associates. Fall back to the deck's TITLE and a real extension. */
|
|
25
|
+
export const DECK_EXPORT_EXT = {
|
|
26
|
+
markdown: 'md', notes: 'txt', html: 'html', svg: 'svg', pptx: 'pptx',
|
|
27
|
+
// `slides show`'s formats. `png` is the only one that arrives as a data: URI.
|
|
28
|
+
png: 'png', tsv: 'tsv',
|
|
29
|
+
};
|
|
30
|
+
/** Extension for a `data:` URI's mime type. `image/svg+xml` → `svg`, not `svg+xml`. */
|
|
31
|
+
export const DATA_URI_EXT = {
|
|
32
|
+
'image/png': 'png', 'image/jpeg': 'jpg', 'image/webp': 'webp', 'image/svg+xml': 'svg',
|
|
33
|
+
'application/pdf': 'pdf',
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* ⚠⚠ THE EXPORT DECISION, EXTRACTED SO A GUARD CAN EXECUTE IT (CBH2-42).
|
|
37
|
+
*
|
|
38
|
+
* This logic used to live inline in the `export` branch, which made it unreachable from a test —
|
|
39
|
+
* so its guard (`slides-export-writes.test.ts`) could only read the file as TEXT and assert that
|
|
40
|
+
* identifiers like `writeFileSync` appeared somewhere in it. That guard called itself behavioural
|
|
41
|
+
* and was not: every string it matched also survives in a COMMENT, so the fix could be deleted and
|
|
42
|
+
* the suite stayed green. Extraction IS the fix to that criticism, not a tidy-up — the same move
|
|
43
|
+
* that rescued `clipToRange` in `sheet-rpcs.ts`.
|
|
44
|
+
*
|
|
45
|
+
* Everything here is a DECISION; the caller does the IO (two `writeFileSync`, one `console.log`).
|
|
46
|
+
* `exists` and `resolvePath` are INJECTED rather than imported, so the clobber refusal and the
|
|
47
|
+
* filename derivation can both be exercised without touching a real filesystem or a real cwd.
|
|
48
|
+
*
|
|
49
|
+
* ⚠ BASE64 vs TEXT IS DECIDED BY WHICH FIELD CAME BACK, never by the format name. Keying on
|
|
50
|
+
* `format === 'pptx'` breaks the day another binary format is added, and writing a base64 STRING
|
|
51
|
+
* into a .pptx produces a file that opens as garbage rather than failing loudly.
|
|
52
|
+
*
|
|
53
|
+
* Sabotage-tested (2026-09-05): removing the clobber refusal fails 1, keying on the format name
|
|
54
|
+
* fails 3, and falling back to `id.format` fails 2.
|
|
55
|
+
*
|
|
56
|
+
* ⚠⚠ THREE VERBS SHARE IT NOW (CBH2-85): `slides export`, `slides show` and `sheet get`. `--out`
|
|
57
|
+
* and `-o` are registered GLOBALLY in `parseArgs`, so EVERY verb accepts them — acceptance has
|
|
58
|
+
* never been evidence of support, and those two rendered/printed and wrote no file. The clobber
|
|
59
|
+
* refusal and the extension fallback are both non-obvious and already tested; a second and third
|
|
60
|
+
* copy of them is how one caller keeps refusing to overwrite while another silently does.
|
|
61
|
+
*
|
|
62
|
+
* ⚠ IT IS SHAPED BY THE PAYLOAD, NOT BY THE VERB, which is what lets a sheet reuse it: hand it
|
|
63
|
+
* `{ content, filename }` and it plans one text file, the same way `deck-export`'s markdown does.
|
|
64
|
+
* The name stays `planDeckExport` — renaming it would churn every guard that names it for no gain.
|
|
65
|
+
*
|
|
66
|
+
* ⚠⚠ A `data:` URI IS BINARY AND MUST BE DECODED. `deck-render { format: 'png' }` answers with
|
|
67
|
+
* `slides: ['data:image/png;base64,…']`, and writing those STRINGS to `.png` files produces images
|
|
68
|
+
* that open as garbage — exactly the failure the base64-vs-text rule above exists to prevent, one
|
|
69
|
+
* payload shape further out. The extension follows the URI's own mime type rather than the
|
|
70
|
+
* requested format, so a fallback to SVG (which `deck-render` does silently when rasterising
|
|
71
|
+
* fails) lands in a file named `.svg`, not a `.png` containing SVG.
|
|
72
|
+
*/
|
|
73
|
+
export function planDeckExport(res, opts, exists, resolvePath) {
|
|
74
|
+
const fmt = String(opts.format ?? 'pptx');
|
|
75
|
+
const ext = DECK_EXPORT_EXT[fmt] ?? fmt;
|
|
76
|
+
const name = String(res?.filename || `${res?.title || opts.id}.${ext}`);
|
|
77
|
+
/**
|
|
78
|
+
* ⚠⚠ `svg` IS A THIRD PAYLOAD SHAPE — `slides: [...]`, one entry per slide, with NO `base64`
|
|
79
|
+
* and NO `content`. Measured on a 12-slide deck. The first cut of this verb knew only the two
|
|
80
|
+
* shapes, so `--format svg` refused with "neither base64 nor content" on a format the help
|
|
81
|
+
* offers. **A deck of N slides is N SVGs** — there is no single-file form to fall back to, so
|
|
82
|
+
* `--out` names the BASE and the slides land beside it as `<base>-1.svg` … `<base>-N.svg`.
|
|
83
|
+
*/
|
|
84
|
+
const slides = Array.isArray(res?.slides) ? res.slides : [];
|
|
85
|
+
if (slides.length) {
|
|
86
|
+
/**
|
|
87
|
+
* ⚠ ANY KNOWN EXTENSION IS STRIPPED, NOT ONLY THE ONE THIS RUN EMITS. `slides show --out
|
|
88
|
+
* shot.png` whose render DEGRADES to SVG would otherwise write `shot.png-1.svg` — a name
|
|
89
|
+
* carrying two extensions, the first of which is wrong. Only extensions this CLI itself emits
|
|
90
|
+
* are stripped, so `My.Deck.Name` keeps its dots.
|
|
91
|
+
*/
|
|
92
|
+
const known = new Set([...Object.values(DECK_EXPORT_EXT), ...Object.values(DATA_URI_EXT)]);
|
|
93
|
+
const base = String(opts.out ?? name).replace(/\.([A-Za-z0-9]+)$/, (m, e) => (known.has(String(e).toLowerCase()) ? '' : m));
|
|
94
|
+
const files = slides.map((s, i) => {
|
|
95
|
+
const body = typeof s === 'string' ? s : String(s?.svg ?? s?.content ?? '');
|
|
96
|
+
// ⚠ See the header: a `data:` URI carries BINARY, and its own mime names the extension.
|
|
97
|
+
const uri = /^data:([\w.+-]+\/[\w.+-]+);base64,(.*)$/s.exec(body);
|
|
98
|
+
if (uri) {
|
|
99
|
+
const mimeExt = DATA_URI_EXT[uri[1]] ?? uri[1].split('/')[1] ?? ext;
|
|
100
|
+
return {
|
|
101
|
+
dest: resolvePath(`${base}-${i + 1}.${mimeExt}`),
|
|
102
|
+
body: { kind: 'binary', base64: uri[2] },
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return { dest: resolvePath(`${base}-${i + 1}.${ext}`), body: { kind: 'text', content: body } };
|
|
106
|
+
});
|
|
107
|
+
const clash = opts.force ? null : files.find((f) => exists(f.dest));
|
|
108
|
+
if (clash) {
|
|
109
|
+
return { ok: false, code: 'exists', error: `${clash.dest} already exists`,
|
|
110
|
+
hint: `This export writes ${files.length} files. Pass --force to overwrite, or --out <base> to write elsewhere.` };
|
|
111
|
+
}
|
|
112
|
+
return { ok: true, files };
|
|
113
|
+
}
|
|
114
|
+
const dest = resolvePath(String(opts.out ?? name));
|
|
115
|
+
// ⚠ An export is a READ. Silently overwriting a neighbouring file is not something a read
|
|
116
|
+
// should be able to do; `--force` is the opt-in.
|
|
117
|
+
if (exists(dest) && !opts.force) {
|
|
118
|
+
return { ok: false, code: 'exists', error: `${dest} already exists`,
|
|
119
|
+
hint: 'Pass --force to overwrite, or --out <path> to write elsewhere.' };
|
|
120
|
+
}
|
|
121
|
+
if (typeof res?.base64 === 'string' && res.base64) {
|
|
122
|
+
return { ok: true, files: [{ dest, body: { kind: 'binary', base64: res.base64 } }] };
|
|
123
|
+
}
|
|
124
|
+
if (typeof res?.content === 'string') {
|
|
125
|
+
return { ok: true, files: [{ dest, body: { kind: 'text', content: res.content } }] };
|
|
126
|
+
}
|
|
127
|
+
// ⚠ Say what came back rather than writing an empty file that looks like a successful export.
|
|
128
|
+
return { ok: false, code: 'no-payload',
|
|
129
|
+
error: 'the export returned neither `base64`, `content` nor `slides` — nothing to write',
|
|
130
|
+
hint: `Keys returned: ${Object.keys(res ?? {}).join(', ')}` };
|
|
131
|
+
}
|
|
132
|
+
const c = {
|
|
133
|
+
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
134
|
+
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
135
|
+
};
|
|
136
|
+
function getTimeout(values) {
|
|
137
|
+
const t = Number(values?.timeout);
|
|
138
|
+
return Number.isFinite(t) && t > 0 ? t : 30_000;
|
|
139
|
+
}
|
|
140
|
+
function failure(res) {
|
|
141
|
+
if (!res || typeof res !== 'object')
|
|
142
|
+
return null;
|
|
143
|
+
if (typeof res.error === 'string' && res.error)
|
|
144
|
+
return res.error;
|
|
145
|
+
if (res.ok === false)
|
|
146
|
+
return String(res.message ?? res.code ?? 'the workspace refused the call');
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
function die(msg, hint) {
|
|
150
|
+
console.error(` ${msg}`);
|
|
151
|
+
if (hint)
|
|
152
|
+
console.error(c.dim(` ${hint}`));
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
export async function cmdSlides(sub, rest, values) {
|
|
156
|
+
const t = getTimeout(values);
|
|
157
|
+
if (!sub || sub === 'help') {
|
|
158
|
+
console.log(`
|
|
159
|
+
${c.cyan('beehaven slides')} — presentations
|
|
160
|
+
|
|
161
|
+
slides list Every deck, newest first
|
|
162
|
+
slides new "<title>" Create one (--project <id>, --aspect 16:9|4:3)
|
|
163
|
+
slides get <id> The outline (--slide N for one slide in full)
|
|
164
|
+
slides add <id> "<title>" Append a slide (--layout, --bullets "a|b|c", --notes, --at N)
|
|
165
|
+
slides move <id> <n> <to> Reorder — <to> is the 1-based destination
|
|
166
|
+
slides rm <id> <n> Delete a slide
|
|
167
|
+
slides layouts The layout ids you may pass to --layout
|
|
168
|
+
slides show <id> Render it to look at (--slide N)
|
|
169
|
+
slides export <id> Take the file out (--format pptx|html|markdown|notes|svg)
|
|
170
|
+
|
|
171
|
+
${c.dim('<id> is a deck id or its key. <n> is a 1-based slide number or a slide id.')}
|
|
172
|
+
${c.dim('show renders a picture; export produces a file. They are not the same verb.')}
|
|
173
|
+
`);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (sub === 'list') {
|
|
177
|
+
const res = await rpcOnce('deck-list', {}, t, values);
|
|
178
|
+
const f = failure(res);
|
|
179
|
+
if (f)
|
|
180
|
+
die(f);
|
|
181
|
+
if (values.json) {
|
|
182
|
+
console.log(JSON.stringify(res, null, 2));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const rows = res?.decks ?? [];
|
|
186
|
+
if (!rows.length) {
|
|
187
|
+
console.log(' No decks.');
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
console.log('');
|
|
191
|
+
for (const d of rows) {
|
|
192
|
+
const n = d.slideCount ?? d.slides?.length;
|
|
193
|
+
console.log(` ${c.cyan(String(d.key ?? d.id ?? ''))} ${String(d.title ?? 'Untitled')}${n != null ? c.dim(` · ${n} slides`) : ''}`);
|
|
194
|
+
}
|
|
195
|
+
console.log('');
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (sub === 'new' || sub === 'create') {
|
|
199
|
+
const title = rest.join(' ').trim();
|
|
200
|
+
if (!title)
|
|
201
|
+
die('a title is required', 'beehaven slides new "Q1 review"');
|
|
202
|
+
const res = await rpcOnce('deck-create', {
|
|
203
|
+
title,
|
|
204
|
+
...(values.aspect ? { aspect: String(values.aspect) } : {}),
|
|
205
|
+
...(values.project ? { projectId: String(values.project) } : {}),
|
|
206
|
+
}, t, values);
|
|
207
|
+
const f = failure(res);
|
|
208
|
+
if (f)
|
|
209
|
+
die(f);
|
|
210
|
+
if (values.json) {
|
|
211
|
+
console.log(JSON.stringify(res, null, 2));
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
console.log(` Created ${c.cyan(String(res?.key ?? res?.id ?? ''))}`);
|
|
215
|
+
console.log(c.dim(' Add slides: beehaven slides add <id> "<title>" --bullets "a|b|c"'));
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (sub === 'get') {
|
|
219
|
+
const id = rest[0];
|
|
220
|
+
if (!id)
|
|
221
|
+
die('a deck id or key is required', 'beehaven slides get CLR-42');
|
|
222
|
+
const res = await rpcOnce('deck-get', {
|
|
223
|
+
id,
|
|
224
|
+
...(values.slide ? { slide: Number(values.slide) } : {}),
|
|
225
|
+
}, t, values);
|
|
226
|
+
const f = failure(res);
|
|
227
|
+
if (f)
|
|
228
|
+
die(f);
|
|
229
|
+
if (values.json) {
|
|
230
|
+
console.log(JSON.stringify(res, null, 2));
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const outline = res?.outline ?? [];
|
|
234
|
+
if (!outline.length) {
|
|
235
|
+
console.log(JSON.stringify(res, null, 2));
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
console.log('');
|
|
239
|
+
outline.forEach((s, i) => console.log(` ${String(i + 1).padStart(2)}. ${String(s.title ?? s.layout ?? '')}`));
|
|
240
|
+
console.log('');
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (sub === 'add') {
|
|
244
|
+
const [id, ...titleParts] = rest;
|
|
245
|
+
if (!id)
|
|
246
|
+
die('a deck id or key is required', 'beehaven slides add CLR-42 "Results"');
|
|
247
|
+
const res = await rpcOnce('deck-add-slide', {
|
|
248
|
+
id,
|
|
249
|
+
...(titleParts.length ? { title: titleParts.join(' ') } : {}),
|
|
250
|
+
...(values.layout ? { layout: String(values.layout) } : {}),
|
|
251
|
+
// ⚠ PIPE-SEPARATED, not comma — a bullet routinely contains a comma and almost never a pipe.
|
|
252
|
+
...(values.bullets ? { bullets: String(values.bullets).split('|').map((b) => b.trim()).filter(Boolean) } : {}),
|
|
253
|
+
...(values.notes ? { notes: String(values.notes) } : {}),
|
|
254
|
+
...(values.at ? { at: Number(values.at) } : {}),
|
|
255
|
+
}, t, values);
|
|
256
|
+
const f = failure(res);
|
|
257
|
+
if (f)
|
|
258
|
+
die(f);
|
|
259
|
+
if (values.json) {
|
|
260
|
+
console.log(JSON.stringify(res, null, 2));
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
console.log(` Added slide to ${c.cyan(id)}`);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (sub === 'move') {
|
|
267
|
+
const [id, slide, to] = rest;
|
|
268
|
+
// ⚠ `to` POSITIONALLY, because the action refuses without it and the verb name gives no hint.
|
|
269
|
+
if (!id || !slide || !to) {
|
|
270
|
+
die('usage: beehaven slides move <id> <n> <to>', 'e.g. beehaven slides move CLR-42 5 2 — <to> is the 1-based destination and is required.');
|
|
271
|
+
}
|
|
272
|
+
const res = await rpcOnce('deck-move-slide', { id, slide, to: Number(to) }, t, values);
|
|
273
|
+
const f = failure(res);
|
|
274
|
+
if (f)
|
|
275
|
+
die(f);
|
|
276
|
+
if (values.json) {
|
|
277
|
+
console.log(JSON.stringify(res, null, 2));
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
console.log(` Moved slide ${slide} → ${to} in ${c.cyan(id)}`);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (sub === 'rm' || sub === 'delete') {
|
|
284
|
+
const [id, slide] = rest;
|
|
285
|
+
if (!id || !slide)
|
|
286
|
+
die('usage: beehaven slides rm <id> <n>');
|
|
287
|
+
const res = await rpcOnce('deck-delete-slide', { id, slide }, t, values);
|
|
288
|
+
const f = failure(res);
|
|
289
|
+
if (f)
|
|
290
|
+
die(f);
|
|
291
|
+
if (values.json) {
|
|
292
|
+
console.log(JSON.stringify(res, null, 2));
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
// ⚠ The action returns the slide it removed precisely so this can say WHAT went.
|
|
296
|
+
const gone = res?.removed?.title;
|
|
297
|
+
console.log(` Removed slide ${slide}${gone ? ` — ${c.dim(String(gone))}` : ''} from ${c.cyan(id)}`);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (sub === 'layouts') {
|
|
301
|
+
const res = await rpcOnce('deck-layouts', {}, t, values);
|
|
302
|
+
const f = failure(res);
|
|
303
|
+
if (f)
|
|
304
|
+
die(f);
|
|
305
|
+
if (values.json) {
|
|
306
|
+
console.log(JSON.stringify(res, null, 2));
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
console.log('');
|
|
310
|
+
for (const l of (res?.layouts ?? []))
|
|
311
|
+
console.log(` ${c.cyan(String(l.id))} ${c.dim(String(l.label ?? ''))}`);
|
|
312
|
+
console.log('');
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (sub === 'show' || sub === 'render') {
|
|
316
|
+
const id = rest[0];
|
|
317
|
+
if (!id)
|
|
318
|
+
die('a deck id or key is required');
|
|
319
|
+
const res = await rpcOnce('deck-render', {
|
|
320
|
+
id,
|
|
321
|
+
...(values.slide ? { slide: Number(values.slide) } : {}),
|
|
322
|
+
...(values.format ? { format: String(values.format) } : {}),
|
|
323
|
+
}, t, values);
|
|
324
|
+
const f = failure(res);
|
|
325
|
+
if (f)
|
|
326
|
+
die(f);
|
|
327
|
+
/**
|
|
328
|
+
* ⚠⚠ IT ACCEPTED `--out` AND WROTE NOTHING (CBH2-85) — `--out`/`-o` are registered globally in
|
|
329
|
+
* `parseArgs`, so this verb took the flag, ignored it, and printed a JSON envelope holding
|
|
330
|
+
* megabytes of base64 PNG to the terminal. Same accept-and-ignore class as `slides export`
|
|
331
|
+
* before CBH2-2, and the same fix: `planDeckExport` owns the decision, this branch does IO.
|
|
332
|
+
*
|
|
333
|
+
* ⚠ THE FLAG IS THE OPT-IN. Without it the behaviour is byte-identical to before — a render is
|
|
334
|
+
* usually looked at, not saved, and writing files from a read verb by default is a surprise.
|
|
335
|
+
*/
|
|
336
|
+
const outPath = (values.out ?? values.output);
|
|
337
|
+
if (!outPath) {
|
|
338
|
+
console.log(JSON.stringify(res, null, 2));
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const { writeFileSync, existsSync } = await import('fs');
|
|
342
|
+
const { resolve } = await import('path');
|
|
343
|
+
const plan = planDeckExport(res,
|
|
344
|
+
// ⚠ THE SERVER'S OWN `format`, NOT THE REQUESTED ONE. `deck-render` falls back to SVG when
|
|
345
|
+
// rasterising fails (`degraded`), so trusting `--format png` would name a `.png` holding SVG.
|
|
346
|
+
{ format: String(res?.format ?? values.format ?? 'svg'), out: outPath, id, force: !!values.force }, existsSync, resolve);
|
|
347
|
+
if (!plan.ok)
|
|
348
|
+
die(plan.error, plan.hint);
|
|
349
|
+
if (res?.degraded)
|
|
350
|
+
console.log(` ${c.dim(`⚠ ${String(res.degraded)}`)}`);
|
|
351
|
+
for (const file of plan.files) {
|
|
352
|
+
const buf = file.body.kind === 'binary'
|
|
353
|
+
? Buffer.from(file.body.base64, 'base64')
|
|
354
|
+
: Buffer.from(file.body.content, 'utf8');
|
|
355
|
+
writeFileSync(file.dest, buf);
|
|
356
|
+
console.log(` Wrote ${c.cyan(file.dest)}${c.dim(` · ${buf.length} bytes`)}`);
|
|
357
|
+
}
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (sub === 'export') {
|
|
361
|
+
const id = rest[0];
|
|
362
|
+
if (!id)
|
|
363
|
+
die('a deck id or key is required');
|
|
364
|
+
const res = await rpcOnce('deck-export', {
|
|
365
|
+
id,
|
|
366
|
+
format: String(values.format ?? 'pptx'),
|
|
367
|
+
}, t, values);
|
|
368
|
+
const f = failure(res);
|
|
369
|
+
if (f)
|
|
370
|
+
die(f);
|
|
371
|
+
if (values.json) {
|
|
372
|
+
console.log(JSON.stringify(res, null, 2));
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* ⚠⚠ WRITE THE FILE. THIS PRINTED THE PAYLOAD TO STDOUT (fixed 2026-09-05, CBH2-2).
|
|
377
|
+
*
|
|
378
|
+
* The help I wrote for this verb says export "produces a FILE to take away", and it produced
|
|
379
|
+
* no file anywhere — it dumped the envelope to stdout, which for the DEFAULT format (pptx)
|
|
380
|
+
* is 16.5 KB of base64 into the terminal. `--out` and `-o` are registered globally in
|
|
381
|
+
* `parseArgs`, so they were accepted and silently ignored: the exact accept-and-ignore class
|
|
382
|
+
* this sprint keeps finding, in a verb written during the sprint.
|
|
383
|
+
*
|
|
384
|
+
* ⚠ THE ACTION ALREADY HAD EVERYTHING NEEDED — it returns `filename` and either `base64`
|
|
385
|
+
* (pptx) or `content` (text formats). Nothing had to be added server-side; the verb simply
|
|
386
|
+
* never used them.
|
|
387
|
+
*
|
|
388
|
+
* ⚠ BASE64 vs TEXT IS DECIDED BY WHICH FIELD CAME BACK, not by the format name. Keying on
|
|
389
|
+
* `format === 'pptx'` would break the day another binary format is added, and writing a
|
|
390
|
+
* base64 STRING to a .pptx produces a file that opens as garbage rather than failing.
|
|
391
|
+
*
|
|
392
|
+
* ⚠ REFUSES TO CLOBBER. An export is a read; silently overwriting a neighbouring file is not
|
|
393
|
+
* something a read should be able to do. `--force` is the opt-in.
|
|
394
|
+
*/
|
|
395
|
+
const { writeFileSync, existsSync } = await import('fs');
|
|
396
|
+
const { resolve } = await import('path');
|
|
397
|
+
// ⚠ The DECISION is `planDeckExport` (pure, executed by the guard); this branch does only IO.
|
|
398
|
+
const plan = planDeckExport(res, { format: String(values.format ?? 'pptx'), out: (values.out ?? values.output), id, force: !!values.force }, existsSync, resolve);
|
|
399
|
+
if (!plan.ok)
|
|
400
|
+
die(plan.error, plan.hint);
|
|
401
|
+
/**
|
|
402
|
+
* ⚠ THE SIZE IS MEASURED HERE, NOT TAKEN FROM THE SERVER. `deck-export` reports `bytes` as a
|
|
403
|
+
* CHARACTER count: a 3,993-character markdown deck is 4,046 bytes on disk once the em-dashes
|
|
404
|
+
* and ⚠ glyphs are UTF-8 encoded, and the CLI printed the server's 3,993 beside a file that
|
|
405
|
+
* `ls` calls 4,046. Small, and exactly the class this sprint is about — a number that looks
|
|
406
|
+
* like an answer to a question nobody asked it.
|
|
407
|
+
*/
|
|
408
|
+
for (const f of plan.files) {
|
|
409
|
+
const buf = f.body.kind === 'binary'
|
|
410
|
+
? Buffer.from(f.body.base64, 'base64')
|
|
411
|
+
: Buffer.from(f.body.content, 'utf8');
|
|
412
|
+
writeFileSync(f.dest, buf);
|
|
413
|
+
console.log(` Wrote ${c.cyan(f.dest)}${c.dim(` · ${buf.length} bytes`)}`);
|
|
414
|
+
}
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
die(`unknown subcommand "${sub}"`, 'beehaven slides help');
|
|
418
|
+
}
|