slidemind 0.2.2 → 0.2.4
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +8 -1
- package/AGENTS.md +88 -21
- package/README.md +82 -34
- package/assets/app-icon.png +0 -0
- package/bin/slidemind.mjs +1497 -298
- package/package.json +2 -1
- package/skills/create-slides/SKILL.md +32 -28
- package/skills/search/SKILL.md +28 -22
- package/src/bridge/build-and-push.mjs +19 -6
- package/src/bridge/client.mjs +897 -107
- package/src/bridge/config.mjs +2 -2
- package/src/bridge/engine-download.mjs +61 -8
- package/src/bridge/{pair.mjs → http-json.mjs} +9 -24
- package/src/bridge/setup-claim.mjs +127 -0
- package/src/builders/resolve-skill.mjs +64 -2
- package/src/installers/claude.mjs +24 -0
- package/src/installers/codex.mjs +60 -4
- package/src/installers/lib.mjs +49 -19
- package/src/installers/opencode.mjs +16 -0
- package/src/installers/tui.mjs +21 -10
- package/src/pptx/fill/engine.mjs +112 -134
package/bin/slidemind.mjs
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
// slidemind — single CLI entry point for the slidemind npm package.
|
|
3
3
|
//
|
|
4
4
|
// Subcommands:
|
|
5
|
+
// slidemind setup <claim> one-command onboarding from the add-in's
|
|
6
|
+
// "Connect CLI" button: connect + global
|
|
7
|
+
// install + skills + ready (run via npx)
|
|
5
8
|
// slidemind init [--for <target>] set up slidemind in a coding harness
|
|
6
9
|
// (deploy the skill);
|
|
7
10
|
// targets: claude | codex | opencode | all
|
|
@@ -52,11 +55,22 @@ function printHelp() {
|
|
|
52
55
|
`slidemind ${pkg.version} — drive the slidemind PowerPoint add-in to build native, on-brand decks\n` +
|
|
53
56
|
`\n` +
|
|
54
57
|
`Usage:\n` +
|
|
58
|
+
` slidemind setup <claim> One-command onboarding. Copy the command from PowerPoint →\n` +
|
|
59
|
+
` slidemind → Settings → General → Connect CLI and paste it:\n` +
|
|
60
|
+
` it connects this machine, installs slidemind globally, deploys\n` +
|
|
61
|
+
` the skills, and runs 'ready'. Designed for 'npx slidemind@latest'.\n` +
|
|
62
|
+
` --for <target> harness to deploy skills into (default: auto — every\n` +
|
|
63
|
+
` coding agent found on this machine; none to skip)\n` +
|
|
64
|
+
` --no-install skip the global npm install (already installed)\n` +
|
|
65
|
+
` --no-ready skip the closing readiness check\n` +
|
|
66
|
+
` --json machine-readable result\n` +
|
|
55
67
|
` slidemind ready [deck] One-command session setup — run this FIRST each session.\n` +
|
|
56
|
-
` Verifies
|
|
68
|
+
` Verifies authorization/connection/CLI version, resolves the open\n` +
|
|
57
69
|
` design, materializes its bundle, and (with a deck) binds it.\n` +
|
|
58
70
|
` Exit 0 = ready; non-zero prints one actionable blocker.\n` +
|
|
71
|
+
` A deck already bound to a design readies with PowerPoint closed.\n` +
|
|
59
72
|
` --to <presId> bind to a specific open window\n` +
|
|
73
|
+
` --brand <id> bind to a design by id (presFP) — no window needed\n` +
|
|
60
74
|
` --force re-download the bundle even if current\n` +
|
|
61
75
|
` --connect-only check the connection only (no design/bundle)\n` +
|
|
62
76
|
` --json machine-readable readiness payload\n` +
|
|
@@ -68,7 +82,8 @@ function printHelp() {
|
|
|
68
82
|
` blocks until the user decides, prints DECISION: approved | comments\n` +
|
|
69
83
|
` --port <n> bind a fixed port (default: ephemeral)\n` +
|
|
70
84
|
` slidemind concepts <deck> After plan sign-off, generate one concept mockup PNG per planned\n` +
|
|
71
|
-
` slide into <deck>/concepts/concept-slide-NN.png (
|
|
85
|
+
` slide into <deck>/concepts/concept-slide-NN.png (bound deck,\n` +
|
|
86
|
+
` pane-free — no PowerPoint window needed).\n` +
|
|
72
87
|
` Read each as a design target, then rebuild it as slide JSX.\n` +
|
|
73
88
|
` Optional aid — skips cleanly (exit 0) if unavailable.\n` +
|
|
74
89
|
` slidemind build <deck> Build self-contained HTML\n` +
|
|
@@ -76,31 +91,46 @@ function printHelp() {
|
|
|
76
91
|
` approves or leaves comments (host-free)\n` +
|
|
77
92
|
` --port <n> bind a fixed port (default: ephemeral)\n` +
|
|
78
93
|
` slidemind build-pptx <deck> Export native .pptx — via the add-in for compose decks (host), LOCALLY for template packs (host-free)\n` +
|
|
94
|
+
` --pack <dir> explicit template-pack directory (plugin integrations)\n` +
|
|
79
95
|
` --to <presId> target a specific window (required if >1 open)\n` +
|
|
80
96
|
` --out <path> output path (default <deck>/<deck>.pptx)\n` +
|
|
97
|
+
` slidemind inspect-template-deck <deck> Inspect a template deck as JSON for pack-owned validation\n` +
|
|
98
|
+
` --pack <dir> explicit template-pack directory (required)\n` +
|
|
99
|
+
` --allow-unrendered-freeform include JSX inserts without .pptx artifacts\n` +
|
|
100
|
+
` --json print the normalized deck model\n` +
|
|
81
101
|
` slidemind view-draft <deck> Render the deck's slides to per-slide PNGs (no push, no .pptx)\n` +
|
|
82
102
|
` so the agent can look before review; host-free once the\n` +
|
|
83
103
|
` bundle is materialized\n` +
|
|
84
104
|
` --to <presId> window to fetch the bundle from (only if not cached)\n` +
|
|
85
105
|
` --out <dir> output dir (default <deck>/.draft-preview/)\n` +
|
|
86
106
|
` --open open each rendered PNG\n` +
|
|
87
|
-
` slidemind search "<query>" Search an engine or scope for images or slides (
|
|
107
|
+
` slidemind search "<query>" Search an engine or scope for images or slides (no window needed on\n` +
|
|
108
|
+
` add-ins that serve the pane-free engine routes; older ones relay\n` +
|
|
109
|
+
` through an open host)\n` +
|
|
88
110
|
` --engine <id> engine, or scope:all | scope:sharepoint to search unfiltered\n` +
|
|
89
111
|
` (default: first image engine; scope:all for slide intents)\n` +
|
|
90
112
|
` --result-type <t> slide | image — required for a scope:* target\n` +
|
|
91
113
|
` --intent <i> auto | content | visual (slide engines)\n` +
|
|
92
114
|
` --top <N> max results\n` +
|
|
93
|
-
` --
|
|
94
|
-
` --
|
|
95
|
-
`
|
|
115
|
+
` --deck <deck> route by this deck's bound design (multi-environment)\n` +
|
|
116
|
+
` --to <presId> pin a window on the relayed path (ignored pane-free)\n` +
|
|
117
|
+
` --json print the raw result payload (carries the searchId)\n` +
|
|
118
|
+
` slidemind view-slide [opts] Fetch a slide search result's preview image to a file\n` +
|
|
96
119
|
` --search <id> searchId from 'slidemind search'\n` +
|
|
97
120
|
` --pick <n> 1-based selection number to view\n` +
|
|
98
|
-
` --to <presId> the window the search ran on (
|
|
121
|
+
` --to <presId> the window the search ran on (relayed path only)\n` +
|
|
99
122
|
` --out <dir> output dir (default: a slidemind temp dir)\n` +
|
|
100
|
-
` slidemind
|
|
123
|
+
` slidemind read-source [opts] Read the ORIGINAL deck a slide search result came from\n` +
|
|
124
|
+
` --search <id> searchId from 'slidemind search'\n` +
|
|
125
|
+
` --pick <n> 1-based selection number\n` +
|
|
126
|
+
` --from-slide <n> first slide to read (1-based)\n` +
|
|
127
|
+
` --to-slide <n> last slide to read (1-based)\n` +
|
|
128
|
+
` --to <presId> the window the search ran on (relayed path only)\n` +
|
|
129
|
+
` --out <file> write the Markdown to a file instead of stdout\n` +
|
|
130
|
+
` slidemind ls engines List the live engine catalog + search scopes\n` +
|
|
101
131
|
` --type <t> collection | search | scope\n` +
|
|
102
132
|
` --subtype <s> image | slide | template | content\n` +
|
|
103
|
-
` --to <presId> target a specific window/backend\n` +
|
|
133
|
+
` --to <presId> target a specific window/backend (relayed path only)\n` +
|
|
104
134
|
` --all include every paired backend\n` +
|
|
105
135
|
` --json print the raw engine list\n` +
|
|
106
136
|
` slidemind insert-slide [opts] Insert reused slide search results live into a window (requires a host)\n` +
|
|
@@ -109,17 +139,18 @@ function printHelp() {
|
|
|
109
139
|
` --to <presId> target window (required if >1 open)\n` +
|
|
110
140
|
` --at <N> insert after slide N (0-based); default = append\n` +
|
|
111
141
|
` slidemind add-slide [opts] Fold reused slide(s) into a deck as native .pptx\n` +
|
|
112
|
-
` --search <id> searchId from 'slidemind search'
|
|
142
|
+
` --search <id> searchId from 'slidemind search'\n` +
|
|
113
143
|
` --from <file.pptx> fold slide(s) from a local .pptx instead (host-free)\n` +
|
|
114
144
|
` --pick <n,..> 1-based selection number(s)\n` +
|
|
115
145
|
` --deck <deck> deck to add the slides to\n` +
|
|
116
|
-
` --to <presId>
|
|
146
|
+
` --to <presId> the window the search ran on (relayed path only; ignored with --from)\n` +
|
|
117
147
|
` --at <N> numeric prefix to start at (default: append)\n` +
|
|
118
|
-
`
|
|
148
|
+
` --json machine-readable result (carries the searchId)\n` +
|
|
149
|
+
` slidemind download-result [opts] Download image/icon search result(s) into a deck's assets/\n` +
|
|
119
150
|
` --search <id> searchId from 'slidemind search'\n` +
|
|
120
151
|
` --pick <n,..> 1-based selection number(s)\n` +
|
|
121
152
|
` --deck <deck> deck to write the asset(s) into\n` +
|
|
122
|
-
` --to <presId> the window the search ran on (
|
|
153
|
+
` --to <presId> the window the search ran on (relayed path only)\n` +
|
|
123
154
|
` --icon-color <hex[,hex,..]> bake a colour into SVG result(s) (one for all, or one per --pick)\n` +
|
|
124
155
|
` slidemind imagen "<prompt>" Generate ONE image with the add-in's image model into a deck's assets/ (paired backend, pane-free)\n` +
|
|
125
156
|
` --deck <deck> bound deck to write the image into (required)\n` +
|
|
@@ -132,7 +163,9 @@ function printHelp() {
|
|
|
132
163
|
` --force re-download even if the cached bundle is current\n` +
|
|
133
164
|
` slidemind presentations List active PowerPoint panes registered with the add-in\n` +
|
|
134
165
|
` --all include retained suspended entries\n` +
|
|
135
|
-
` slidemind doctor Diagnose
|
|
166
|
+
` slidemind doctor Diagnose two independent ladders: backend authoring readiness\n` +
|
|
167
|
+
` (session, backend, CLI version, engine routes) and live\n` +
|
|
168
|
+
` PowerPoint readiness (windows, pane)\n` +
|
|
136
169
|
` --json print machine-readable status\n` +
|
|
137
170
|
` slidemind read [presId|file.pptx] Pull the content of an open presentation (requires a host),\n` +
|
|
138
171
|
` or of a local .pptx path (read host-free from disk)\n` +
|
|
@@ -144,13 +177,12 @@ function printHelp() {
|
|
|
144
177
|
` --at <index> insert after slide N (0-based); default = append\n` +
|
|
145
178
|
` --replace delete existing slides first (destructive)\n` +
|
|
146
179
|
` --slides <ids> comma-separated source slide ids to push\n` +
|
|
147
|
-
` slidemind
|
|
148
|
-
` --label <name> name this environment (default: derived from URL)\n` +
|
|
149
|
-
` Pair once per environment; commands fan out across all of them.\n` +
|
|
150
|
-
` slidemind whoami Show every paired environment (* = default)\n` +
|
|
180
|
+
` slidemind whoami Show every connected environment (* = default)\n` +
|
|
151
181
|
` slidemind use <url|env> Set the default environment\n` +
|
|
152
182
|
` slidemind logout [url|env] Unpair one environment (--all to unpair every one)\n` +
|
|
153
183
|
` slidemind init --for <t> Set up slidemind in a harness (deploys the skill; clears the bundle cache)\n` +
|
|
184
|
+
` <t> = claude | codex | opencode | all | auto (every harness found here)\n` +
|
|
185
|
+
` 'setup' already does this — only needed after installing a new agent\n` +
|
|
154
186
|
` slidemind uninstall --for <t> Remove a previously-installed skill (clears the bundle cache)\n` +
|
|
155
187
|
` slidemind update [--for <t>] Fetch the latest release and redeploy\n` +
|
|
156
188
|
` --for <t> harness to redeploy into (default: all)\n` +
|
|
@@ -418,15 +450,86 @@ async function cmdImagen(rest) {
|
|
|
418
450
|
);
|
|
419
451
|
}
|
|
420
452
|
|
|
453
|
+
async function cmdInspectTemplateDeck(rest) {
|
|
454
|
+
const { deck, passthrough } = splitDeckArg(rest, "inspect-template-deck");
|
|
455
|
+
let packDir = null;
|
|
456
|
+
let json = false;
|
|
457
|
+
let allowUnrenderedFreeform = false;
|
|
458
|
+
for (let i = 0; i < passthrough.length; i++) {
|
|
459
|
+
const a = passthrough[i];
|
|
460
|
+
if (a === "--pack") {
|
|
461
|
+
packDir = passthrough[++i];
|
|
462
|
+
if (!packDir) fail("--pack expects a directory");
|
|
463
|
+
} else if (a === "--json") {
|
|
464
|
+
json = true;
|
|
465
|
+
} else if (a === "--allow-unrendered-freeform") {
|
|
466
|
+
allowUnrenderedFreeform = true;
|
|
467
|
+
} else if (a.startsWith("-")) {
|
|
468
|
+
fail(`unknown flag: ${a}`);
|
|
469
|
+
} else {
|
|
470
|
+
fail(`unexpected argument: ${a}`);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
if (!packDir) fail("inspect-template-deck requires --pack <dir>");
|
|
474
|
+
|
|
475
|
+
const deckDir = resolveDeckArg(deck);
|
|
476
|
+
const resolveMod = await import(
|
|
477
|
+
pathToFileURL(path.join(SRC_DIR, "builders", "resolve-skill.mjs")).href
|
|
478
|
+
);
|
|
479
|
+
const skill = resolveMod.resolveSkillForDeck(deckDir, {
|
|
480
|
+
packDir: path.resolve(packDir),
|
|
481
|
+
});
|
|
482
|
+
if (skill.mode !== "fill-template") {
|
|
483
|
+
fail(`pack ${skill.skillDir} is mode ${skill.mode}, not fill-template`);
|
|
484
|
+
}
|
|
485
|
+
const engine = await import(
|
|
486
|
+
pathToFileURL(path.join(SRC_DIR, "pptx", "fill", "engine.mjs")).href
|
|
487
|
+
);
|
|
488
|
+
const result = engine.inspectFillTemplateDeck({
|
|
489
|
+
deckDir,
|
|
490
|
+
skill,
|
|
491
|
+
allowUnrenderedFreeform,
|
|
492
|
+
});
|
|
493
|
+
const payload = {
|
|
494
|
+
version: 1,
|
|
495
|
+
pack: {
|
|
496
|
+
name: skill.skillName,
|
|
497
|
+
mode: skill.mode,
|
|
498
|
+
directory: skill.skillDir,
|
|
499
|
+
template: result.templateKey,
|
|
500
|
+
locale: result.locale,
|
|
501
|
+
},
|
|
502
|
+
slides: result.sequence,
|
|
503
|
+
warnings: result.warnings,
|
|
504
|
+
problems: result.problems,
|
|
505
|
+
};
|
|
506
|
+
if (json) {
|
|
507
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
508
|
+
} else {
|
|
509
|
+
for (const item of result.sequence) {
|
|
510
|
+
const suffix = item.kind === "master" ? ` → template ${item.no}` : "";
|
|
511
|
+
process.stdout.write(`${item.file}: ${item.kind}${suffix}\n`);
|
|
512
|
+
}
|
|
513
|
+
for (const warning of result.warnings) process.stderr.write(` warning: ${warning}\n`);
|
|
514
|
+
}
|
|
515
|
+
if (result.problems.length) {
|
|
516
|
+
fail(`inspection failed:\n - ${result.problems.join("\n - ")}`);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
421
520
|
async function cmdBuildPptx(rest) {
|
|
422
521
|
const { deck, passthrough } = splitDeckArg(rest, "build-pptx");
|
|
423
522
|
let to = null;
|
|
424
523
|
let outPath = null;
|
|
524
|
+
let packDir = null;
|
|
425
525
|
for (let i = 0; i < passthrough.length; i++) {
|
|
426
526
|
const a = passthrough[i];
|
|
427
527
|
if (a === "--to") {
|
|
428
528
|
to = passthrough[++i];
|
|
429
529
|
if (!to) fail("--to expects a presId");
|
|
530
|
+
} else if (a === "--pack") {
|
|
531
|
+
packDir = passthrough[++i];
|
|
532
|
+
if (!packDir) fail("--pack expects a directory");
|
|
430
533
|
} else if (a === "--out" || a === "-o") {
|
|
431
534
|
outPath = passthrough[++i];
|
|
432
535
|
if (!outPath) fail("--out expects a path");
|
|
@@ -449,8 +552,13 @@ async function cmdBuildPptx(rest) {
|
|
|
449
552
|
const resolveMod = await import(
|
|
450
553
|
pathToFileURL(path.join(SRC_DIR, "builders", "resolve-skill.mjs")).href
|
|
451
554
|
);
|
|
452
|
-
|
|
453
|
-
|
|
555
|
+
const resolveOpts = packDir ? { packDir: path.resolve(packDir) } : {};
|
|
556
|
+
const deckMode = resolveMod.resolveDeckMode(deckDir, resolveOpts);
|
|
557
|
+
if (packDir && deckMode.mode !== "fill-template") {
|
|
558
|
+
fail(`explicit --pack ${path.resolve(packDir)} is mode ${deckMode.mode}, not fill-template`);
|
|
559
|
+
}
|
|
560
|
+
if (deckMode.mode === "fill-template") {
|
|
561
|
+
const skill = resolveMod.resolveSkillForDeck(deckDir, resolveOpts);
|
|
454
562
|
if (to) {
|
|
455
563
|
fail(
|
|
456
564
|
"--to targets a PowerPoint window, but fill-template decks build " +
|
|
@@ -688,9 +796,169 @@ function writeSearchGrid(gridDataUri, searchId, resolveExt) {
|
|
|
688
796
|
}
|
|
689
797
|
}
|
|
690
798
|
|
|
799
|
+
// Where an engine op runs. Newer add-ins serve the engine surface off the CLI
|
|
800
|
+
// session itself (`/api/cli/engines`, `/api/cli/searches/*`), so discovery,
|
|
801
|
+
// search, download and extraction need no PowerPoint window at all; older ones
|
|
802
|
+
// relay every one of them through a task-pane webview, which is what
|
|
803
|
+
// requirePaneHost gates. The DIRECT path is preferred whenever it exists and the
|
|
804
|
+
// pane path stays as the compat fallback, so both add-in generations work.
|
|
805
|
+
//
|
|
806
|
+
// `--to <presId>` stops being a REQUIREMENT — the searchId is server-owned
|
|
807
|
+
// rather than bound to a window, so nothing has to be targeted — but it is still
|
|
808
|
+
// honoured, and naming a window means "relay through THAT pane". That is the
|
|
809
|
+
// escape hatch for the two things the pane-free routes refuse (`asset_needs_host`
|
|
810
|
+
// for SharePoint-hosted bytes, `recolor_needs_host` for an icon tint), both of
|
|
811
|
+
// which the pane can do; ignoring --to made their own remediation unreachable.
|
|
812
|
+
// `brandId` is the deck's bound design — the only thing that routes an engine op
|
|
813
|
+
// in a multi-environment session when no window is involved.
|
|
814
|
+
// No connected environment holds a server-owned search record. Authored once
|
|
815
|
+
// because every consumer of an opaque searchId reaches the same dead end and
|
|
816
|
+
// the same way out — the record's own environment is the only thing that can
|
|
817
|
+
// serve it, so a re-search is the fix regardless of who asked.
|
|
818
|
+
function expiredSearchMessage(searchId) {
|
|
819
|
+
return (
|
|
820
|
+
`search ${searchId} was not found on any connected environment — server-side ` +
|
|
821
|
+
"search records expire after ~2 hours. Re-run 'slidemind search' and pick again."
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
async function resolveEngineRouting(client, { to = null, brandId = null, pick = "one", searchId = null } = {}) {
|
|
826
|
+
// An opaque (server-minted) searchId pins the route by itself: the record
|
|
827
|
+
// lives on the environment that minted it, and the pane relay can never
|
|
828
|
+
// serve it — its handler only knows its own per-window numeric ids. So the
|
|
829
|
+
// record's origin outranks the deck's design AND --to, and when no connected
|
|
830
|
+
// environment holds the record the only honest answer is "search again",
|
|
831
|
+
// not a pane fallback that is guaranteed to 4xx.
|
|
832
|
+
if (client.isOpaqueSearchId?.(searchId)) {
|
|
833
|
+
if (to) {
|
|
834
|
+
fail(
|
|
835
|
+
`searchId ${searchId} is a pane-free search — it cannot be relayed through a window. ` +
|
|
836
|
+
`To go through the pane, re-run 'slidemind search "<query>" --to ${to}' and use ITS searchId.`,
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
let origin = null;
|
|
840
|
+
try {
|
|
841
|
+
origin = await client.resolveSearchBackend(searchId);
|
|
842
|
+
} catch (err) {
|
|
843
|
+
// The probe distinguishes "no environment holds this record" from the
|
|
844
|
+
// failures that only LOOK like it — a dead session, a stale snapshot —
|
|
845
|
+
// and hands the latter back for their own remediation.
|
|
846
|
+
await failOnEngineBlocker(client, err, { to });
|
|
847
|
+
}
|
|
848
|
+
if (origin) {
|
|
849
|
+
return { direct: true, backendURL: origin, presId: null, registry: null, entry: null };
|
|
850
|
+
}
|
|
851
|
+
fail(expiredSearchMessage(searchId));
|
|
852
|
+
}
|
|
853
|
+
let backendURL = null;
|
|
854
|
+
if (!to) {
|
|
855
|
+
backendURL = await client.resolveEngineBackend({ brandId });
|
|
856
|
+
}
|
|
857
|
+
if (backendURL) {
|
|
858
|
+
return { direct: true, backendURL, presId: null, registry: null, entry: null };
|
|
859
|
+
}
|
|
860
|
+
const registry = await client.requirePaneHost();
|
|
861
|
+
const entry =
|
|
862
|
+
pick === "any"
|
|
863
|
+
? client.pickAnyPresentationEntry(registry, to)
|
|
864
|
+
: client.pickPresentationEntry(registry, to);
|
|
865
|
+
return {
|
|
866
|
+
direct: false,
|
|
867
|
+
backendURL: entry.backendURL ?? null,
|
|
868
|
+
presId: entry.presId,
|
|
869
|
+
registry,
|
|
870
|
+
entry,
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// The design a deck is bound to (presFP == brandId), or null when it isn't bound
|
|
875
|
+
// / isn't a deck. Routing state only — never printed.
|
|
876
|
+
async function deckBrandId(deckDir) {
|
|
877
|
+
if (!deckDir) return null;
|
|
878
|
+
const bundleMod = await import(
|
|
879
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "bundle.mjs")).href
|
|
880
|
+
);
|
|
881
|
+
return bundleMod.readDeckBundleRef(deckDir)?.presFP ?? null;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// A pane-ready window, or null — never a throw, and never a wait. Used by the
|
|
885
|
+
// stale-snapshot paths below, where an open pane is an OPTION rather than a
|
|
886
|
+
// precondition. `to` is honoured only when it names a window that is actually
|
|
887
|
+
// registered; a stale --to must not cost the caller its fallback.
|
|
888
|
+
async function openPaneEntry(client, to = null) {
|
|
889
|
+
const registry = (await client.paneReadyPresentations?.()) ?? [];
|
|
890
|
+
if (registry.length === 0) return null;
|
|
891
|
+
const explicit = registry.some((p) => p.presId === to) ? to : null;
|
|
892
|
+
try {
|
|
893
|
+
return { registry, entry: client.pickAnyPresentationEntry(registry, explicit) };
|
|
894
|
+
} catch {
|
|
895
|
+
// Several windows and no way to choose — no automatic fallback from here.
|
|
896
|
+
return null;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// The ONE rendering of a stale permission snapshot. The snapshot is the
|
|
901
|
+
// backend's cached copy of the user's group memberships and it backs the
|
|
902
|
+
// PANE-FREE path only: an open task pane signs every request with a live
|
|
903
|
+
// browser token, which is strictly fresher than any snapshot. So when a
|
|
904
|
+
// pane-ready window is already open, "open the slidemind add-in once" is wrong
|
|
905
|
+
// twice over — it IS open, and that window can serve a fresh search right now.
|
|
906
|
+
// Returns the message, or null when this isn't a stale-snapshot failure.
|
|
907
|
+
async function staleSnapshotMessage(client, err, { to = null } = {}) {
|
|
908
|
+
const blocker = client.permissionsStaleBlocker?.(err);
|
|
909
|
+
if (!blocker) return null;
|
|
910
|
+
const pane = await openPaneEntry(client, to);
|
|
911
|
+
if (!pane) return blocker.message;
|
|
912
|
+
const env = pane.entry.env ? ` [${pane.entry.env}]` : "";
|
|
913
|
+
return (
|
|
914
|
+
`${blocker.message} A slidemind pane is already open${env} and signs each request live, so a fresh ` +
|
|
915
|
+
`search through that window works now: slidemind search "<query>" --to ${pane.entry.presId}.`
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// Engine-route failures the USER has to clear get one actionable line instead of
|
|
920
|
+
// a relay error. `permissions_stale` is the search permission snapshot having
|
|
921
|
+
// aged out — the server hands back the exact remediation ("open the slidemind
|
|
922
|
+
// add-in once…"), and only corpus search degrades; render and bundle don't.
|
|
923
|
+
// Anything else is re-thrown for the top-level handler.
|
|
924
|
+
async function failOnEngineBlocker(client, err, { to = null } = {}) {
|
|
925
|
+
const message = await staleSnapshotMessage(client, err, { to });
|
|
926
|
+
if (message) fail(message);
|
|
927
|
+
throw err;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// A stale snapshot that an OPEN PANE can simply route around. Only an operation
|
|
931
|
+
// that can be replayed pane-side qualifies — creating a NEW search, whose id the
|
|
932
|
+
// pane mints itself. A record-bound op on an opaque searchId cannot: the pane's
|
|
933
|
+
// handler only understands its own per-window numeric ids, so those get
|
|
934
|
+
// staleSnapshotMessage's extended blocker instead. Returns { registry, entry }
|
|
935
|
+
// or null.
|
|
936
|
+
async function paneRelayForStaleSnapshot(client, err, { to = null } = {}) {
|
|
937
|
+
if (!client.permissionsStaleBlocker?.(err)) return null;
|
|
938
|
+
return openPaneEntry(client, to);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// The pane-free routes refuse exactly two things outright: bytes that live in
|
|
942
|
+
// SharePoint (`asset_needs_host`, which needs a delegated token only a browser
|
|
943
|
+
// can mint) and an icon tint (`recolor_needs_host`, whose classifier is
|
|
944
|
+
// DOM-based). An open pane does both, so the remediation is the RELAY — name it,
|
|
945
|
+
// rather than echoing a 409 whose "retry" would take the identical route.
|
|
946
|
+
// Returns null for anything else.
|
|
947
|
+
function describeHostOnlyEngineFailure(err) {
|
|
948
|
+
const code = err?.code ?? err?.body?.code ?? null;
|
|
949
|
+
if (code !== "asset_needs_host" && code !== "recolor_needs_host") return null;
|
|
950
|
+
const detail =
|
|
951
|
+
typeof err?.body?.error === "string" ? err.body.error : (err?.message ?? "");
|
|
952
|
+
return (
|
|
953
|
+
`${detail} Run 'slidemind search "<query>" --to <presId>' against an open window and ` +
|
|
954
|
+
"re-pick, then repeat this download with the same --to — that relays it through the pane."
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
|
|
691
958
|
async function cmdSearch(rest) {
|
|
692
959
|
let asJson = false;
|
|
693
960
|
let to = null;
|
|
961
|
+
let deck = null;
|
|
694
962
|
let engineId = null;
|
|
695
963
|
let resultType = null;
|
|
696
964
|
let intent = null;
|
|
@@ -703,6 +971,9 @@ async function cmdSearch(rest) {
|
|
|
703
971
|
} else if (a === "--to") {
|
|
704
972
|
to = rest[++i];
|
|
705
973
|
if (!to) fail("--to expects a presId");
|
|
974
|
+
} else if (a === "--deck") {
|
|
975
|
+
deck = rest[++i];
|
|
976
|
+
if (!deck) fail("--deck expects a deck name or path");
|
|
706
977
|
} else if (a === "--engine" || a === "-e" || a === "--engineId") {
|
|
707
978
|
engineId = rest[++i];
|
|
708
979
|
if (!engineId) fail("--engine expects an engineId");
|
|
@@ -734,12 +1005,13 @@ async function cmdSearch(rest) {
|
|
|
734
1005
|
const query = queryParts.join(" ").trim();
|
|
735
1006
|
if (!query) {
|
|
736
1007
|
fail(
|
|
737
|
-
`usage: slidemind search "<query>" [--engine <id|scope:all|scope:sharepoint>] [--result-type slide|image] [--intent auto|content|visual] [--top <N>] [--to <presId>] [--json]`,
|
|
1008
|
+
`usage: slidemind search "<query>" [--engine <id|scope:all|scope:sharepoint>] [--result-type slide|image] [--intent auto|content|visual] [--top <N>] [--deck <deck>] [--to <presId>] [--json]`,
|
|
738
1009
|
);
|
|
739
1010
|
}
|
|
740
1011
|
|
|
741
|
-
// Engine search runs against the add-in's live engine over the bridge —
|
|
742
|
-
//
|
|
1012
|
+
// Engine search runs against the add-in's live engine over the bridge —
|
|
1013
|
+
// pane-free where the add-in serves the direct routes, relayed through an open
|
|
1014
|
+
// window (host required) where it doesn't.
|
|
743
1015
|
const client = await import(
|
|
744
1016
|
pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
|
|
745
1017
|
);
|
|
@@ -749,14 +1021,23 @@ async function cmdSearch(rest) {
|
|
|
749
1021
|
const { resolveAssetExt } = await import(
|
|
750
1022
|
pathToFileURL(path.join(SRC_DIR, "bridge", "engine-download.mjs")).href
|
|
751
1023
|
);
|
|
752
|
-
|
|
753
|
-
//
|
|
754
|
-
//
|
|
755
|
-
//
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
1024
|
+
// The engine catalog is per-user, not window-bound. On the pane path search
|
|
1025
|
+
// attaches to any connected window (preferring a live one) and the chosen
|
|
1026
|
+
// window is printed below, because the searchId lives on that pane; on the
|
|
1027
|
+
// direct path the searchId is the backend's and no window is involved.
|
|
1028
|
+
const routing = await resolveEngineRouting(client, {
|
|
1029
|
+
to,
|
|
1030
|
+
brandId: await deckBrandId(deck ? resolveDeckArg(deck) : null),
|
|
1031
|
+
pick: "any",
|
|
1032
|
+
});
|
|
1033
|
+
const presId = routing.presId;
|
|
1034
|
+
const backendURL = routing.backendURL;
|
|
1035
|
+
// Mutable, because a stale permission snapshot can push a direct search onto
|
|
1036
|
+
// the pane relay below — and once it does, the searchId belongs to a WINDOW
|
|
1037
|
+
// and every next-step hint has to name it.
|
|
1038
|
+
let direct = routing.direct;
|
|
1039
|
+
let registry = routing.registry;
|
|
1040
|
+
let entry = routing.entry;
|
|
760
1041
|
|
|
761
1042
|
// An explicit content/visual intent means "search slides", not imagery.
|
|
762
1043
|
const slideIntent = intent === "content" || intent === "visual";
|
|
@@ -789,9 +1070,18 @@ async function cmdSearch(rest) {
|
|
|
789
1070
|
// Best-effort: a bundle that can't be materialized (e.g. an incomplete
|
|
790
1071
|
// one) must not replace the actionable "wire an image engine" message
|
|
791
1072
|
// below with a bundle error — the live catalog is the real answer here.
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
1073
|
+
// Pane path: fetch by the window's presId. Direct path: there is no
|
|
1074
|
+
// window, so only a bound deck (--deck) can name the design; without one
|
|
1075
|
+
// there is nothing to fall back to and the live catalog's answer stands.
|
|
1076
|
+
const brandId = presId
|
|
1077
|
+
? null
|
|
1078
|
+
: await deckBrandId(deck ? resolveDeckArg(deck) : null);
|
|
1079
|
+
const mat =
|
|
1080
|
+
presId || brandId
|
|
1081
|
+
? await bundle
|
|
1082
|
+
.materializeBundle(presId, { backendURL, brandId })
|
|
1083
|
+
.catch(() => null)
|
|
1084
|
+
: null;
|
|
795
1085
|
if (mat) pick = bundle.pickImageEngine(bundle.readEnginesFromDir(mat.dir));
|
|
796
1086
|
}
|
|
797
1087
|
if (!pick) {
|
|
@@ -818,23 +1108,65 @@ async function cmdSearch(rest) {
|
|
|
818
1108
|
resultType = null;
|
|
819
1109
|
}
|
|
820
1110
|
|
|
821
|
-
//
|
|
822
|
-
// window
|
|
823
|
-
//
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
1111
|
+
// Direct: one pane-free call against the backend that owns the searchId.
|
|
1112
|
+
// Pane: window-agnostic — tries the picked pane, then fails over to any other
|
|
1113
|
+
// live window on the same backend if it's suspended/frozen. The answering
|
|
1114
|
+
// window (res.presId) is what reuse must target there, since the searchId is
|
|
1115
|
+
// bound to it.
|
|
1116
|
+
const relaySearch = (reg) =>
|
|
1117
|
+
client.engineSearchAnyPane({
|
|
1118
|
+
registry: reg,
|
|
1119
|
+
explicitPresId: to,
|
|
1120
|
+
engineId,
|
|
1121
|
+
query,
|
|
1122
|
+
subtype,
|
|
1123
|
+
resultType,
|
|
1124
|
+
intent: intent ?? null,
|
|
1125
|
+
top: top ?? null,
|
|
1126
|
+
});
|
|
1127
|
+
let res;
|
|
1128
|
+
try {
|
|
1129
|
+
res = direct
|
|
1130
|
+
? await client.engineSearch({
|
|
1131
|
+
engineId,
|
|
1132
|
+
query,
|
|
1133
|
+
subtype,
|
|
1134
|
+
resultType,
|
|
1135
|
+
intent: intent ?? null,
|
|
1136
|
+
top: top ?? null,
|
|
1137
|
+
backendURL,
|
|
1138
|
+
})
|
|
1139
|
+
: await relaySearch(routing.registry);
|
|
1140
|
+
} catch (err) {
|
|
1141
|
+
// A stale permission snapshot degrades the PANE-FREE path only — an open
|
|
1142
|
+
// pane signs each request live, which is strictly fresher than the
|
|
1143
|
+
// snapshot. Creating a new search is exactly the operation the pane can
|
|
1144
|
+
// replay (it mints its own searchId), so relay it rather than telling the
|
|
1145
|
+
// user to open an add-in that is already open.
|
|
1146
|
+
const relay = direct ? await paneRelayForStaleSnapshot(client, err, { to }) : null;
|
|
1147
|
+
if (!relay) await failOnEngineBlocker(client, err, { to });
|
|
1148
|
+
process.stderr.write(
|
|
1149
|
+
`note: this environment's search permission snapshot has aged out, so the search ran through the open ` +
|
|
1150
|
+
`slidemind pane${relay.entry.env ? ` [${relay.entry.env}]` : ""} instead (it signs each request live).\n`,
|
|
1151
|
+
);
|
|
1152
|
+
direct = false;
|
|
1153
|
+
registry = relay.registry;
|
|
1154
|
+
entry = relay.entry;
|
|
1155
|
+
try {
|
|
1156
|
+
res = await relaySearch(registry);
|
|
1157
|
+
} catch (relayErr) {
|
|
1158
|
+
await failOnEngineBlocker(client, relayErr, { to });
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
const ranPresId = res?.presId ?? entry?.presId ?? presId;
|
|
1162
|
+
const ranEntry = direct
|
|
1163
|
+
? { env: res?.env ?? null }
|
|
1164
|
+
: (registry.find((p) => p.presId === ranPresId) ?? entry);
|
|
836
1165
|
if (asJson) {
|
|
837
|
-
|
|
1166
|
+
// The searchId rides the JSON so an interrupted agent can resume its picks
|
|
1167
|
+
// without re-searching. The backendURL never does — env label only.
|
|
1168
|
+
const { backendURL: _routingOnly, ...pub } = res ?? {};
|
|
1169
|
+
process.stdout.write(`${JSON.stringify(pub, null, 2)}\n`);
|
|
838
1170
|
return;
|
|
839
1171
|
}
|
|
840
1172
|
if (!res?.ok) {
|
|
@@ -891,6 +1223,16 @@ async function cmdSearch(rest) {
|
|
|
891
1223
|
`Contact sheet of all ${count} result(s) — LOOK AT THIS FILE to choose, ` +
|
|
892
1224
|
`don't fetch previews one by one: ${gridPath}\n`,
|
|
893
1225
|
);
|
|
1226
|
+
} else if (direct) {
|
|
1227
|
+
// The pane composes the collage in a DOM; the pane-free route may not
|
|
1228
|
+
// have one to hand back (`gridUnavailable`, or simply no grid field on an
|
|
1229
|
+
// add-in that hasn't grown it yet). Say where previews come from instead
|
|
1230
|
+
// — otherwise an agent that has learned to expect a contact sheet quietly
|
|
1231
|
+
// issues N thumbnail fetches to rebuild it.
|
|
1232
|
+
process.stdout.write(
|
|
1233
|
+
`No contact sheet on this path — preview candidates one at a time instead: ` +
|
|
1234
|
+
`slidemind view-slide --search ${res.searchId} --pick <n>\n`,
|
|
1235
|
+
);
|
|
894
1236
|
}
|
|
895
1237
|
}
|
|
896
1238
|
// Tailor the next-step hint to the result kind: slide verbs (insert-slide /
|
|
@@ -899,8 +1241,24 @@ async function cmdSearch(rest) {
|
|
|
899
1241
|
// add-ins that don't echo a top-level subtype.
|
|
900
1242
|
const isSlideResults =
|
|
901
1243
|
res.subtype === "slide" || results.some((r) => r.kind === "slide");
|
|
902
|
-
const envTag = ranEntry
|
|
903
|
-
if (
|
|
1244
|
+
const envTag = ranEntry?.env ? ` [${ranEntry.env}]` : "";
|
|
1245
|
+
if (direct) {
|
|
1246
|
+
// The searchId belongs to the BACKEND here, not to a window: picks work with
|
|
1247
|
+
// PowerPoint closed, so the follow-up commands carry no --to. Live insert is
|
|
1248
|
+
// the one step that still names a window (it types into an open deck).
|
|
1249
|
+
if (isSlideResults) {
|
|
1250
|
+
process.stdout.write(
|
|
1251
|
+
`searchId ${res.searchId}${envTag} is held by the add-in — reuse it with no PowerPoint window:\n` +
|
|
1252
|
+
` slidemind add-slide --search ${res.searchId} --pick <n> --deck <deck>\n` +
|
|
1253
|
+
` slidemind insert-slide --search ${res.searchId} --pick <n> --to <presId> (live insert needs an open window)\n`,
|
|
1254
|
+
);
|
|
1255
|
+
} else {
|
|
1256
|
+
process.stdout.write(
|
|
1257
|
+
`searchId ${res.searchId}${envTag} is held by the add-in — download image result(s) into a deck (no window needed):\n` +
|
|
1258
|
+
` slidemind download-result --search ${res.searchId} --pick <n[,n,...]> --deck <deck> [--icon-color <hex>]\n`,
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1261
|
+
} else if (isSlideResults) {
|
|
904
1262
|
// The searchId is bound to the window the search ran on — reuse must target
|
|
905
1263
|
// that same window, so spell out its presId in the next-step commands.
|
|
906
1264
|
process.stdout.write(
|
|
@@ -962,16 +1320,58 @@ async function cmdLs(rest) {
|
|
|
962
1320
|
if (all && to) fail("--all cannot be combined with --to");
|
|
963
1321
|
let backendURL = null;
|
|
964
1322
|
if (!all) {
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
1323
|
+
// Pane-free where the add-in serves the direct routes; otherwise the catalog
|
|
1324
|
+
// is relayed through a window, so a live pane is still the precondition.
|
|
1325
|
+
try {
|
|
1326
|
+
backendURL = (await resolveEngineRouting(client, { to, pick: "any" }))
|
|
1327
|
+
.backendURL;
|
|
1328
|
+
} catch (err) {
|
|
1329
|
+
// Listing is the one engine op that can simply show EVERY environment
|
|
1330
|
+
// (each engine is tagged with its env), so an ambiguous multi-environment
|
|
1331
|
+
// session fans out here rather than demanding --backend.
|
|
1332
|
+
if (err?.code !== "engine_backend_ambiguous") throw err;
|
|
1333
|
+
backendURL = null;
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
let engines;
|
|
1337
|
+
// The pane-free route's summary of the catalog BEFORE the type/subtype
|
|
1338
|
+
// filter; null on older add-ins and on the pane-relay path.
|
|
1339
|
+
let catalog = null;
|
|
1340
|
+
// Which environments answered. The catalog is POOLED, and pooling dedupes the
|
|
1341
|
+
// scopes every environment repeats — so a second environment can contribute
|
|
1342
|
+
// no row of its own, and the surviving rows can no longer be counted to learn
|
|
1343
|
+
// whether more than one answered.
|
|
1344
|
+
let envs = [];
|
|
1345
|
+
try {
|
|
1346
|
+
({ engines, catalog, envs = [] } = await client.listEngines({
|
|
1347
|
+
type,
|
|
1348
|
+
subtype,
|
|
1349
|
+
backendURL,
|
|
1350
|
+
}));
|
|
1351
|
+
} catch (err) {
|
|
1352
|
+
// No pane fallback here: `GET /api/cli/engines` is the catalog route on both
|
|
1353
|
+
// add-in generations (newer ones answer it directly, older ones relay it
|
|
1354
|
+
// themselves), so there is no second route to retry on. The message does
|
|
1355
|
+
// name the open pane, because searching THROUGH it still works.
|
|
1356
|
+
await failOnEngineBlocker(client, err, { to });
|
|
968
1357
|
}
|
|
969
|
-
const { engines } = await client.listEngines({ type, subtype, backendURL });
|
|
970
1358
|
if (asJson) {
|
|
971
|
-
process.stdout.write(`${JSON.stringify(engines, null, 2)}\n`);
|
|
1359
|
+
process.stdout.write(`${JSON.stringify({ engines, catalog, envs }, null, 2)}\n`);
|
|
972
1360
|
return;
|
|
973
1361
|
}
|
|
974
1362
|
if (engines.length === 0) {
|
|
1363
|
+
// A filter that matched nothing is NOT an empty deployment — the summary is
|
|
1364
|
+
// the only thing that tells the two apart, so say which one this is and the
|
|
1365
|
+
// next move follows (drop the filter vs. search the corpus unfiltered).
|
|
1366
|
+
const unfiltered = catalog ? catalog.totalEngines + catalog.totalScopes : 0;
|
|
1367
|
+
if (catalog?.filtered && unfiltered > 0) {
|
|
1368
|
+
process.stderr.write(
|
|
1369
|
+
`No engine or scope matched this filter, but ${catalog.totalEngines} engine(s) ` +
|
|
1370
|
+
`and ${catalog.totalScopes} scope(s) exist. Re-run 'slidemind ls engines' ` +
|
|
1371
|
+
"without --type/--subtype to see them.\n",
|
|
1372
|
+
);
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
975
1375
|
// Deliberately not "nothing to search": an engine is a saved folder filter
|
|
976
1376
|
// over the add-in's shared corpus, so an empty catalog says only that nobody
|
|
977
1377
|
// saved a filter. An older add-in that predates scopes lands here too.
|
|
@@ -986,8 +1386,11 @@ async function cmdLs(rest) {
|
|
|
986
1386
|
);
|
|
987
1387
|
return;
|
|
988
1388
|
}
|
|
989
|
-
// Show the env tag only when more than one environment contributed
|
|
990
|
-
|
|
1389
|
+
// Show the env tag only when more than one environment contributed. Two
|
|
1390
|
+
// environments can carry identically-named engines, and without the tag the
|
|
1391
|
+
// pooled list gives no way to tell them apart. Deduped scope rows carry no env
|
|
1392
|
+
// of their own (they exist on every environment), so they stay untagged.
|
|
1393
|
+
const multiEnv = envs.length > 1 || new Set(engines.map((e) => e.env)).size > 1;
|
|
991
1394
|
for (const e of engines) {
|
|
992
1395
|
const kind = [e.type, e.subtype].filter(Boolean).join("/");
|
|
993
1396
|
const env = multiEnv && e.env ? `[${e.env}] ` : "";
|
|
@@ -998,12 +1401,28 @@ async function cmdLs(rest) {
|
|
|
998
1401
|
e.type === "scope"
|
|
999
1402
|
? ` (needs --result-type ${(e.capabilities?.resultTypes ?? ["slide", "image"]).join("|")})`
|
|
1000
1403
|
: "";
|
|
1404
|
+
// An engine that exists but has nothing indexed yet returns zero hits for
|
|
1405
|
+
// every query; say so here so an empty search isn't read as a bad engine id.
|
|
1406
|
+
const state =
|
|
1407
|
+
e.content?.state === "empty"
|
|
1408
|
+
? " (nothing indexed yet)"
|
|
1409
|
+
: e.content?.state === "indexing"
|
|
1410
|
+
? " (indexing)"
|
|
1411
|
+
: "";
|
|
1001
1412
|
process.stdout.write(
|
|
1002
|
-
`${env}${e.id} · ${e.label ?? e.id} · ${kind}${hint}${desc}\n`,
|
|
1413
|
+
`${env}${e.id} · ${e.label ?? e.id} · ${kind}${hint}${state}${desc}\n`,
|
|
1003
1414
|
);
|
|
1004
1415
|
}
|
|
1005
1416
|
}
|
|
1006
1417
|
|
|
1418
|
+
// A searchId is a number when the pane minted it (the relay path) and an opaque
|
|
1419
|
+
// server-minted id on the pane-free engine routes, where the search record lives
|
|
1420
|
+
// on the backend. Accept both so a pick round-trips whichever search produced it.
|
|
1421
|
+
function parseSearchIdArg(raw, flag = "--search") {
|
|
1422
|
+
if (!raw || raw.startsWith("-")) fail(`${flag} expects a searchId, got '${raw}'`);
|
|
1423
|
+
return /^\d+$/.test(raw) ? parseInt(raw, 10) : raw;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1007
1426
|
function parsePicks(raw, flag = "--pick") {
|
|
1008
1427
|
if (!raw) fail(`${flag} expects a comma-separated list of selection numbers`);
|
|
1009
1428
|
// Tolerate a trailing/empty segment ("1," or "1,,2") by dropping blank parts
|
|
@@ -1023,6 +1442,45 @@ function parsePicks(raw, flag = "--pick") {
|
|
|
1023
1442
|
return picks;
|
|
1024
1443
|
}
|
|
1025
1444
|
|
|
1445
|
+
/**
|
|
1446
|
+
* Turn a live-insert failure into the action that actually fixes it. The client
|
|
1447
|
+
* has already re-listed the registry and retried ONCE against the live window
|
|
1448
|
+
* (a pane reconnect churns the presId), so a pane error that reaches here is one
|
|
1449
|
+
* only the user can clear — and the way around it is the window-free fold.
|
|
1450
|
+
*
|
|
1451
|
+
* A stale permission snapshot is NOT among them: it is authored once, in
|
|
1452
|
+
* staleSnapshotMessage, which the caller consults first.
|
|
1453
|
+
*/
|
|
1454
|
+
function describeInsertSlideFailure(err, searchId) {
|
|
1455
|
+
const code = err?.code ?? err?.body?.code ?? null;
|
|
1456
|
+
if (code === "unknown_search") {
|
|
1457
|
+
return `searchId ${searchId} is no longer available. Run 'slidemind search' again, then re-pick.`;
|
|
1458
|
+
}
|
|
1459
|
+
if (code === "searchId_required") {
|
|
1460
|
+
// The pane was handed an id it cannot resolve — its search context is
|
|
1461
|
+
// per-window and numeric, and a server-minted id was never in it.
|
|
1462
|
+
return (
|
|
1463
|
+
`searchId ${searchId} belongs to the add-in, not to that PowerPoint window. ` +
|
|
1464
|
+
`Re-run 'slidemind search' (the CLI picks the reuse route from where the search ran), then re-pick.`
|
|
1465
|
+
);
|
|
1466
|
+
}
|
|
1467
|
+
const paneAsleep =
|
|
1468
|
+
code === "pane_suspended" ||
|
|
1469
|
+
code === "pane_timeout" ||
|
|
1470
|
+
// A relay 404 about the presId itself carries prose and no machine code,
|
|
1471
|
+
// and bridgeFetch falls `code` back to that prose — so read the PARSED one.
|
|
1472
|
+
(err?.status === 404 &&
|
|
1473
|
+
(!err?.body?.code || err.body.code === "unknown_presentation"));
|
|
1474
|
+
if (paneAsleep) {
|
|
1475
|
+
return (
|
|
1476
|
+
"the slidemind task pane in that PowerPoint window is not responding (already retried once). " +
|
|
1477
|
+
"Foreground PowerPoint and click the slidemind pane to wake it, then re-run — or reuse the same " +
|
|
1478
|
+
`result with no window at all: slidemind add-slide --search ${searchId} --pick <n> --deck <deck>.`
|
|
1479
|
+
);
|
|
1480
|
+
}
|
|
1481
|
+
return `insert-slide failed: ${err?.message ?? "unknown error"}`;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1026
1484
|
async function cmdInsertSlide(rest) {
|
|
1027
1485
|
let searchId = null;
|
|
1028
1486
|
let picks = null;
|
|
@@ -1031,10 +1489,7 @@ async function cmdInsertSlide(rest) {
|
|
|
1031
1489
|
for (let i = 0; i < rest.length; i++) {
|
|
1032
1490
|
const a = rest[i];
|
|
1033
1491
|
if (a === "--search") {
|
|
1034
|
-
|
|
1035
|
-
const n = parseInt(next, 10);
|
|
1036
|
-
if (!Number.isFinite(n)) fail(`--search expects a searchId, got '${next}'`);
|
|
1037
|
-
searchId = n;
|
|
1492
|
+
searchId = parseSearchIdArg(rest[++i]);
|
|
1038
1493
|
} else if (a === "--pick") {
|
|
1039
1494
|
picks = parsePicks(rest[++i]);
|
|
1040
1495
|
} else if (a === "--to") {
|
|
@@ -1059,20 +1514,97 @@ async function cmdInsertSlide(rest) {
|
|
|
1059
1514
|
const client = await import(
|
|
1060
1515
|
pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
|
|
1061
1516
|
);
|
|
1517
|
+
// Live insert types into an OPEN presentation, so it stays window- and
|
|
1518
|
+
// pane-bound even where the rest of the engine surface has moved off the pane.
|
|
1062
1519
|
const registry = await client.requirePaneHost();
|
|
1063
1520
|
const entry = client.pickPresentationEntry(registry, to);
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1521
|
+
// Window identity for the client's single retry: a pane reconnect mints a NEW
|
|
1522
|
+
// presId for the same window, and the design fingerprint re-matches it — but
|
|
1523
|
+
// only against windows we did NOT already see next to the target, or a second
|
|
1524
|
+
// deck on the same corporate design would take the insert.
|
|
1525
|
+
const identity = {
|
|
1526
|
+
fileName: entry.fileName ?? entry.title ?? null,
|
|
1527
|
+
brandId: entry.brandId ?? null,
|
|
1528
|
+
otherPresIds: registry
|
|
1529
|
+
.map((p) => p.presId)
|
|
1530
|
+
.filter((id) => id && id !== entry.presId),
|
|
1531
|
+
};
|
|
1532
|
+
// WHERE the searchId lives decides how the reuse runs. A number came from
|
|
1533
|
+
// that window's own pane search and the pane resolves it; an opaque id is the
|
|
1534
|
+
// backend's (the pane-free search) and the pane has never seen it — so those
|
|
1535
|
+
// picks are extracted pane-free and inserted as bytes instead.
|
|
1536
|
+
//
|
|
1537
|
+
// Which backend extracts them is the RECORD's, not the window's. Those are
|
|
1538
|
+
// different environments as soon as two are connected, and spending a
|
|
1539
|
+
// server-owned id against the window's origin 404s as `unknown_search` — read
|
|
1540
|
+
// as "the search expired", which sends the agent round a re-search loop that
|
|
1541
|
+
// can never resolve. The window stays the INSERT target either way.
|
|
1542
|
+
let engineBackendURL = null;
|
|
1543
|
+
if (client.isOpaqueSearchId?.(searchId)) {
|
|
1544
|
+
try {
|
|
1545
|
+
engineBackendURL = await client.resolveSearchBackend(searchId);
|
|
1546
|
+
} catch (err) {
|
|
1547
|
+
await failOnEngineBlocker(client, err, { to: entry.presId });
|
|
1548
|
+
}
|
|
1549
|
+
if (!engineBackendURL) fail(expiredSearchMessage(searchId));
|
|
1550
|
+
}
|
|
1551
|
+
let res;
|
|
1552
|
+
try {
|
|
1553
|
+
res = engineBackendURL
|
|
1554
|
+
? await client.insertSlidesFromSearch({
|
|
1555
|
+
presId: entry.presId,
|
|
1556
|
+
searchId,
|
|
1557
|
+
selections: picks,
|
|
1558
|
+
at,
|
|
1559
|
+
backendURL: entry.backendURL ?? null,
|
|
1560
|
+
engineBackendURL,
|
|
1561
|
+
...identity,
|
|
1562
|
+
})
|
|
1563
|
+
: await client.insertSlideLive({
|
|
1564
|
+
presId: entry.presId,
|
|
1565
|
+
searchId,
|
|
1566
|
+
selections: picks,
|
|
1567
|
+
at,
|
|
1568
|
+
backendURL: entry.backendURL ?? null,
|
|
1569
|
+
...identity,
|
|
1570
|
+
});
|
|
1571
|
+
} catch (err) {
|
|
1572
|
+
fail(
|
|
1573
|
+
(await staleSnapshotMessage(client, err, { to: entry.presId })) ??
|
|
1574
|
+
describeInsertSlideFailure(err, searchId),
|
|
1575
|
+
);
|
|
1576
|
+
}
|
|
1071
1577
|
if (!res?.ok) {
|
|
1072
1578
|
fail(`insert-slide failed: ${res?.error ?? "unknown error"}`);
|
|
1073
1579
|
}
|
|
1074
1580
|
process.stdout.write(
|
|
1075
|
-
`Inserted ${res.insertedCount ?? picks.length} slide(s) into presentation ${entry.presId}\n`,
|
|
1581
|
+
`Inserted ${res.insertedCount ?? picks.length} slide(s) into presentation ${res.presId ?? entry.presId}\n`,
|
|
1582
|
+
);
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
// One line per written slide, plus the machine-readable twin. The searchId is
|
|
1586
|
+
// echoed on purpose: an agent interrupted between picks can resume from the JSON
|
|
1587
|
+
// without re-running the search (the record lives on the backend now).
|
|
1588
|
+
function writeAddSlideResult({ asJson, searchId, deckDir, deck, written, slides }) {
|
|
1589
|
+
if (asJson) {
|
|
1590
|
+
process.stdout.write(
|
|
1591
|
+
`${JSON.stringify(
|
|
1592
|
+
{
|
|
1593
|
+
ok: true,
|
|
1594
|
+
searchId,
|
|
1595
|
+
deck: path.relative(process.cwd(), deckDir),
|
|
1596
|
+
slides,
|
|
1597
|
+
},
|
|
1598
|
+
null,
|
|
1599
|
+
2,
|
|
1600
|
+
)}\n`,
|
|
1601
|
+
);
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1604
|
+
process.stdout.write(
|
|
1605
|
+
`Added ${written.length} reused slide(s) to ${path.relative(process.cwd(), deckDir)}:\n` +
|
|
1606
|
+
written.map((w) => ` ${w}`).join("\n") +
|
|
1607
|
+
`\nThey are native .pptx in slides/ — re-run 'slidemind build-pptx ${deck}' or 'slidemind push ${deck}' to render the deck with them spliced in.\n`,
|
|
1076
1608
|
);
|
|
1077
1609
|
}
|
|
1078
1610
|
|
|
@@ -1083,13 +1615,13 @@ async function cmdAddSlide(rest) {
|
|
|
1083
1615
|
let at = null;
|
|
1084
1616
|
let to = null;
|
|
1085
1617
|
let from = null;
|
|
1618
|
+
let asJson = false;
|
|
1086
1619
|
for (let i = 0; i < rest.length; i++) {
|
|
1087
1620
|
const a = rest[i];
|
|
1088
|
-
if (a === "--
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
searchId = n;
|
|
1621
|
+
if (a === "--json") {
|
|
1622
|
+
asJson = true;
|
|
1623
|
+
} else if (a === "--search") {
|
|
1624
|
+
searchId = parseSearchIdArg(rest[++i]);
|
|
1093
1625
|
} else if (a === "--from") {
|
|
1094
1626
|
from = rest[++i];
|
|
1095
1627
|
if (!from) fail("--from expects a path to a .pptx file");
|
|
@@ -1162,21 +1694,30 @@ async function cmdAddSlide(rest) {
|
|
|
1162
1694
|
.filter((f) => f.endsWith(".jsx") || f.endsWith(".pptx"));
|
|
1163
1695
|
const targets = reused.reusedSlideTargets(existing, results, at);
|
|
1164
1696
|
const written = [];
|
|
1697
|
+
const slides = [];
|
|
1165
1698
|
results.forEach((r, i) => {
|
|
1166
1699
|
const target = path.join(slidesDir, targets[i].filename);
|
|
1167
1700
|
fs.writeFileSync(target, Buffer.from(r.bytes));
|
|
1168
1701
|
written.push(path.relative(process.cwd(), target));
|
|
1702
|
+
slides.push({
|
|
1703
|
+
selection: picks[i] ?? null,
|
|
1704
|
+
slideId: r.slideId ?? null,
|
|
1705
|
+
file: path.relative(process.cwd(), target),
|
|
1706
|
+
});
|
|
1707
|
+
});
|
|
1708
|
+
writeAddSlideResult({
|
|
1709
|
+
asJson,
|
|
1710
|
+
searchId: null,
|
|
1711
|
+
deckDir,
|
|
1712
|
+
deck,
|
|
1713
|
+
written,
|
|
1714
|
+
slides,
|
|
1169
1715
|
});
|
|
1170
|
-
process.stdout.write(
|
|
1171
|
-
`Added ${written.length} reused slide(s) to ${path.relative(process.cwd(), deckDir)}:\n` +
|
|
1172
|
-
written.map((w) => ` ${w}`).join("\n") +
|
|
1173
|
-
`\nThey are native .pptx in slides/ — re-run 'slidemind build-pptx ${deck}' or 'slidemind push ${deck}' to render the deck with them spliced in.\n`,
|
|
1174
|
-
);
|
|
1175
1716
|
return;
|
|
1176
1717
|
}
|
|
1177
1718
|
|
|
1178
1719
|
if (searchId === null || !picks || !deck) {
|
|
1179
|
-
fail("usage: slidemind add-slide (--search <id> | --from <file.pptx>) --pick <n[,n,...]> --deck <deck> [--to <presId>] [--at <N>]");
|
|
1720
|
+
fail("usage: slidemind add-slide (--search <id> | --from <file.pptx>) --pick <n[,n,...]> --deck <deck> [--to <presId>] [--at <N>] [--json]");
|
|
1180
1721
|
}
|
|
1181
1722
|
const deckDir = resolveDeckArg(deck);
|
|
1182
1723
|
const slidesDir = path.join(deckDir, "slides");
|
|
@@ -1185,12 +1726,17 @@ async function cmdAddSlide(rest) {
|
|
|
1185
1726
|
const client = await import(
|
|
1186
1727
|
pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
|
|
1187
1728
|
);
|
|
1188
|
-
|
|
1189
|
-
//
|
|
1190
|
-
//
|
|
1191
|
-
|
|
1192
|
-
const
|
|
1193
|
-
|
|
1729
|
+
// Direct path: an opaque searchId routes to the environment that minted the
|
|
1730
|
+
// record; the deck's bound design only decides when the id can't. Pane path:
|
|
1731
|
+
// the searchId is presentation-scoped, so --to targets the window the search
|
|
1732
|
+
// ran on (without it, a registry with >1 open window is a dead-end).
|
|
1733
|
+
const routing = await resolveEngineRouting(client, {
|
|
1734
|
+
to,
|
|
1735
|
+
brandId: await deckBrandId(deckDir),
|
|
1736
|
+
searchId,
|
|
1737
|
+
});
|
|
1738
|
+
const presId = routing.presId;
|
|
1739
|
+
const backendURL = routing.backendURL;
|
|
1194
1740
|
|
|
1195
1741
|
// Extract each picked slide's bytes over the bridge, then let the shared
|
|
1196
1742
|
// helper compute the play-order filenames (NN-reused-<shortid>.pptx). The
|
|
@@ -1211,9 +1757,12 @@ async function cmdAddSlide(rest) {
|
|
|
1211
1757
|
if (s.tempPath) fs.rmSync(s.tempPath, { force: true });
|
|
1212
1758
|
}
|
|
1213
1759
|
};
|
|
1760
|
+
// A searchId can be an opaque server id on the direct path — keep it usable as
|
|
1761
|
+
// a file name component.
|
|
1762
|
+
const searchKey = String(searchId).replace(/[^A-Za-z0-9._-]/g, "_");
|
|
1214
1763
|
try {
|
|
1215
1764
|
for (const pick of picks) {
|
|
1216
|
-
const tempPath = path.join(slidesDir, `.reused-${
|
|
1765
|
+
const tempPath = path.join(slidesDir, `.reused-${searchKey}-${pick}.part`);
|
|
1217
1766
|
if (typeof client.extractSlideToFile === "function") {
|
|
1218
1767
|
try {
|
|
1219
1768
|
const res = await client.extractSlideToFile({
|
|
@@ -1229,11 +1778,17 @@ async function cmdAddSlide(rest) {
|
|
|
1229
1778
|
continue;
|
|
1230
1779
|
} catch (err) {
|
|
1231
1780
|
fs.rmSync(tempPath, { force: true });
|
|
1781
|
+
const stale = await staleSnapshotMessage(client, err, { to });
|
|
1782
|
+
if (stale) {
|
|
1783
|
+
cleanupTemps();
|
|
1784
|
+
fail(stale);
|
|
1785
|
+
}
|
|
1232
1786
|
// Route absent (bodyless 404 / 501) → this add-in predates the streaming
|
|
1233
|
-
// route; fall through to the capped base64 ack path below.
|
|
1787
|
+
// route; fall through to the capped base64 ack path below. On the
|
|
1788
|
+
// pane-free path there is no ack path to fall back to.
|
|
1234
1789
|
const routeMissing =
|
|
1235
1790
|
(err?.status === 404 && !err?.code) || err?.status === 501;
|
|
1236
|
-
if (!routeMissing) {
|
|
1791
|
+
if (!routeMissing || !presId) {
|
|
1237
1792
|
cleanupTemps();
|
|
1238
1793
|
fail(`add-slide failed for selection ${pick}: ${err.message}`);
|
|
1239
1794
|
}
|
|
@@ -1260,6 +1815,7 @@ async function cmdAddSlide(rest) {
|
|
|
1260
1815
|
.filter((f) => f.endsWith(".jsx") || f.endsWith(".pptx"));
|
|
1261
1816
|
const targets = reused.reusedSlideTargets(existing, extracted, at);
|
|
1262
1817
|
const written = [];
|
|
1818
|
+
const slides = [];
|
|
1263
1819
|
extracted.forEach((slide, i) => {
|
|
1264
1820
|
const target = path.join(slidesDir, targets[i].filename);
|
|
1265
1821
|
if (slide.tempPath) {
|
|
@@ -1268,12 +1824,13 @@ async function cmdAddSlide(rest) {
|
|
|
1268
1824
|
fs.writeFileSync(target, Buffer.from(slide.base64, "base64"));
|
|
1269
1825
|
}
|
|
1270
1826
|
written.push(path.relative(process.cwd(), target));
|
|
1827
|
+
slides.push({
|
|
1828
|
+
selection: slide.selection,
|
|
1829
|
+
slideId: slide.slideId ?? null,
|
|
1830
|
+
file: path.relative(process.cwd(), target),
|
|
1831
|
+
});
|
|
1271
1832
|
});
|
|
1272
|
-
|
|
1273
|
-
`Added ${written.length} reused slide(s) to ${path.relative(process.cwd(), deckDir)}:\n` +
|
|
1274
|
-
written.map((w) => ` ${w}`).join("\n") +
|
|
1275
|
-
`\nThey are native .pptx in slides/ — re-run 'slidemind build-pptx ${deck}' or 'slidemind push ${deck}' to render the deck with them spliced in.\n`,
|
|
1276
|
-
);
|
|
1833
|
+
writeAddSlideResult({ asJson, searchId, deckDir, deck, written, slides });
|
|
1277
1834
|
}
|
|
1278
1835
|
|
|
1279
1836
|
// Map a `--icon-color` value onto the `--pick` list. Accepts either one colour
|
|
@@ -1307,10 +1864,7 @@ async function cmdViewSlide(rest) {
|
|
|
1307
1864
|
for (let i = 0; i < rest.length; i++) {
|
|
1308
1865
|
const a = rest[i];
|
|
1309
1866
|
if (a === "--search") {
|
|
1310
|
-
|
|
1311
|
-
const n = parseInt(next, 10);
|
|
1312
|
-
if (!Number.isFinite(n)) fail(`--search expects a searchId, got '${next}'`);
|
|
1313
|
-
searchId = n;
|
|
1867
|
+
searchId = parseSearchIdArg(rest[++i]);
|
|
1314
1868
|
} else if (a === "--pick") {
|
|
1315
1869
|
const next = rest[++i];
|
|
1316
1870
|
const n = parseInt(next, 10);
|
|
@@ -1331,7 +1885,7 @@ async function cmdViewSlide(rest) {
|
|
|
1331
1885
|
}
|
|
1332
1886
|
}
|
|
1333
1887
|
if (searchId === null || pick === null) {
|
|
1334
|
-
fail("usage: slidemind view-slide --search <id> --pick <n> --to <presId> [--out <dir>]");
|
|
1888
|
+
fail("usage: slidemind view-slide --search <id> --pick <n> [--to <presId>] [--out <dir>]");
|
|
1335
1889
|
}
|
|
1336
1890
|
const client = await import(
|
|
1337
1891
|
pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
|
|
@@ -1339,16 +1893,18 @@ async function cmdViewSlide(rest) {
|
|
|
1339
1893
|
const { resolveAssetExt } = await import(
|
|
1340
1894
|
pathToFileURL(path.join(SRC_DIR, "bridge", "engine-download.mjs")).href
|
|
1341
1895
|
);
|
|
1342
|
-
const
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
})
|
|
1896
|
+
const routing = await resolveEngineRouting(client, { to, searchId });
|
|
1897
|
+
let res;
|
|
1898
|
+
try {
|
|
1899
|
+
res = await client.viewSlide({
|
|
1900
|
+
presId: routing.presId,
|
|
1901
|
+
searchId,
|
|
1902
|
+
selection: pick,
|
|
1903
|
+
backendURL: routing.backendURL,
|
|
1904
|
+
});
|
|
1905
|
+
} catch (err) {
|
|
1906
|
+
await failOnEngineBlocker(client, err, { to });
|
|
1907
|
+
}
|
|
1352
1908
|
if (!res?.ok || !res.dataUri) {
|
|
1353
1909
|
fail(`view-slide failed: ${res?.error ?? "no image returned"}`);
|
|
1354
1910
|
}
|
|
@@ -1372,6 +1928,135 @@ async function cmdViewSlide(rest) {
|
|
|
1372
1928
|
);
|
|
1373
1929
|
}
|
|
1374
1930
|
|
|
1931
|
+
async function cmdReadSource(rest) {
|
|
1932
|
+
let searchId = null;
|
|
1933
|
+
let pick = null;
|
|
1934
|
+
let fromSlide = null;
|
|
1935
|
+
let toSlide = null;
|
|
1936
|
+
let to = null;
|
|
1937
|
+
let outFile = null;
|
|
1938
|
+
for (let i = 0; i < rest.length; i++) {
|
|
1939
|
+
const a = rest[i];
|
|
1940
|
+
if (a === "--search") {
|
|
1941
|
+
searchId = parseSearchIdArg(rest[++i]);
|
|
1942
|
+
} else if (a === "--pick") {
|
|
1943
|
+
const next = rest[++i];
|
|
1944
|
+
const n = parseInt(next, 10);
|
|
1945
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
1946
|
+
fail(`--pick expects a positive selection number, got '${next}'`);
|
|
1947
|
+
}
|
|
1948
|
+
pick = n;
|
|
1949
|
+
} else if (a === "--from-slide") {
|
|
1950
|
+
const next = rest[++i];
|
|
1951
|
+
const n = parseInt(next, 10);
|
|
1952
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
1953
|
+
fail(`--from-slide expects a positive slide number, got '${next}'`);
|
|
1954
|
+
}
|
|
1955
|
+
fromSlide = n;
|
|
1956
|
+
} else if (a === "--to-slide") {
|
|
1957
|
+
const next = rest[++i];
|
|
1958
|
+
const n = parseInt(next, 10);
|
|
1959
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
1960
|
+
fail(`--to-slide expects a positive slide number, got '${next}'`);
|
|
1961
|
+
}
|
|
1962
|
+
toSlide = n;
|
|
1963
|
+
} else if (a === "--to") {
|
|
1964
|
+
// Deliberately the WINDOW, matching every other search-scoped command.
|
|
1965
|
+
// The slide window is --from-slide/--to-slide.
|
|
1966
|
+
to = rest[++i];
|
|
1967
|
+
if (!to) fail("--to expects a presId");
|
|
1968
|
+
} else if (a === "--out" || a === "-o") {
|
|
1969
|
+
outFile = rest[++i];
|
|
1970
|
+
if (!outFile) fail("--out expects a file path");
|
|
1971
|
+
} else if (a.startsWith("-")) {
|
|
1972
|
+
fail(`unknown flag: ${a}`);
|
|
1973
|
+
} else {
|
|
1974
|
+
fail(`unexpected argument: ${a}`);
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
if (searchId === null || pick === null) {
|
|
1978
|
+
fail(
|
|
1979
|
+
"usage: slidemind read-source --search <id> --pick <n> [--from-slide <n>] [--to-slide <n>] [--to <presId>] [--out <file>]",
|
|
1980
|
+
);
|
|
1981
|
+
}
|
|
1982
|
+
const client = await import(
|
|
1983
|
+
pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
|
|
1984
|
+
);
|
|
1985
|
+
// Pane path: the searchId is presentation-scoped, so --to targets the window
|
|
1986
|
+
// the search ran on — same rule as view-slide. Direct path: the record is the
|
|
1987
|
+
// backend's, so no window is involved.
|
|
1988
|
+
const routing = await resolveEngineRouting(client, { to, searchId });
|
|
1989
|
+
let res;
|
|
1990
|
+
try {
|
|
1991
|
+
res = await client.readSourcePresentation({
|
|
1992
|
+
presId: routing.presId,
|
|
1993
|
+
searchId,
|
|
1994
|
+
selection: pick,
|
|
1995
|
+
fromSlide,
|
|
1996
|
+
toSlide,
|
|
1997
|
+
backendURL: routing.backendURL,
|
|
1998
|
+
});
|
|
1999
|
+
} catch (err) {
|
|
2000
|
+
// The stale-snapshot line is authored once and knows about an open pane;
|
|
2001
|
+
// everything else is this command's own vocabulary.
|
|
2002
|
+
fail(
|
|
2003
|
+
(await staleSnapshotMessage(client, err, { to })) ??
|
|
2004
|
+
describeReadSourceFailure(err, searchId, pick),
|
|
2005
|
+
);
|
|
2006
|
+
}
|
|
2007
|
+
if (!res?.markdown) {
|
|
2008
|
+
fail("read-source returned no content. Try the search again, then re-pick.");
|
|
2009
|
+
}
|
|
2010
|
+
if (outFile) {
|
|
2011
|
+
const target = path.resolve(process.cwd(), outFile);
|
|
2012
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
2013
|
+
fs.writeFileSync(target, res.markdown, "utf8");
|
|
2014
|
+
process.stdout.write(
|
|
2015
|
+
`Wrote the source presentation of result ${pick} (searchId ${searchId}) to ${target}\n`,
|
|
2016
|
+
);
|
|
2017
|
+
} else {
|
|
2018
|
+
process.stdout.write(res.markdown.endsWith("\n") ? res.markdown : `${res.markdown}\n`);
|
|
2019
|
+
}
|
|
2020
|
+
// Truncation is the one thing a reader must not miss: the body they just got
|
|
2021
|
+
// is a slide map, not the deck.
|
|
2022
|
+
if (res.truncated) {
|
|
2023
|
+
process.stderr.write(
|
|
2024
|
+
`note: this deck was too long to return in full — the output is its slide map. ` +
|
|
2025
|
+
`Re-run with --from-slide/--to-slide to read a range.\n`,
|
|
2026
|
+
);
|
|
2027
|
+
} else if (res.totalSlideCount && res.slideCount < res.totalSlideCount) {
|
|
2028
|
+
process.stderr.write(
|
|
2029
|
+
`note: showing ${res.slideCount} of ${res.totalSlideCount} slides.\n`,
|
|
2030
|
+
);
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
/**
|
|
2035
|
+
* Turn a source-presentation failure into the action that actually fixes it.
|
|
2036
|
+
* A stale permission snapshot is handled one level up, in staleSnapshotMessage
|
|
2037
|
+
* — it is the one blocker only the USER can clear and it reads identically
|
|
2038
|
+
* across every command.
|
|
2039
|
+
*/
|
|
2040
|
+
function describeReadSourceFailure(err, searchId, pick) {
|
|
2041
|
+
const code = err?.code ?? err?.body?.code ?? null;
|
|
2042
|
+
switch (code) {
|
|
2043
|
+
case "unknown_search":
|
|
2044
|
+
return `searchId ${searchId} is no longer available on that window. Run 'slidemind search' again, then re-pick.`;
|
|
2045
|
+
case "wrong_result_type":
|
|
2046
|
+
return `result ${pick} has no source presentation — only slide search results come from a deck (image and collection results do not).`;
|
|
2047
|
+
case "access_denied":
|
|
2048
|
+
return "you do not have access to that result's source presentation.";
|
|
2049
|
+
case "no_indexed_content":
|
|
2050
|
+
return "that presentation has no indexed slides yet. It may still be syncing — try again later.";
|
|
2051
|
+
case "invalid_source":
|
|
2052
|
+
return `result ${pick} carries no source file reference. Re-run the search, or pick a different result.`;
|
|
2053
|
+
case "source_presentation_unsupported":
|
|
2054
|
+
return err.message;
|
|
2055
|
+
default:
|
|
2056
|
+
return `read-source failed: ${err?.message ?? "unknown error"}`;
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
|
|
1375
2060
|
async function cmdDownloadResult(rest) {
|
|
1376
2061
|
let searchId = null;
|
|
1377
2062
|
let picks = null;
|
|
@@ -1381,10 +2066,7 @@ async function cmdDownloadResult(rest) {
|
|
|
1381
2066
|
for (let i = 0; i < rest.length; i++) {
|
|
1382
2067
|
const a = rest[i];
|
|
1383
2068
|
if (a === "--search") {
|
|
1384
|
-
|
|
1385
|
-
const n = parseInt(next, 10);
|
|
1386
|
-
if (!Number.isFinite(n)) fail(`--search expects a searchId, got '${next}'`);
|
|
1387
|
-
searchId = n;
|
|
2069
|
+
searchId = parseSearchIdArg(rest[++i]);
|
|
1388
2070
|
} else if (a === "--pick") {
|
|
1389
2071
|
picks = parsePicks(rest[++i]);
|
|
1390
2072
|
} else if (a === "--deck") {
|
|
@@ -1404,7 +2086,7 @@ async function cmdDownloadResult(rest) {
|
|
|
1404
2086
|
}
|
|
1405
2087
|
if (searchId === null || !picks || !deck) {
|
|
1406
2088
|
fail(
|
|
1407
|
-
"usage: slidemind download-result --search <id> --pick <n[,n,...]> --deck <deck> --to <presId> [--icon-color <hex[,hex,...]>]",
|
|
2089
|
+
"usage: slidemind download-result --search <id> --pick <n[,n,...]> --deck <deck> [--to <presId>] [--icon-color <hex[,hex,...]>]",
|
|
1408
2090
|
);
|
|
1409
2091
|
}
|
|
1410
2092
|
const iconColors = parseIconColors(iconColorsRaw, picks.length);
|
|
@@ -1419,10 +2101,15 @@ async function cmdDownloadResult(rest) {
|
|
|
1419
2101
|
const { downloadOneResult } = await import(
|
|
1420
2102
|
pathToFileURL(path.join(SRC_DIR, "bridge", "engine-download.mjs")).href
|
|
1421
2103
|
);
|
|
1422
|
-
|
|
1423
|
-
//
|
|
1424
|
-
//
|
|
1425
|
-
|
|
2104
|
+
// Direct path: an opaque searchId routes to the record's own environment,
|
|
2105
|
+
// else the deck's bound design decides; no window needed. Pane path: the
|
|
2106
|
+
// searchId is presentation-scoped, so --to targets the window the search ran
|
|
2107
|
+
// on (without it, >1 open window is a dead-end).
|
|
2108
|
+
const routing = await resolveEngineRouting(client, {
|
|
2109
|
+
to,
|
|
2110
|
+
brandId: await deckBrandId(deckDir),
|
|
2111
|
+
searchId,
|
|
2112
|
+
});
|
|
1426
2113
|
|
|
1427
2114
|
// Concurrent — each download is its own bridge round-trip; allSettled so one
|
|
1428
2115
|
// bad selection doesn't sink the rest of the batch.
|
|
@@ -1430,8 +2117,8 @@ async function cmdDownloadResult(rest) {
|
|
|
1430
2117
|
picks.map((selection, i) =>
|
|
1431
2118
|
downloadOneResult({
|
|
1432
2119
|
client,
|
|
1433
|
-
presId:
|
|
1434
|
-
backendURL:
|
|
2120
|
+
presId: routing.presId,
|
|
2121
|
+
backendURL: routing.backendURL,
|
|
1435
2122
|
searchId,
|
|
1436
2123
|
assetsDir,
|
|
1437
2124
|
selection,
|
|
@@ -1439,10 +2126,22 @@ async function cmdDownloadResult(rest) {
|
|
|
1439
2126
|
}),
|
|
1440
2127
|
),
|
|
1441
2128
|
);
|
|
2129
|
+
// One user-clearable blocker sinks the whole batch identically — every pick
|
|
2130
|
+
// would fail the same way, and the fix is one action, not N error lines.
|
|
2131
|
+
const stale = settled.find(
|
|
2132
|
+
(r) => r.status === "rejected" && client.permissionsStaleBlocker?.(r.reason),
|
|
2133
|
+
);
|
|
2134
|
+
if (stale) fail(await staleSnapshotMessage(client, stale.reason, { to }));
|
|
1442
2135
|
const results = settled.map((r, i) =>
|
|
1443
2136
|
r.status === "fulfilled"
|
|
1444
2137
|
? r.value
|
|
1445
|
-
: {
|
|
2138
|
+
: {
|
|
2139
|
+
selection: picks[i],
|
|
2140
|
+
error:
|
|
2141
|
+
describeHostOnlyEngineFailure(r.reason) ??
|
|
2142
|
+
r.reason?.message ??
|
|
2143
|
+
String(r.reason),
|
|
2144
|
+
},
|
|
1446
2145
|
);
|
|
1447
2146
|
const { formatSize } = await import(
|
|
1448
2147
|
pathToFileURL(path.join(SRC_DIR, "bridge", "image-dimensions.mjs")).href
|
|
@@ -1787,26 +2486,28 @@ function publicConnectionStatus(status, client) {
|
|
|
1787
2486
|
};
|
|
1788
2487
|
}
|
|
1789
2488
|
|
|
1790
|
-
// The
|
|
1791
|
-
//
|
|
1792
|
-
//
|
|
1793
|
-
//
|
|
1794
|
-
//
|
|
1795
|
-
//
|
|
1796
|
-
//
|
|
1797
|
-
//
|
|
1798
|
-
//
|
|
1799
|
-
//
|
|
1800
|
-
//
|
|
1801
|
-
//
|
|
1802
|
-
//
|
|
1803
|
-
|
|
2489
|
+
// The readiness ladder splits in two, because the two halves fail
|
|
2490
|
+
// independently: BACKEND AUTHORING readiness (a session, a reachable add-in
|
|
2491
|
+
// backend, a supported CLI — everything render, bundle and the pane-free engine
|
|
2492
|
+
// routes need) and LIVE POWERPOINT readiness (a registered window, render-ready
|
|
2493
|
+
// metadata, an awake pane — what push/insert-slide and reading an open deck
|
|
2494
|
+
// need). Keeping them apart is the whole point of doc 13: a dead pane is no
|
|
2495
|
+
// longer "cannot work", it only defers the last mile.
|
|
2496
|
+
//
|
|
2497
|
+
// Each rung is a STRUCTURED blocker: `code` is the machine-copeable reason an
|
|
2498
|
+
// agent switches on; `actor` says who can clear it — only `cli_update_required`
|
|
2499
|
+
// is the AGENT's to fix (it can run `slidemind update`), every other rung needs
|
|
2500
|
+
// a human at the add-in. The one exception is a plugin-installed CLI:
|
|
2501
|
+
// `/plugin` is the only updater that replaces the binary on the session's PATH,
|
|
2502
|
+
// and the agent cannot drive it, so that rung hands back to the user (see
|
|
2503
|
+
// src/install-channel.mjs).
|
|
2504
|
+
function backendBlocker(pub) {
|
|
1804
2505
|
if (!pub.paired) {
|
|
1805
2506
|
return {
|
|
1806
2507
|
code: "not_paired",
|
|
1807
2508
|
actor: "user",
|
|
1808
2509
|
message:
|
|
1809
|
-
"Not
|
|
2510
|
+
"Not connected. In PowerPoint open slidemind → Settings → General → Connect CLI, press the link button, and run the command it shows.",
|
|
1810
2511
|
};
|
|
1811
2512
|
}
|
|
1812
2513
|
if ((pub.backends ?? []).every((b) => !b.ok)) {
|
|
@@ -1830,6 +2531,15 @@ function connectionBlocker(pub) {
|
|
|
1830
2531
|
updateCliInstruction(),
|
|
1831
2532
|
};
|
|
1832
2533
|
}
|
|
2534
|
+
return null;
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
// The live-PowerPoint half. Never gates render, bundle or the pane-free engine
|
|
2538
|
+
// routes — it only says whether the ops that genuinely type into an open deck
|
|
2539
|
+
// (push's insert, insert-slide, reading an unsaved presentation) can run now.
|
|
2540
|
+
// The `message` strings are BYTE-IDENTICAL to the pre-split ladder so doctor's
|
|
2541
|
+
// printed output never churns.
|
|
2542
|
+
function paneBlocker(pub) {
|
|
1833
2543
|
if ((pub.presentations ?? []).length === 0) {
|
|
1834
2544
|
return {
|
|
1835
2545
|
code: "no_window",
|
|
@@ -1864,18 +2574,273 @@ function connectionBlocker(pub) {
|
|
|
1864
2574
|
code: "pane_suspended",
|
|
1865
2575
|
actor: "user",
|
|
1866
2576
|
message:
|
|
1867
|
-
"Rendering is available, but pane operations are suspended. Bring PowerPoint forward and click/reopen the slidemind pane before push
|
|
2577
|
+
"Rendering is available, but pane operations are suspended. Bring PowerPoint forward and click/reopen the slidemind pane before push, insert-slide, or reading this window.",
|
|
1868
2578
|
};
|
|
1869
2579
|
}
|
|
1870
2580
|
return null;
|
|
1871
2581
|
}
|
|
1872
2582
|
|
|
2583
|
+
// The two ladders end-to-end, in the order a user hits them: nothing about a
|
|
2584
|
+
// window matters while the backend half is unmet. `slidemind ready` layers
|
|
2585
|
+
// `ambiguous_design` on top and treats `pane_suspended` as non-blocking (a
|
|
2586
|
+
// render-ready-but-suspended window is still a design source).
|
|
2587
|
+
function connectionBlocker(pub) {
|
|
2588
|
+
return backendBlocker(pub) ?? paneBlocker(pub);
|
|
2589
|
+
}
|
|
2590
|
+
|
|
1873
2591
|
// Back-compat projection for doctor: just the blocker's message, or "Ready."
|
|
1874
2592
|
// when the ladder is clear. Keeps the ladder authored in ONE place.
|
|
1875
2593
|
function connectionNextAction(pub) {
|
|
1876
2594
|
return connectionBlocker(pub)?.message ?? "Ready.";
|
|
1877
2595
|
}
|
|
1878
2596
|
|
|
2597
|
+
// The permission SNAPSHOT behind corpus search ages out when nobody opens the
|
|
2598
|
+
// add-in for a while. It is a DEGRADATION, never a blocker: render, bundle and
|
|
2599
|
+
// every already-materialized asset keep working, only corpus search stops — so
|
|
2600
|
+
// it must not sit on either ladder. Newer add-ins report it on the readiness
|
|
2601
|
+
// capabilities; the message is authored once, in the client, next to the live
|
|
2602
|
+
// 403 shape the engine routes return (client.permissionsStaleBlocker).
|
|
2603
|
+
// Which connected environments report an aged-out snapshot on
|
|
2604
|
+
// `/api/cli/readiness`. Newer add-ins emit the flag; older ones never do, and
|
|
2605
|
+
// there the condition only surfaces as a live 403 on a search.
|
|
2606
|
+
function permissionsStaleEnvs(pub) {
|
|
2607
|
+
return (pub.backends ?? [])
|
|
2608
|
+
.filter((b) => b.ok && b.capabilities?.permissionsStale === true)
|
|
2609
|
+
.map((b) => b.env);
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2612
|
+
function permissionsStaleDegradation(pub, client) {
|
|
2613
|
+
const stale = (pub.backends ?? []).find(
|
|
2614
|
+
(b) => b.ok && b.capabilities?.permissionsStale === true,
|
|
2615
|
+
);
|
|
2616
|
+
if (!stale) return null;
|
|
2617
|
+
const action = stale.capabilities?.permissionsStaleAction;
|
|
2618
|
+
return client.permissionsStaleBlocker({
|
|
2619
|
+
code: "permissions_stale",
|
|
2620
|
+
body: typeof action === "string" ? { action } : {},
|
|
2621
|
+
});
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2624
|
+
// ---------- one-command setup (npx bootstrap) ----------
|
|
2625
|
+
//
|
|
2626
|
+
// `npx --yes slidemind@latest setup <claim>` is the whole onboarding: the user
|
|
2627
|
+
// clicks "Connect CLI" in the add-in, pastes ONE command, and ends up with a
|
|
2628
|
+
// connected, globally-installed CLI. npx only runs the package from its cache —
|
|
2629
|
+
// it never installs it — so this command performs the persistent
|
|
2630
|
+
// `npm install -g` itself.
|
|
2631
|
+
//
|
|
2632
|
+
// Order matters: redeem FIRST. The claim is short-lived and one-time, while a
|
|
2633
|
+
// global npm install can take a while or hit an EACCES prefix problem. Spending
|
|
2634
|
+
// the claim up front means a failed install leaves a connected CLI the user can
|
|
2635
|
+
// fix at their leisure, instead of forcing them back to PowerPoint for a fresh
|
|
2636
|
+
// command.
|
|
2637
|
+
async function cmdSetup(rest) {
|
|
2638
|
+
let claimArg = null;
|
|
2639
|
+
// `auto` is the whole point: setup finishes onboarding by itself, so it
|
|
2640
|
+
// deploys into the coding agents this machine actually has instead of asking
|
|
2641
|
+
// the user to run `init` afterwards.
|
|
2642
|
+
let target = "auto";
|
|
2643
|
+
let doInstall = true;
|
|
2644
|
+
let doReady = true;
|
|
2645
|
+
let asJson = false;
|
|
2646
|
+
for (let i = 0; i < rest.length; i++) {
|
|
2647
|
+
const a = rest[i];
|
|
2648
|
+
if (a === "--for" || a === "-f") {
|
|
2649
|
+
target = rest[++i];
|
|
2650
|
+
if (!target) fail("--for expects a target");
|
|
2651
|
+
if (![...HARNESS_TARGETS, "all", "auto", "none"].includes(target)) {
|
|
2652
|
+
fail(`unknown install target: ${target}`);
|
|
2653
|
+
}
|
|
2654
|
+
} else if (a === "--no-install") {
|
|
2655
|
+
doInstall = false;
|
|
2656
|
+
} else if (a === "--no-ready") {
|
|
2657
|
+
doReady = false;
|
|
2658
|
+
} else if (a === "--json") {
|
|
2659
|
+
asJson = true;
|
|
2660
|
+
} else if (a.startsWith("-")) {
|
|
2661
|
+
fail(`unknown flag: ${a}`);
|
|
2662
|
+
} else if (claimArg === null) {
|
|
2663
|
+
claimArg = a;
|
|
2664
|
+
} else {
|
|
2665
|
+
fail("setup takes exactly one setup claim");
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2669
|
+
if (!claimArg) {
|
|
2670
|
+
fail(
|
|
2671
|
+
"setup needs the connection command from the add-in.\n" +
|
|
2672
|
+
" Open PowerPoint → slidemind → Settings → General → Connect CLI, copy the command, and paste it here.\n" +
|
|
2673
|
+
" Already have the CLI? The panel's `CLI already installed?` toggle gives the short `slidemind setup …` form.",
|
|
2674
|
+
);
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
const pkg = readPackageJson();
|
|
2678
|
+
const claims = await loadBridgeModule("setup-claim");
|
|
2679
|
+
// Decode before any UI so a mistyped/partial paste fails in one clean line.
|
|
2680
|
+
const { backendURL, secret } = claims.decodeSetupClaim(claimArg);
|
|
2681
|
+
const cfg = await loadBridgeModule("config");
|
|
2682
|
+
const envLabel = cfg.deriveEnvLabel(backendURL);
|
|
2683
|
+
|
|
2684
|
+
// The installers paint their own progress rail. In --json mode stdout is
|
|
2685
|
+
// reserved for the payload, so silence the whole UI rather than letting it
|
|
2686
|
+
// corrupt what the caller parses.
|
|
2687
|
+
const ui = await loadInstallerUi();
|
|
2688
|
+
if (asJson) ui.silence();
|
|
2689
|
+
ui.banner(pkg.version, "setup");
|
|
2690
|
+
|
|
2691
|
+
// --- 1. spend the claim -------------------------------------------------
|
|
2692
|
+
await ui.section("connect", envLabel);
|
|
2693
|
+
const sessionToken = await claims.redeemSetupClaim({
|
|
2694
|
+
backendURL,
|
|
2695
|
+
secret,
|
|
2696
|
+
cliVersion: pkg.version,
|
|
2697
|
+
});
|
|
2698
|
+
cfg.upsertSession({ backendURL, sessionToken, label: envLabel });
|
|
2699
|
+
ui.step({ label: "authorize", value: envLabel, note: "session saved" });
|
|
2700
|
+
|
|
2701
|
+
// --- 2. make this CLI permanent ----------------------------------------
|
|
2702
|
+
// Without this the whole thing evaporates with npx's cache: the user would be
|
|
2703
|
+
// connected but have no `slidemind` on PATH.
|
|
2704
|
+
let installed = false;
|
|
2705
|
+
let installError = null;
|
|
2706
|
+
const pluginInstall = isClaudePluginInstall();
|
|
2707
|
+
if (doInstall && pluginInstall) {
|
|
2708
|
+
ui.step({
|
|
2709
|
+
status: "warn",
|
|
2710
|
+
label: "install",
|
|
2711
|
+
value: "skipped",
|
|
2712
|
+
note: "this slidemind came from the Claude Code plugin — update it with /plugin",
|
|
2713
|
+
});
|
|
2714
|
+
} else if (doInstall) {
|
|
2715
|
+
await ui.section("install", "npm global");
|
|
2716
|
+
ui.step({ label: "install", value: "slidemind@latest", note: "global (npm)" });
|
|
2717
|
+
try {
|
|
2718
|
+
await spawnInherit(npmBin(), [
|
|
2719
|
+
"install",
|
|
2720
|
+
"-g",
|
|
2721
|
+
"--loglevel=error",
|
|
2722
|
+
"slidemind@latest",
|
|
2723
|
+
]);
|
|
2724
|
+
installed = true;
|
|
2725
|
+
} catch (err) {
|
|
2726
|
+
installError = err?.message || String(err);
|
|
2727
|
+
ui.step({
|
|
2728
|
+
status: "warn",
|
|
2729
|
+
label: "install",
|
|
2730
|
+
value: "failed",
|
|
2731
|
+
note: "the connection is saved — see the note below",
|
|
2732
|
+
});
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
// --- 3. deploy the skills ----------------------------------------------
|
|
2737
|
+
// In-process, from THIS package copy: the npx copy and the global install are
|
|
2738
|
+
// the same published version, and going through PATH here would fail on a
|
|
2739
|
+
// shell that has not rehashed yet.
|
|
2740
|
+
//
|
|
2741
|
+
// With the default `auto` this is what removes `slidemind init` from
|
|
2742
|
+
// onboarding: the skills land in every coding agent present on the machine,
|
|
2743
|
+
// and skip any whose plugin already supplies them (copying over a plugin's
|
|
2744
|
+
// skills plants a second set that drifts on the next plugin update).
|
|
2745
|
+
let skills = [];
|
|
2746
|
+
let skippedPlugins = [];
|
|
2747
|
+
if (target !== "none") {
|
|
2748
|
+
if (target === "auto") {
|
|
2749
|
+
const harnesses = await detectHarnesses();
|
|
2750
|
+
skippedPlugins = harnesses
|
|
2751
|
+
.filter((h) => h.present && h.pluginInstalled)
|
|
2752
|
+
.map((h) => h.target);
|
|
2753
|
+
}
|
|
2754
|
+
await ui.section(
|
|
2755
|
+
"skills",
|
|
2756
|
+
target === "auto"
|
|
2757
|
+
? "detected harnesses"
|
|
2758
|
+
: target === "all"
|
|
2759
|
+
? "all harnesses"
|
|
2760
|
+
: target,
|
|
2761
|
+
);
|
|
2762
|
+
skills = await runInstaller(target, "install", []);
|
|
2763
|
+
for (const t of skippedPlugins) {
|
|
2764
|
+
ui.step({
|
|
2765
|
+
status: "warn",
|
|
2766
|
+
label: t,
|
|
2767
|
+
value: "skipped",
|
|
2768
|
+
note: "its slidemind plugin already supplies the skills",
|
|
2769
|
+
});
|
|
2770
|
+
}
|
|
2771
|
+
if (skills.length === 0 && skippedPlugins.length === 0) {
|
|
2772
|
+
ui.step({
|
|
2773
|
+
status: "warn",
|
|
2774
|
+
label: "harnesses",
|
|
2775
|
+
value: "none detected",
|
|
2776
|
+
note: "install a coding agent, then run `slidemind init --for auto`",
|
|
2777
|
+
});
|
|
2778
|
+
}
|
|
2779
|
+
const bundleMod = await loadBridgeModule("bundle");
|
|
2780
|
+
bundleMod.clearBundleCache();
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2783
|
+
if (asJson) {
|
|
2784
|
+
const globalInstall =
|
|
2785
|
+
doInstall && !pluginInstall
|
|
2786
|
+
? installed
|
|
2787
|
+
? "installed"
|
|
2788
|
+
: "failed"
|
|
2789
|
+
: "skipped";
|
|
2790
|
+
process.stdout.write(
|
|
2791
|
+
`${JSON.stringify(
|
|
2792
|
+
{
|
|
2793
|
+
ok: true,
|
|
2794
|
+
connected: true,
|
|
2795
|
+
env: envLabel,
|
|
2796
|
+
globalInstall,
|
|
2797
|
+
skills,
|
|
2798
|
+
// Present-but-plugin-served harnesses, so an agent can tell "nothing
|
|
2799
|
+
// deployed because nothing is installed" from "nothing needed".
|
|
2800
|
+
skippedPlugins,
|
|
2801
|
+
nextAction: "slidemind ready",
|
|
2802
|
+
// Only on the unhappy path: the session IS saved, so `ok` stays true,
|
|
2803
|
+
// but an agent needs to know the binary did not land.
|
|
2804
|
+
...(installError ? { installError } : {}),
|
|
2805
|
+
},
|
|
2806
|
+
null,
|
|
2807
|
+
2,
|
|
2808
|
+
)}\n`,
|
|
2809
|
+
);
|
|
2810
|
+
return;
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
ui.done(
|
|
2814
|
+
target === "none" || (skills.length === 0 && skippedPlugins.length === 0)
|
|
2815
|
+
? `connected to ${envLabel} — run 'slidemind ready' to start`
|
|
2816
|
+
: `connected to ${envLabel} and ready to use`,
|
|
2817
|
+
);
|
|
2818
|
+
|
|
2819
|
+
if (installError) {
|
|
2820
|
+
process.stderr.write(
|
|
2821
|
+
"\nslidemind is connected, but the global install could not write to npm's global package directory.\n" +
|
|
2822
|
+
"This usually happens on macOS/Linux when Node was installed with a system-owned npm prefix.\n\n" +
|
|
2823
|
+
"Avoid `sudo npm`. Use Homebrew/nvm, or move npm's global prefix into your home directory:\n\n" +
|
|
2824
|
+
" mkdir -p ~/.npm-global\n" +
|
|
2825
|
+
" npm config set prefix ~/.npm-global\n" +
|
|
2826
|
+
" echo 'export PATH=\"$HOME/.npm-global/bin:$PATH\"' >> ~/.zshrc\n" +
|
|
2827
|
+
" source ~/.zshrc\n" +
|
|
2828
|
+
" npm install -g slidemind\n\n" +
|
|
2829
|
+
`(details: ${installError})\n`,
|
|
2830
|
+
);
|
|
2831
|
+
return;
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
// --- 4. finish the session the user actually came for -------------------
|
|
2835
|
+
// `ready` exits non-zero for every legitimate not-yet blocker (PowerPoint
|
|
2836
|
+
// closed, no design activated). Setup already succeeded at that point, so its
|
|
2837
|
+
// blocker is information, not a failure — run it for the message and the
|
|
2838
|
+
// warmed bundle, and never inherit its exit code.
|
|
2839
|
+
if (doReady) {
|
|
2840
|
+
await spawnStatus(process.execPath, [__filename, "ready"]);
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
|
|
1879
2844
|
// The one-command agent preflight. Collapses the old 4-call session start
|
|
1880
2845
|
// (presentations → pick → download-bundle → download-bundle-to-bind) into a
|
|
1881
2846
|
// single verb: check pairing/reachability/CLI-version/windows, resolve the OPEN
|
|
@@ -1890,6 +2855,7 @@ async function cmdReady(rest) {
|
|
|
1890
2855
|
let asJson = false;
|
|
1891
2856
|
let force = false;
|
|
1892
2857
|
let connectOnly = false;
|
|
2858
|
+
let brand = null;
|
|
1893
2859
|
const positional = [];
|
|
1894
2860
|
for (let i = 0; i < rest.length; i++) {
|
|
1895
2861
|
const a = rest[i];
|
|
@@ -1902,12 +2868,18 @@ async function cmdReady(rest) {
|
|
|
1902
2868
|
force = true;
|
|
1903
2869
|
} else if (a === "--connect-only") {
|
|
1904
2870
|
connectOnly = true;
|
|
2871
|
+
} else if (a === "--brand") {
|
|
2872
|
+
brand = rest[++i];
|
|
2873
|
+
if (!brand) fail("--brand expects a design id (presFP)");
|
|
1905
2874
|
} else if (a.startsWith("-")) {
|
|
1906
2875
|
fail(`unknown flag: ${a}`);
|
|
1907
2876
|
} else {
|
|
1908
2877
|
positional.push(a);
|
|
1909
2878
|
}
|
|
1910
2879
|
}
|
|
2880
|
+
if (brand && to) {
|
|
2881
|
+
fail("--brand and --to are mutually exclusive (a design id or a window, not both)");
|
|
2882
|
+
}
|
|
1911
2883
|
|
|
1912
2884
|
// --- Local: resolve the deck dir (never create it) and read its existing
|
|
1913
2885
|
// bundle ref. deckPresFP feeds ONLY the mirror tie-break + `suggested`; it
|
|
@@ -1954,10 +2926,20 @@ async function cmdReady(rest) {
|
|
|
1954
2926
|
fail(blocker.message);
|
|
1955
2927
|
};
|
|
1956
2928
|
|
|
1957
|
-
// ---
|
|
2929
|
+
// --- First use: there is nothing this process can do on its own. Connecting
|
|
2930
|
+
// starts in the add-in because the claim it mints carries the deployment's
|
|
2931
|
+
// address, and an unconnected CLI has no way to learn which backend its user
|
|
2932
|
+
// belongs to. So name the one action that works and stop. ---
|
|
1958
2933
|
const cfg = await loadBridgeModule("config");
|
|
1959
2934
|
if (cfg.readSessions().length === 0) {
|
|
1960
|
-
finishBlocker({
|
|
2935
|
+
finishBlocker({
|
|
2936
|
+
blocker: {
|
|
2937
|
+
code: "not_connected",
|
|
2938
|
+
actor: "user",
|
|
2939
|
+
message:
|
|
2940
|
+
"Connect this CLI first: in PowerPoint open slidemind → Settings → General → Connect CLI, press the link button, copy the command it shows, and run it here.",
|
|
2941
|
+
},
|
|
2942
|
+
});
|
|
1961
2943
|
return;
|
|
1962
2944
|
}
|
|
1963
2945
|
|
|
@@ -1987,10 +2969,162 @@ async function cmdReady(rest) {
|
|
|
1987
2969
|
return;
|
|
1988
2970
|
}
|
|
1989
2971
|
|
|
2972
|
+
// --- Backend authoring readiness beyond the connection itself: which
|
|
2973
|
+
// environments serve the pane-free engine routes. Probed at most once (and
|
|
2974
|
+
// only when a payload actually reports it), and never fatal — an add-in
|
|
2975
|
+
// without them still authors, it just relays search through a window. ---
|
|
2976
|
+
let engineInfo = null;
|
|
2977
|
+
const engines = async () => {
|
|
2978
|
+
if (!engineInfo) {
|
|
2979
|
+
try {
|
|
2980
|
+
engineInfo = await client.engineReachability({
|
|
2981
|
+
backends: statusRaw.backends,
|
|
2982
|
+
});
|
|
2983
|
+
} catch {
|
|
2984
|
+
engineInfo = { available: false, envs: [] };
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
return engineInfo;
|
|
2988
|
+
};
|
|
2989
|
+
|
|
2990
|
+
// --- A design resolved WITHOUT a window — the deck's own binding, an
|
|
2991
|
+
// explicit --brand, or the backend's brand list. Same payload either way:
|
|
2992
|
+
// ready, `windowless: true`, and a pane status that is INFORMATIONAL. What
|
|
2993
|
+
// the missing window actually costs depends on the engine routes: with them
|
|
2994
|
+
// only push/insert-slide and reading an open presentation need PowerPoint.
|
|
2995
|
+
//
|
|
2996
|
+
// "Windowless" is about how the DESIGN was resolved, not about the registry:
|
|
2997
|
+
// --brand runs before the registry is consulted at all, and the later rungs
|
|
2998
|
+
// fire on "nothing render-ready", which a connected-but-still-matching window
|
|
2999
|
+
// satisfies. So the pane status is DERIVED from the registry as it stands at
|
|
3000
|
+
// call time (the hydration wait reassigns it), and the prose only claims
|
|
3001
|
+
// "no PowerPoint window" when there genuinely is none. ---
|
|
3002
|
+
const finishWindowless = async (result) => {
|
|
3003
|
+
if (deckDir) {
|
|
3004
|
+
bundle.writeDeckBundleRef(deckDir, {
|
|
3005
|
+
presFP: result.presFP,
|
|
3006
|
+
etag: result.etag,
|
|
3007
|
+
});
|
|
3008
|
+
}
|
|
3009
|
+
const designName = readDesignName(result.dir);
|
|
3010
|
+
const env = result.backendURL ? cfg.deriveEnvLabel(result.backendURL) : null;
|
|
3011
|
+
const eng = await engines();
|
|
3012
|
+
const paneReady = registry.some(client.isPaneReadyPresentation);
|
|
3013
|
+
const noWindow = registry.length === 0;
|
|
3014
|
+
const nextAction = noWindow
|
|
3015
|
+
? eng.available
|
|
3016
|
+
? "Ready (no PowerPoint window — authoring, engine search and build-pptx all work; only push/insert-slide and reading an open presentation need one)."
|
|
3017
|
+
: "Ready (no PowerPoint window — build-pptx/build/review/view-draft work; push/search/read need a pane)."
|
|
3018
|
+
: paneReady
|
|
3019
|
+
? "Ready (design resolved without targeting a window; a registered PowerPoint window is available for push/insert-slide)."
|
|
3020
|
+
: eng.available
|
|
3021
|
+
? "Ready (design resolved without targeting a window; pane operations — push/insert-slide/read of a window — will wait for a pane click; engine search does not)."
|
|
3022
|
+
: "Ready (design resolved without targeting a window; pane operations — push/search/read — will wait for a pane click).";
|
|
3023
|
+
if (asJson) {
|
|
3024
|
+
process.stdout.write(
|
|
3025
|
+
JSON.stringify(
|
|
3026
|
+
{
|
|
3027
|
+
ready: true,
|
|
3028
|
+
windowless: true,
|
|
3029
|
+
deck: deckSlug,
|
|
3030
|
+
bound: Boolean(deckDir),
|
|
3031
|
+
env,
|
|
3032
|
+
presId: null,
|
|
3033
|
+
presFP: result.presFP,
|
|
3034
|
+
designName,
|
|
3035
|
+
bundleDir: result.dir,
|
|
3036
|
+
bundleEtag: result.etag ?? null,
|
|
3037
|
+
paneSuspended: !paneReady,
|
|
3038
|
+
engines: eng,
|
|
3039
|
+
warnings: result.warnings ?? [],
|
|
3040
|
+
nextAction,
|
|
3041
|
+
},
|
|
3042
|
+
null,
|
|
3043
|
+
2,
|
|
3044
|
+
) + "\n",
|
|
3045
|
+
);
|
|
3046
|
+
return;
|
|
3047
|
+
}
|
|
3048
|
+
const designLabel = designName
|
|
3049
|
+
? `${designName} (${result.presFP})`
|
|
3050
|
+
: result.presFP;
|
|
3051
|
+
process.stdout.write(
|
|
3052
|
+
`Ready — design ${designLabel}${env ? ` [${env}]` : ""}${noWindow ? " (no PowerPoint window)" : ""}\n`,
|
|
3053
|
+
);
|
|
3054
|
+
process.stdout.write(` bundle: ${result.dir} (etag ${result.etag ?? "?"})\n`);
|
|
3055
|
+
if (deckDir) process.stdout.write(` deck ${deckSlug} bound\n`);
|
|
3056
|
+
process.stdout.write(
|
|
3057
|
+
noWindow
|
|
3058
|
+
? eng.available
|
|
3059
|
+
? " Note: authoring, engine search and build-pptx need no window; push/insert-slide and reading an open presentation do.\n"
|
|
3060
|
+
: " Note: build-pptx/build/review/view-draft work without a window; push/search/read need a pane.\n"
|
|
3061
|
+
: paneReady
|
|
3062
|
+
? " Note: the design was resolved without targeting a window; a registered PowerPoint window is available for push/insert-slide.\n"
|
|
3063
|
+
: eng.available
|
|
3064
|
+
? " Note: pane operations (push/insert-slide/read of a window) will wait for a pane click; engine search does not.\n"
|
|
3065
|
+
: " Note: pane operations (push/search/read) will wait for a pane click.\n",
|
|
3066
|
+
);
|
|
3067
|
+
for (const w of result.warnings ?? []) {
|
|
3068
|
+
process.stderr.write(` warning: ${w}\n`);
|
|
3069
|
+
}
|
|
3070
|
+
};
|
|
3071
|
+
|
|
3072
|
+
// Materialize a design by id over the pane-free brand route. Returns null on
|
|
3073
|
+
// any miss (old add-in, unknown/revoked design) so the caller can fall
|
|
3074
|
+
// through to the window ladder instead of dead-ending.
|
|
3075
|
+
const materializeBrand = async (brandId) => {
|
|
3076
|
+
try {
|
|
3077
|
+
return await bundle.materializeBundle(null, { force, brandId });
|
|
3078
|
+
} catch {
|
|
3079
|
+
return null;
|
|
3080
|
+
}
|
|
3081
|
+
};
|
|
3082
|
+
|
|
3083
|
+
// --- --brand <presFP>: the user (or the agent, after an ambiguous brand
|
|
3084
|
+
// list) named the design outright. It resolves with or without a window, so
|
|
3085
|
+
// it runs before the registry is consulted at all. ---
|
|
3086
|
+
if (brand) {
|
|
3087
|
+
const result = await materializeBrand(brand);
|
|
3088
|
+
if (!result) {
|
|
3089
|
+
finishBlocker({
|
|
3090
|
+
blocker: {
|
|
3091
|
+
code: "unknown_brand",
|
|
3092
|
+
actor: "user",
|
|
3093
|
+
message: `No connected slidemind add-in serves design '${brand}'. Run 'slidemind ready' with no --brand to see which designs are available.`,
|
|
3094
|
+
},
|
|
3095
|
+
});
|
|
3096
|
+
return;
|
|
3097
|
+
}
|
|
3098
|
+
await finishWindowless(result);
|
|
3099
|
+
return;
|
|
3100
|
+
}
|
|
3101
|
+
|
|
3102
|
+
const isRR = client.isRenderReadyPresentation;
|
|
3103
|
+
|
|
3104
|
+
// --- Bound-deck ladder: session → bundle freshness, no pane precondition.
|
|
3105
|
+
// The design identity (presFP == brandId) lives in the deck's
|
|
3106
|
+
// .bundle-ref.json and the bundle syncs over the pane-free brand route, so
|
|
3107
|
+
// with PowerPoint CLOSED (an empty registry) a bound deck readies straight
|
|
3108
|
+
// away instead of sitting out the hydration wait for a window that isn't
|
|
3109
|
+
// coming. A registry that holds windows still hydrates first — the open
|
|
3110
|
+
// add-in stays the source of truth whenever there IS one. Skipped for
|
|
3111
|
+
// --connect-only (which resolves no design at all) and for --to (an explicit
|
|
3112
|
+
// window must resolve through the registry); a miss (old add-in, legacy ref,
|
|
3113
|
+
// revoked grant) falls through to the window ladder. ---
|
|
3114
|
+
const boundDeckLadder = deckDir && deckPresFP && !to && !connectOnly;
|
|
3115
|
+
let boundRungTried = false;
|
|
3116
|
+
if (boundDeckLadder && registry.length === 0) {
|
|
3117
|
+
boundRungTried = true;
|
|
3118
|
+
const result = await materializeBrand(deckPresFP);
|
|
3119
|
+
if (result) {
|
|
3120
|
+
await finishWindowless(result);
|
|
3121
|
+
return;
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
|
|
1990
3125
|
// --- Bounded hydration wait when nothing is render-ready yet: covers the
|
|
1991
3126
|
// just-clicked / just-woke pane still hydrating brand metadata. Polls even on
|
|
1992
3127
|
// an empty registry (that's the flaky window this papers over). ---
|
|
1993
|
-
const isRR = client.isRenderReadyPresentation;
|
|
1994
3128
|
if (!registry.some(isRR)) {
|
|
1995
3129
|
const waitMs =
|
|
1996
3130
|
parseInt(process.env.SLIDEMIND_BRIDGE_PRESENTATIONS_WAIT_MS || "6000", 10) ||
|
|
@@ -2004,74 +3138,89 @@ async function cmdReady(rest) {
|
|
|
2004
3138
|
}
|
|
2005
3139
|
}
|
|
2006
3140
|
if (!registry.some(isRR)) {
|
|
2007
|
-
//
|
|
2008
|
-
//
|
|
2009
|
-
//
|
|
2010
|
-
//
|
|
2011
|
-
//
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
3141
|
+
// --connect-only asks one question — "can I work?" — and since the engine
|
|
3142
|
+
// routes answer off the session, a backend that serves them IS the answer:
|
|
3143
|
+
// discovery, search and every per-result fetch run with PowerPoint closed.
|
|
3144
|
+
// Without them search is still relayed through a pane, so the window rung
|
|
3145
|
+
// below stands.
|
|
3146
|
+
if (connectOnly && (await engines()).available) {
|
|
3147
|
+
const eng = await engines();
|
|
3148
|
+
if (asJson) {
|
|
3149
|
+
process.stdout.write(
|
|
3150
|
+
JSON.stringify(
|
|
3151
|
+
{
|
|
3152
|
+
ready: true,
|
|
3153
|
+
connectOnly: true,
|
|
3154
|
+
windowless: true,
|
|
3155
|
+
engines: eng,
|
|
3156
|
+
nextAction:
|
|
3157
|
+
"Ready (no PowerPoint window — engine search runs off the connection; only insert-slide needs one).",
|
|
3158
|
+
backends: (pub.backends ?? []).map((b) => ({ env: b.env, ok: b.ok })),
|
|
3159
|
+
},
|
|
3160
|
+
null,
|
|
3161
|
+
2,
|
|
3162
|
+
) + "\n",
|
|
3163
|
+
);
|
|
3164
|
+
} else {
|
|
3165
|
+
process.stdout.write(
|
|
3166
|
+
`Connected. ${pub.backendCount} backend(s), 0 window(s) registered — ` +
|
|
3167
|
+
`engine search runs without one [${eng.envs.join(", ")}].\n`,
|
|
3168
|
+
);
|
|
3169
|
+
}
|
|
3170
|
+
return;
|
|
3171
|
+
}
|
|
3172
|
+
// The bound-deck ladder again, for the windows-registered-but-never-ready
|
|
3173
|
+
// case the pre-poll rung deliberately left to the hydration wait.
|
|
3174
|
+
if (boundDeckLadder && !boundRungTried) {
|
|
3175
|
+
boundRungTried = true;
|
|
3176
|
+
const result = await materializeBrand(deckPresFP);
|
|
3177
|
+
if (result) {
|
|
3178
|
+
await finishWindowless(result);
|
|
3179
|
+
return;
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
3182
|
+
// --- Unbound + no window: ask the backend which designs this user may
|
|
3183
|
+
// build against. One is the answer; several genuinely need a human choice,
|
|
3184
|
+
// which --brand records. A backend without the route (or a deck already
|
|
3185
|
+
// bound to a design the brand route just refused) falls through unchanged. ---
|
|
3186
|
+
if (!connectOnly && !to && !deckPresFP && !boundRungTried) {
|
|
3187
|
+
let brands = [];
|
|
2016
3188
|
try {
|
|
2017
|
-
|
|
2018
|
-
force,
|
|
2019
|
-
brandId: deckPresFP,
|
|
2020
|
-
});
|
|
3189
|
+
brands = await client.listBrands();
|
|
2021
3190
|
} catch {
|
|
2022
|
-
/*
|
|
3191
|
+
/* no brand list — the window ladder below is the answer */
|
|
2023
3192
|
}
|
|
2024
|
-
if (
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
});
|
|
2029
|
-
const designName = readDesignName(result.dir);
|
|
2030
|
-
const env = result.backendURL
|
|
2031
|
-
? cfg.deriveEnvLabel(result.backendURL)
|
|
2032
|
-
: null;
|
|
2033
|
-
const nextAction =
|
|
2034
|
-
"Ready (no PowerPoint window — build-pptx/build/review/view-draft work; push/search/read need a pane).";
|
|
2035
|
-
if (asJson) {
|
|
2036
|
-
process.stdout.write(
|
|
2037
|
-
JSON.stringify(
|
|
2038
|
-
{
|
|
2039
|
-
ready: true,
|
|
2040
|
-
windowless: true,
|
|
2041
|
-
deck: deckSlug,
|
|
2042
|
-
bound: true,
|
|
2043
|
-
env,
|
|
2044
|
-
presId: null,
|
|
2045
|
-
presFP: result.presFP,
|
|
2046
|
-
designName,
|
|
2047
|
-
bundleDir: result.dir,
|
|
2048
|
-
bundleEtag: result.etag ?? null,
|
|
2049
|
-
paneSuspended: true,
|
|
2050
|
-
warnings: result.warnings ?? [],
|
|
2051
|
-
nextAction,
|
|
2052
|
-
},
|
|
2053
|
-
null,
|
|
2054
|
-
2,
|
|
2055
|
-
) + "\n",
|
|
2056
|
-
);
|
|
3193
|
+
if (brands.length === 1) {
|
|
3194
|
+
const result = await materializeBrand(brands[0].presFP);
|
|
3195
|
+
if (result) {
|
|
3196
|
+
await finishWindowless(result);
|
|
2057
3197
|
return;
|
|
2058
3198
|
}
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
:
|
|
2062
|
-
process.stdout.write(
|
|
2063
|
-
`Ready — design ${designLabel}${env ? ` [${env}]` : ""} (no PowerPoint window)\n`,
|
|
2064
|
-
);
|
|
2065
|
-
process.stdout.write(
|
|
2066
|
-
` bundle: ${result.dir} (etag ${result.etag ?? "?"})\n`,
|
|
3199
|
+
} else if (brands.length > 1) {
|
|
3200
|
+
const rows = brands.map(
|
|
3201
|
+
(b) => ` ${b.presFP}${b.env ? ` [${b.env}]` : ""} ${b.name ?? ""}`.trimEnd(),
|
|
2067
3202
|
);
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
}
|
|
3203
|
+
const blocker = {
|
|
3204
|
+
code: "ambiguous_brand",
|
|
3205
|
+
actor: "user",
|
|
3206
|
+
message:
|
|
3207
|
+
"Several corporate designs are available and no PowerPoint window says which — ask the user which to build against, then re-run: slidemind ready" +
|
|
3208
|
+
`${deckSlug ? ` ${deckSlug}` : ""} --brand <presFP>.`,
|
|
3209
|
+
};
|
|
3210
|
+
finishBlocker({
|
|
3211
|
+
blocker,
|
|
3212
|
+
extras: {
|
|
3213
|
+
brands: brands.map((b) => ({
|
|
3214
|
+
presFP: b.presFP,
|
|
3215
|
+
name: b.name ?? null,
|
|
3216
|
+
env: b.env ?? null,
|
|
3217
|
+
})),
|
|
3218
|
+
},
|
|
3219
|
+
humanLines:
|
|
3220
|
+
"slidemind: several corporate designs are available — ask the user which to build against:\n" +
|
|
3221
|
+
`${rows.join("\n")}\n` +
|
|
3222
|
+
`Then re-run: slidemind ready${deckSlug ? ` ${deckSlug}` : ""} --brand <presFP>`,
|
|
3223
|
+
});
|
|
2075
3224
|
return;
|
|
2076
3225
|
}
|
|
2077
3226
|
}
|
|
@@ -2104,6 +3253,7 @@ async function cmdReady(rest) {
|
|
|
2104
3253
|
{
|
|
2105
3254
|
ready: true,
|
|
2106
3255
|
connectOnly: true,
|
|
3256
|
+
engines: await engines(),
|
|
2107
3257
|
nextAction: "Ready.",
|
|
2108
3258
|
backends: (pub.backends ?? []).map((b) => ({ env: b.env, ok: b.ok })),
|
|
2109
3259
|
},
|
|
@@ -2227,6 +3377,7 @@ async function cmdReady(rest) {
|
|
|
2227
3377
|
|
|
2228
3378
|
const designName = readDesignName(result.dir);
|
|
2229
3379
|
const paneSuspended = !client.isPaneReadyPresentation(entry);
|
|
3380
|
+
const engineInfoForWindow = await engines();
|
|
2230
3381
|
|
|
2231
3382
|
if (asJson) {
|
|
2232
3383
|
process.stdout.write(
|
|
@@ -2246,6 +3397,7 @@ async function cmdReady(rest) {
|
|
|
2246
3397
|
fileName: entry.fileName ?? null,
|
|
2247
3398
|
},
|
|
2248
3399
|
paneSuspended,
|
|
3400
|
+
engines: engineInfoForWindow,
|
|
2249
3401
|
warnings: result.warnings ?? [],
|
|
2250
3402
|
nextAction: "Ready.",
|
|
2251
3403
|
},
|
|
@@ -2270,7 +3422,9 @@ async function cmdReady(rest) {
|
|
|
2270
3422
|
if (bound) process.stdout.write(` deck ${deckSlug} bound\n`);
|
|
2271
3423
|
if (paneSuspended) {
|
|
2272
3424
|
process.stdout.write(
|
|
2273
|
-
|
|
3425
|
+
engineInfoForWindow.available
|
|
3426
|
+
? " Note: pane operations (push/insert-slide/read of this window) will wait for a pane click; engine search does not.\n"
|
|
3427
|
+
: " Note: pane operations (push/search/read) will wait for a pane click.\n",
|
|
2274
3428
|
);
|
|
2275
3429
|
}
|
|
2276
3430
|
for (const w of result.warnings ?? []) {
|
|
@@ -2289,11 +3443,14 @@ async function cmdDoctor(rest) {
|
|
|
2289
3443
|
// still reaches the reader, as the `cli_update_required` rung of the ladder.
|
|
2290
3444
|
client.setDiagnosticMode(true);
|
|
2291
3445
|
if (sessions.length === 0) {
|
|
3446
|
+
const blocker = backendBlocker({ paired: false });
|
|
2292
3447
|
const pub = {
|
|
2293
3448
|
paired: false,
|
|
2294
3449
|
backendCount: 0,
|
|
2295
3450
|
backends: [],
|
|
2296
3451
|
presentations: [],
|
|
3452
|
+
authoring: { ready: false, blocker, degraded: null, engines: null },
|
|
3453
|
+
host: { ready: false, blocker: paneBlocker({ presentations: [] }) },
|
|
2297
3454
|
nextAction: connectionNextAction({ paired: false }),
|
|
2298
3455
|
};
|
|
2299
3456
|
process[asJson ? "stdout" : "stderr"].write(
|
|
@@ -2301,7 +3458,30 @@ async function cmdDoctor(rest) {
|
|
|
2301
3458
|
);
|
|
2302
3459
|
return;
|
|
2303
3460
|
}
|
|
2304
|
-
const
|
|
3461
|
+
const statusRaw = await client.connectionStatus();
|
|
3462
|
+
const status = publicConnectionStatus(statusRaw, client);
|
|
3463
|
+
// The two ladders are INDEPENDENT: authoring (session → reachable backend →
|
|
3464
|
+
// CLI version → engine reachability) is what render, bundle and the pane-free
|
|
3465
|
+
// engine routes ride on, while the live-PowerPoint ladder only governs
|
|
3466
|
+
// push/insert-slide and reading an open presentation. Reporting them
|
|
3467
|
+
// separately is the point — a dead pane is not "cannot work".
|
|
3468
|
+
const authoringBlocker = backendBlocker(status);
|
|
3469
|
+
let engines = { available: false, envs: [] };
|
|
3470
|
+
if (!authoringBlocker) {
|
|
3471
|
+
try {
|
|
3472
|
+
engines = await client.engineReachability({ backends: statusRaw.backends });
|
|
3473
|
+
} catch {
|
|
3474
|
+
/* leave it as unavailable — never fail a diagnosis on a probe */
|
|
3475
|
+
}
|
|
3476
|
+
}
|
|
3477
|
+
status.authoring = {
|
|
3478
|
+
ready: !authoringBlocker,
|
|
3479
|
+
blocker: authoringBlocker,
|
|
3480
|
+
degraded: permissionsStaleDegradation(status, client),
|
|
3481
|
+
engines,
|
|
3482
|
+
};
|
|
3483
|
+
const hostBlocker = paneBlocker(status);
|
|
3484
|
+
status.host = { ready: !hostBlocker, blocker: hostBlocker };
|
|
2305
3485
|
status.nextAction = connectionNextAction(status);
|
|
2306
3486
|
if (asJson) {
|
|
2307
3487
|
process.stdout.write(`${JSON.stringify(status, null, 2)}\n`);
|
|
@@ -2315,6 +3495,32 @@ async function cmdDoctor(rest) {
|
|
|
2315
3495
|
`\n`,
|
|
2316
3496
|
);
|
|
2317
3497
|
}
|
|
3498
|
+
process.stdout.write(
|
|
3499
|
+
`Backend authoring ready: ${status.authoring.ready ? "yes" : "no"}` +
|
|
3500
|
+
`${status.authoring.ready ? "" : ` — ${authoringBlocker.message}`}\n`,
|
|
3501
|
+
);
|
|
3502
|
+
// One condition, one cause, named the same way everywhere: when the readiness
|
|
3503
|
+
// capabilities already say the permission snapshot has aged out, this line
|
|
3504
|
+
// says so too, instead of reporting a healthy pane-free surface that a corpus
|
|
3505
|
+
// search is about to 403 on for a reason the user learns only then.
|
|
3506
|
+
const staleEnvs = permissionsStaleEnvs(status);
|
|
3507
|
+
process.stdout.write(
|
|
3508
|
+
` engine search (no window needed): ${
|
|
3509
|
+
engines.available
|
|
3510
|
+
? `yes [${engines.envs.join(", ")}]` +
|
|
3511
|
+
(staleEnvs.length
|
|
3512
|
+
? ` — corpus search degraded on ${staleEnvs.join(", ")}: stale permission snapshot`
|
|
3513
|
+
: "")
|
|
3514
|
+
: "no — relayed through an open pane"
|
|
3515
|
+
}\n`,
|
|
3516
|
+
);
|
|
3517
|
+
if (status.authoring.degraded) {
|
|
3518
|
+
process.stdout.write(` degraded: ${status.authoring.degraded.message}\n`);
|
|
3519
|
+
}
|
|
3520
|
+
process.stdout.write(
|
|
3521
|
+
`Live PowerPoint ready: ${status.host.ready ? "yes" : "no"}` +
|
|
3522
|
+
`${status.host.ready ? "" : ` — ${hostBlocker.message}`}\n`,
|
|
3523
|
+
);
|
|
2318
3524
|
if (status.presentations.length === 0) {
|
|
2319
3525
|
process.stdout.write("Open presentations: none\n");
|
|
2320
3526
|
} else {
|
|
@@ -2456,93 +3662,23 @@ async function loadBridgeModule(name) {
|
|
|
2456
3662
|
return import(pathToFileURL(path.join(SRC_DIR, "bridge", `${name}.mjs`)).href);
|
|
2457
3663
|
}
|
|
2458
3664
|
|
|
2459
|
-
async function cmdPair(rest) {
|
|
2460
|
-
let code = null;
|
|
2461
|
-
let label = null;
|
|
2462
|
-
for (let i = 0; i < rest.length; i++) {
|
|
2463
|
-
const a = rest[i];
|
|
2464
|
-
if (a === "--label") {
|
|
2465
|
-
label = rest[++i];
|
|
2466
|
-
if (!label) fail("--label expects a name");
|
|
2467
|
-
} else if (!a.startsWith("--") && !code) {
|
|
2468
|
-
code = a;
|
|
2469
|
-
}
|
|
2470
|
-
}
|
|
2471
|
-
if (!code) {
|
|
2472
|
-
fail('usage: slidemind pair <code> (copy the command from the add-in\'s "Connect CLI")');
|
|
2473
|
-
}
|
|
2474
|
-
const pairMod = await loadBridgeModule("pair");
|
|
2475
|
-
const decoded = pairMod.decodePairingCode(code);
|
|
2476
|
-
if (!decoded) {
|
|
2477
|
-
fail("invalid pairing code — copy the exact `slidemind pair …` command shown in the add-in");
|
|
2478
|
-
}
|
|
2479
|
-
const res = await pairMod.httpJson(
|
|
2480
|
-
"POST",
|
|
2481
|
-
`${decoded.backendURL}/api/cli/pair/redeem`,
|
|
2482
|
-
{ body: { secret: decoded.secret } },
|
|
2483
|
-
);
|
|
2484
|
-
if (res.status !== 200 || !res.json?.sessionToken) {
|
|
2485
|
-
fail(
|
|
2486
|
-
`pairing failed (${res.status}): ${res.json?.error || "no session returned"}\n` +
|
|
2487
|
-
"the code may have expired — generate a fresh one in the add-in's \"Connect CLI\"",
|
|
2488
|
-
);
|
|
2489
|
-
}
|
|
2490
|
-
const cfg = await loadBridgeModule("config");
|
|
2491
|
-
const sessions = cfg.upsertSession({
|
|
2492
|
-
backendURL: decoded.backendURL,
|
|
2493
|
-
sessionToken: res.json.sessionToken,
|
|
2494
|
-
label,
|
|
2495
|
-
});
|
|
2496
|
-
const env = cfg.deriveEnvLabel(decoded.backendURL);
|
|
2497
|
-
const others = sessions.length - 1;
|
|
2498
|
-
const suffix =
|
|
2499
|
-
others > 0
|
|
2500
|
-
? ` (${sessions.length} environments connected — commands now fan out across all of them)`
|
|
2501
|
-
: "";
|
|
2502
|
-
let verify = "";
|
|
2503
|
-
try {
|
|
2504
|
-
const client = await import(
|
|
2505
|
-
pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
|
|
2506
|
-
);
|
|
2507
|
-
const deadline = Date.now() + 8000;
|
|
2508
|
-
for (;;) {
|
|
2509
|
-
const status = await client.connectionStatus();
|
|
2510
|
-
if ((status.presentations ?? []).length > 0) {
|
|
2511
|
-
verify = " Pane is registered.";
|
|
2512
|
-
break;
|
|
2513
|
-
}
|
|
2514
|
-
if (Date.now() >= deadline) {
|
|
2515
|
-
verify =
|
|
2516
|
-
" No pane is registered yet; if PowerPoint is already open, reopen or click the slidemind task pane, then run 'slidemind doctor'.";
|
|
2517
|
-
break;
|
|
2518
|
-
}
|
|
2519
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2520
|
-
}
|
|
2521
|
-
} catch {
|
|
2522
|
-
// Pairing itself succeeded; diagnostics remain available via doctor.
|
|
2523
|
-
}
|
|
2524
|
-
process.stdout.write(
|
|
2525
|
-
`Paired with ${label || env}${suffix}.${verify} Run 'slidemind doctor' to verify.\n`,
|
|
2526
|
-
);
|
|
2527
|
-
}
|
|
2528
|
-
|
|
2529
3665
|
async function cmdWhoami() {
|
|
2530
3666
|
const cfg = await loadBridgeModule("config");
|
|
2531
3667
|
const sessions = cfg.readSessions();
|
|
2532
3668
|
if (sessions.length === 0) {
|
|
2533
3669
|
process.stderr.write(
|
|
2534
|
-
|
|
3670
|
+
"Not connected. In PowerPoint open slidemind → Settings → General → Connect CLI, press the link button, and run the command it shows.\n",
|
|
2535
3671
|
);
|
|
2536
3672
|
return;
|
|
2537
3673
|
}
|
|
2538
|
-
const
|
|
3674
|
+
const { httpJson } = await loadBridgeModule("http-json");
|
|
2539
3675
|
const def = cfg.getDefaultBackend();
|
|
2540
3676
|
const lines = await Promise.all(
|
|
2541
3677
|
sessions.map(async (s) => {
|
|
2542
3678
|
const marker = s.backendURL === def ? "*" : " ";
|
|
2543
3679
|
let detail;
|
|
2544
3680
|
try {
|
|
2545
|
-
const res = await
|
|
3681
|
+
const res = await httpJson(
|
|
2546
3682
|
"GET",
|
|
2547
3683
|
`${s.backendURL}/api/cli/whoami`,
|
|
2548
3684
|
{ token: s.sessionToken },
|
|
@@ -2550,15 +3686,15 @@ async function cmdWhoami() {
|
|
|
2550
3686
|
detail =
|
|
2551
3687
|
res.status === 200 && res.json
|
|
2552
3688
|
? `user ${res.json.oid} · tenant ${res.json.tid ?? "(n/a)"}`
|
|
2553
|
-
: `session invalid (${res.status}) —
|
|
3689
|
+
: `session invalid (${res.status}) — authorize again`;
|
|
2554
3690
|
} catch {
|
|
2555
|
-
detail = "unreachable — check your connection or
|
|
3691
|
+
detail = "unreachable — check your connection or authorize again";
|
|
2556
3692
|
}
|
|
2557
3693
|
return `${marker} ${s.label}\n ${detail}`;
|
|
2558
3694
|
}),
|
|
2559
3695
|
);
|
|
2560
3696
|
process.stdout.write(
|
|
2561
|
-
`
|
|
3697
|
+
`Connected environments (${sessions.length}):\n${lines.join("\n")}\n`,
|
|
2562
3698
|
);
|
|
2563
3699
|
}
|
|
2564
3700
|
|
|
@@ -2593,10 +3729,10 @@ async function cmdLogout(rest) {
|
|
|
2593
3729
|
toRemove = sessions.filter((s) => s.backendURL === def);
|
|
2594
3730
|
}
|
|
2595
3731
|
|
|
2596
|
-
const
|
|
3732
|
+
const { httpJson } = await loadBridgeModule("http-json");
|
|
2597
3733
|
for (const s of toRemove) {
|
|
2598
3734
|
try {
|
|
2599
|
-
await
|
|
3735
|
+
await httpJson("POST", `${s.backendURL}/api/cli/logout`, {
|
|
2600
3736
|
token: s.sessionToken,
|
|
2601
3737
|
});
|
|
2602
3738
|
} catch {
|
|
@@ -2607,7 +3743,7 @@ async function cmdLogout(rest) {
|
|
|
2607
3743
|
const remaining = cfg.readSessions().length;
|
|
2608
3744
|
process.stdout.write(
|
|
2609
3745
|
`Logged out of ${toRemove.map((s) => s.label).join(", ")}.` +
|
|
2610
|
-
(remaining > 0 ? ` ${remaining} environment(s) still
|
|
3746
|
+
(remaining > 0 ? ` ${remaining} environment(s) still connected.\n` : "\n"),
|
|
2611
3747
|
);
|
|
2612
3748
|
}
|
|
2613
3749
|
|
|
@@ -2785,20 +3921,56 @@ async function cmdNew(rest) {
|
|
|
2785
3921
|
);
|
|
2786
3922
|
}
|
|
2787
3923
|
|
|
2788
|
-
|
|
3924
|
+
const HARNESS_TARGETS = ["claude", "codex", "opencode"];
|
|
3925
|
+
|
|
3926
|
+
function parseTargetFlag(rest, { allowAuto = false } = {}) {
|
|
2789
3927
|
const idx = rest.findIndex((a) => a === "--for" || a === "-f");
|
|
2790
3928
|
if (idx < 0 || !rest[idx + 1]) {
|
|
2791
|
-
fail(
|
|
3929
|
+
fail(
|
|
3930
|
+
`missing --for <claude|codex|opencode|all${allowAuto ? "|auto" : ""}>`,
|
|
3931
|
+
);
|
|
2792
3932
|
}
|
|
2793
3933
|
const target = rest[idx + 1];
|
|
2794
|
-
|
|
3934
|
+
const allowed = allowAuto
|
|
3935
|
+
? [...HARNESS_TARGETS, "all", "auto"]
|
|
3936
|
+
: [...HARNESS_TARGETS, "all"];
|
|
3937
|
+
if (!allowed.includes(target)) {
|
|
2795
3938
|
fail(`unknown install target: ${target}`);
|
|
2796
3939
|
}
|
|
2797
3940
|
return target;
|
|
2798
3941
|
}
|
|
2799
3942
|
|
|
3943
|
+
// Which coding agents are actually on this machine, and which of them already
|
|
3944
|
+
// get their skills from a plugin. This is what lets `slidemind setup` finish the
|
|
3945
|
+
// job on its own: it deploys where a harness exists and stays out of the way
|
|
3946
|
+
// where a plugin already supplies the skills, so a separate `init` is no longer
|
|
3947
|
+
// part of onboarding.
|
|
3948
|
+
async function detectHarnesses() {
|
|
3949
|
+
const found = [];
|
|
3950
|
+
for (const target of HARNESS_TARGETS) {
|
|
3951
|
+
const mod = await import(
|
|
3952
|
+
pathToFileURL(path.join(INSTALLERS_DIR, `${target}.mjs`)).href
|
|
3953
|
+
);
|
|
3954
|
+
const info =
|
|
3955
|
+
typeof mod.detect === "function"
|
|
3956
|
+
? mod.detect()
|
|
3957
|
+
: { present: false, pluginInstalled: false, root: null };
|
|
3958
|
+
found.push({ target, ...info });
|
|
3959
|
+
}
|
|
3960
|
+
return found;
|
|
3961
|
+
}
|
|
3962
|
+
|
|
3963
|
+
/** Expand a `--for` value into the concrete harness list to act on. */
|
|
3964
|
+
async function resolveInstallTargets(target) {
|
|
3965
|
+
if (target === "all") return [...HARNESS_TARGETS];
|
|
3966
|
+
if (target !== "auto") return [target];
|
|
3967
|
+
return (await detectHarnesses())
|
|
3968
|
+
.filter((h) => h.present && !h.pluginInstalled)
|
|
3969
|
+
.map((h) => h.target);
|
|
3970
|
+
}
|
|
3971
|
+
|
|
2800
3972
|
async function runInstaller(target, action, rest) {
|
|
2801
|
-
const targets =
|
|
3973
|
+
const targets = await resolveInstallTargets(target);
|
|
2802
3974
|
for (const t of targets) {
|
|
2803
3975
|
const mod = await import(
|
|
2804
3976
|
pathToFileURL(path.join(INSTALLERS_DIR, `${t}.mjs`)).href
|
|
@@ -2812,6 +3984,7 @@ async function runInstaller(target, action, rest) {
|
|
|
2812
3984
|
flags: rest,
|
|
2813
3985
|
});
|
|
2814
3986
|
}
|
|
3987
|
+
return targets;
|
|
2815
3988
|
}
|
|
2816
3989
|
|
|
2817
3990
|
async function loadInstallerUi() {
|
|
@@ -2822,10 +3995,18 @@ async function loadInstallerUi() {
|
|
|
2822
3995
|
}
|
|
2823
3996
|
|
|
2824
3997
|
async function cmdInstall(rest) {
|
|
2825
|
-
const target = parseTargetFlag(rest);
|
|
3998
|
+
const target = parseTargetFlag(rest, { allowAuto: true });
|
|
2826
3999
|
const ui = await loadInstallerUi();
|
|
2827
4000
|
ui.banner(readPackageJson().version, "init");
|
|
2828
|
-
await runInstaller(target, "install", rest);
|
|
4001
|
+
const deployed = await runInstaller(target, "install", rest);
|
|
4002
|
+
if (deployed.length === 0) {
|
|
4003
|
+
ui.step({
|
|
4004
|
+
status: "warn",
|
|
4005
|
+
label: "harnesses",
|
|
4006
|
+
value: "none detected",
|
|
4007
|
+
note: "install a coding agent, then rerun with --for <claude|codex|opencode>",
|
|
4008
|
+
});
|
|
4009
|
+
}
|
|
2829
4010
|
|
|
2830
4011
|
// Force every design bundle to re-download from the add-in on the next
|
|
2831
4012
|
// build/push/download by wiping the materialized cache. init is the
|
|
@@ -2843,9 +4024,11 @@ async function cmdInstall(rest) {
|
|
|
2843
4024
|
});
|
|
2844
4025
|
|
|
2845
4026
|
ui.done(
|
|
2846
|
-
|
|
2847
|
-
? "
|
|
2848
|
-
:
|
|
4027
|
+
deployed.length === 0
|
|
4028
|
+
? "no coding agent found — the CLI is installed, skills are not"
|
|
4029
|
+
: deployed.length === HARNESS_TARGETS.length
|
|
4030
|
+
? "ready — ask your agent to plan, create, or edit a deck"
|
|
4031
|
+
: `ready — ${deployed.join(", ")} can now plan, create, or edit decks`,
|
|
2849
4032
|
);
|
|
2850
4033
|
}
|
|
2851
4034
|
|
|
@@ -2900,6 +4083,16 @@ function spawnInherit(file, args) {
|
|
|
2900
4083
|
});
|
|
2901
4084
|
}
|
|
2902
4085
|
|
|
4086
|
+
// Like spawnInherit, but resolves with the child's exit code instead of
|
|
4087
|
+
// rejecting — for a step whose failure is a message to the user rather than a
|
|
4088
|
+
// failure of the command that ran it (see `setup` → `ready`).
|
|
4089
|
+
function spawnStatus(file, args) {
|
|
4090
|
+
return spawnInherit(file, args).then(
|
|
4091
|
+
() => 0,
|
|
4092
|
+
() => 1,
|
|
4093
|
+
);
|
|
4094
|
+
}
|
|
4095
|
+
|
|
2903
4096
|
// ---------- distribution (public npm) ----------
|
|
2904
4097
|
//
|
|
2905
4098
|
// slidemind is published to public npm as `slidemind`. First install is
|
|
@@ -3040,6 +4233,9 @@ async function main(rawArgv) {
|
|
|
3040
4233
|
case "build-pptx":
|
|
3041
4234
|
await cmdBuildPptx(rest);
|
|
3042
4235
|
return 0;
|
|
4236
|
+
case "inspect-template-deck":
|
|
4237
|
+
await cmdInspectTemplateDeck(rest);
|
|
4238
|
+
return 0;
|
|
3043
4239
|
case "view-draft":
|
|
3044
4240
|
await cmdViewDraft(rest);
|
|
3045
4241
|
return 0;
|
|
@@ -3052,6 +4248,9 @@ async function main(rawArgv) {
|
|
|
3052
4248
|
case "view-slide":
|
|
3053
4249
|
await cmdViewSlide(rest);
|
|
3054
4250
|
return 0;
|
|
4251
|
+
case "read-source":
|
|
4252
|
+
await cmdReadSource(rest);
|
|
4253
|
+
return 0;
|
|
3055
4254
|
case "insert-slide":
|
|
3056
4255
|
await cmdInsertSlide(rest);
|
|
3057
4256
|
return 0;
|
|
@@ -3070,12 +4269,12 @@ async function main(rawArgv) {
|
|
|
3070
4269
|
case "ready":
|
|
3071
4270
|
await cmdReady(rest);
|
|
3072
4271
|
return 0;
|
|
4272
|
+
case "setup":
|
|
4273
|
+
await cmdSetup(rest);
|
|
4274
|
+
return 0;
|
|
3073
4275
|
case "new":
|
|
3074
4276
|
await cmdNew(rest);
|
|
3075
4277
|
return 0;
|
|
3076
|
-
case "pair":
|
|
3077
|
-
await cmdPair(rest);
|
|
3078
|
-
break;
|
|
3079
4278
|
case "whoami":
|
|
3080
4279
|
await cmdWhoami(rest);
|
|
3081
4280
|
break;
|