slidemind 0.3.20 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/AGENTS.md +592 -16
  4. package/LICENSE +4 -0
  5. package/README.md +203 -8
  6. package/THIRD-PARTY-NOTICES.md +201 -0
  7. package/bin/slidemind.mjs +50 -2
  8. package/package.json +5 -3
  9. package/skills/create-slides/SKILL.md +484 -886
  10. package/skills/search/SKILL.md +46 -23
  11. package/src/bridge/build-and-push.mjs +548 -73
  12. package/src/bridge/bundle.mjs +100 -31
  13. package/src/bridge/capture.mjs +14 -17
  14. package/src/bridge/client/brands.mjs +157 -18
  15. package/src/bridge/client/engine-routing.mjs +48 -13
  16. package/src/bridge/client/engine-search.mjs +24 -1
  17. package/src/bridge/client/render.mjs +27 -1
  18. package/src/bridge/client/transport.mjs +499 -23
  19. package/src/bridge/client.mjs +10 -0
  20. package/src/bridge/config.mjs +67 -6
  21. package/src/bridge/engine-download.mjs +0 -10
  22. package/src/bridge/graph-auth.mjs +55 -3
  23. package/src/bridge/image-intent-envelope.mjs +239 -0
  24. package/src/bridge/package-preview.mjs +67 -0
  25. package/src/bridge/sharepoint.mjs +914 -0
  26. package/src/builders/asset-fetch.mjs +203 -0
  27. package/src/builders/block-dividers.mjs +83 -0
  28. package/src/builders/build.mjs +171 -87
  29. package/src/builders/card-height.mjs +294 -0
  30. package/src/builders/chart-hole.mjs +183 -0
  31. package/src/builders/chart-jsx.mjs +410 -0
  32. package/src/builders/chart-labels.mjs +326 -0
  33. package/src/builders/concept-geometry.mjs +370 -0
  34. package/src/builders/concept-intent.mjs +200 -0
  35. package/src/builders/concept-layout.mjs +79 -0
  36. package/src/builders/concept-lines.mjs +509 -0
  37. package/src/builders/concept-lists.mjs +270 -0
  38. package/src/builders/concept-marks.mjs +84 -0
  39. package/src/builders/concept-relationships.mjs +12 -0
  40. package/src/builders/concept-spec.mjs +645 -0
  41. package/src/builders/concept-text.mjs +140 -0
  42. package/src/builders/concept-waves.mjs +57 -0
  43. package/src/builders/copy-fit.mjs +139 -0
  44. package/src/builders/design-tokens.mjs +347 -0
  45. package/src/builders/draft-check.mjs +552 -340
  46. package/src/builders/draft-lines.mjs +369 -0
  47. package/src/builders/edit-route.mjs +29 -0
  48. package/src/builders/figure-sketch.mjs +375 -0
  49. package/src/builders/fold-sets.mjs +639 -0
  50. package/src/builders/gate-server.mjs +9 -4
  51. package/src/builders/gate-session.js +273 -12
  52. package/src/builders/gate-shell.js +1494 -0
  53. package/src/builders/image-intent-check.mjs +308 -0
  54. package/src/builders/image-use-reconcile.mjs +107 -0
  55. package/src/builders/kit-helpers.mjs +121 -0
  56. package/src/builders/layout-names.mjs +32 -0
  57. package/src/builders/legacy-reference-uses.mjs +179 -0
  58. package/src/builders/line-fit.mjs +224 -0
  59. package/src/builders/line-native.mjs +895 -0
  60. package/src/builders/line-relations.mjs +230 -0
  61. package/src/builders/lint-fix.mjs +117 -9
  62. package/src/builders/lint.mjs +25 -20
  63. package/src/builders/list-jsx.mjs +88 -0
  64. package/src/builders/placement.mjs +133 -0
  65. package/src/builders/plan-checks.mjs +135 -0
  66. package/src/builders/plan-comments.mjs +19 -0
  67. package/src/builders/plan-handlers.mjs +95 -0
  68. package/src/builders/plan-markdown-edit.mjs +3 -3
  69. package/src/builders/plan-overlay.js +1530 -1608
  70. package/src/builders/plan.mjs +689 -186
  71. package/src/builders/preset-definitions.json +1 -0
  72. package/src/builders/preset-eval.mjs +244 -0
  73. package/src/builders/preview.html +51 -120
  74. package/src/builders/red-thread-backbone.mjs +135 -36
  75. package/src/builders/repair.mjs +205 -0
  76. package/src/builders/repeat-groups.mjs +540 -0
  77. package/src/builders/resolve-skill.mjs +5 -4
  78. package/src/builders/review-overlay.js +296 -293
  79. package/src/builders/review.mjs +305 -84
  80. package/src/builders/rule-shift.mjs +96 -0
  81. package/src/builders/seat.mjs +354 -0
  82. package/src/builders/settle-driver.mjs +236 -0
  83. package/src/builders/settle.mjs +1448 -0
  84. package/src/builders/slide-prep.mjs +67 -0
  85. package/src/builders/spec-assets.mjs +236 -0
  86. package/src/builders/spec-match.mjs +56 -0
  87. package/src/builders/stack.mjs +413 -0
  88. package/src/builders/storyboard-markdown.mjs +70 -27
  89. package/src/builders/storyboard-schema.mjs +54 -114
  90. package/src/builders/text-blocks.mjs +212 -0
  91. package/src/builders/text-card-fit.mjs +552 -0
  92. package/src/builders/text-stack.mjs +276 -0
  93. package/src/builders/transcribe.mjs +1069 -0
  94. package/src/cli/assets.mjs +164 -0
  95. package/src/cli/common.mjs +173 -12
  96. package/src/cli/concept-spec-support.mjs +76 -0
  97. package/src/cli/concepts.mjs +1002 -103
  98. package/src/cli/deck.mjs +401 -65
  99. package/src/cli/diagnose.mjs +4 -2
  100. package/src/cli/engine-helpers.mjs +17 -10
  101. package/src/cli/engine-results.mjs +76 -3
  102. package/src/cli/help.mjs +232 -28
  103. package/src/cli/install.mjs +9 -0
  104. package/src/cli/maintenance.mjs +104 -0
  105. package/src/cli/read.mjs +2 -1
  106. package/src/cli/ready.mjs +6 -3
  107. package/src/cli/record.mjs +522 -0
  108. package/src/cli/remote.mjs +818 -0
  109. package/src/cli/revision.mjs +686 -0
  110. package/src/cli/search.mjs +30 -11
  111. package/src/cli/session.mjs +56 -13
  112. package/src/cli/setup.mjs +46 -2
  113. package/src/cli/status.mjs +142 -33
  114. package/src/cli/tasks.mjs +246 -0
  115. package/src/cli/transcribe.mjs +854 -0
  116. package/src/cli/workspace-ui.mjs +260 -0
  117. package/src/cli/workspace.mjs +212 -34
  118. package/src/install-channel.mjs +64 -7
  119. package/src/lib/adopt.mjs +346 -25
  120. package/src/lib/concept-spec-binding.mjs +96 -0
  121. package/src/lib/conversions.mjs +70 -6
  122. package/src/lib/convert-folded.mjs +107 -0
  123. package/src/lib/deck-paths.mjs +418 -0
  124. package/src/lib/durable-ids.mjs +47 -0
  125. package/src/lib/edit-operations.mjs +316 -0
  126. package/src/lib/edit-state.mjs +1244 -0
  127. package/src/lib/edit-transitions.mjs +120 -0
  128. package/src/lib/extensions.mjs +501 -0
  129. package/src/lib/image-assets.mjs +465 -0
  130. package/src/lib/image-derivatives.mjs +234 -0
  131. package/src/lib/image-intent-fingerprint.mjs +70 -0
  132. package/src/lib/image-intent-tags.mjs +176 -0
  133. package/src/lib/image-intent.mjs +802 -0
  134. package/src/lib/image-use-overrides.mjs +279 -0
  135. package/src/lib/inline-components.mjs +613 -0
  136. package/src/lib/lint-converted.mjs +68 -4
  137. package/src/lib/merge-json.mjs +163 -0
  138. package/src/lib/overwrite-guard.mjs +3 -3
  139. package/src/lib/presentation-context.mjs +531 -0
  140. package/src/lib/preservation-tags.mjs +157 -0
  141. package/src/lib/preservation-verdict.mjs +1342 -0
  142. package/src/lib/publish.mjs +1557 -0
  143. package/src/lib/rebase.mjs +945 -0
  144. package/src/lib/reconcile-ack.mjs +4 -6
  145. package/src/lib/reconcile.mjs +62 -24
  146. package/src/lib/record-client.mjs +175 -0
  147. package/src/lib/record-state.mjs +151 -0
  148. package/src/lib/remote.mjs +661 -0
  149. package/src/lib/revision.mjs +1210 -0
  150. package/src/lib/slide-guids.mjs +4 -6
  151. package/src/lib/slide-preservation.mjs +423 -0
  152. package/src/lib/slide-selection.mjs +9 -0
  153. package/src/lib/workspace-transaction.mjs +117 -0
  154. package/src/pptx/content-digest.mjs +334 -0
  155. package/src/pptx/deck-assemble.mjs +1278 -0
  156. package/src/pptx/fill/engine.mjs +2 -3
  157. package/src/pptx/fill/freeform.mjs +6 -3
  158. package/src/pptx/ooxml.mjs +20 -0
  159. package/src/pptx/package-evidence.mjs +383 -0
  160. package/src/pptx/tags.mjs +163 -11
  161. package/src/update-check.mjs +22 -4
  162. package/src/workspace/adapters.mjs +222 -0
  163. package/src/workspace/auth.mjs +256 -0
  164. package/src/workspace/autostart.mjs +204 -0
  165. package/src/workspace/daemon.mjs +360 -0
  166. package/src/workspace/discovery.mjs +180 -0
  167. package/src/workspace/edit.mjs +167 -0
  168. package/src/workspace/events.mjs +52 -0
  169. package/src/workspace/gate-client.mjs +290 -0
  170. package/src/workspace/handback.mjs +73 -0
  171. package/src/workspace/lifecycle.mjs +635 -0
  172. package/src/workspace/operations.mjs +200 -0
  173. package/src/workspace/pick-folder.mjs +152 -0
  174. package/src/workspace/registry.mjs +359 -0
  175. package/src/workspace/remote-links.mjs +397 -0
  176. package/src/workspace/remote-status.mjs +359 -0
  177. package/src/workspace/routes.mjs +1331 -0
  178. package/src/workspace/server.mjs +159 -0
  179. package/src/workspace/sync.mjs +1119 -0
  180. package/src/workspace/task-actions.mjs +215 -0
  181. package/src/workspace/tasks.mjs +507 -0
  182. package/src/workspace/ui/app.js +1886 -0
  183. package/src/workspace/ui/index.html +141 -0
  184. package/src/workspace/ui/plan-bridge.js +120 -0
  185. package/src/workspace/ui/sharepoint.svg +59 -0
  186. package/src/workspace/ui/styles.css +423 -0
  187. package/src/builders/visual-families.mjs +0 -46
@@ -0,0 +1,164 @@
1
+ // `slidemind assets import | list` — the deck's image registry.
2
+ //
3
+ // `import` copies a file into the deck, identifies it by its own bytes and
4
+ // records it. That is ALL it does: it decides no fidelity, writes no plan entry
5
+ // and creates no obligation to place the picture anywhere. What an image is FOR
6
+ // is an `assetUses` record the agent writes into the plan, where the user
7
+ // reviews and approves it.
8
+ //
9
+ // It takes filesystem paths only. Acquiring bytes is somebody else's job —
10
+ // `slidemind download-result` for an engine result, the harness's attachment
11
+ // access for something the user pasted into chat, the user's own download for
12
+ // anything else. A command that fetched URLs would be a second, unreviewed way
13
+ // for content to enter a deck.
14
+
15
+ import fs from "node:fs";
16
+ import path from "node:path";
17
+
18
+ import { deckSlug, fail, resolveDeckArg, resolveDeckRoot } from "./common.mjs";
19
+ import { formatSize } from "../bridge/image-dimensions.mjs";
20
+ import { importImageAsset, readImageAssetManifest } from "../lib/image-assets.mjs";
21
+
22
+ const USAGE =
23
+ "usage: slidemind assets import <deck> <file> [--json]\n" +
24
+ " slidemind assets list <deck> [--json]";
25
+
26
+ const emit = (data) => process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
27
+
28
+ const URL_ARG = /^[a-z][a-z0-9+.-]*:\/\//i;
29
+
30
+ function parseArgs(rest, sub) {
31
+ const args = { positional: [], json: false };
32
+ for (const a of rest) {
33
+ if (a === "--json") args.json = true;
34
+ else if (a.startsWith("-")) fail(`unknown flag: ${a}\n${USAGE}`);
35
+ else args.positional.push(a);
36
+ }
37
+ if (!args.positional.length) fail(`slidemind assets ${sub} needs a deck.\n${USAGE}`);
38
+ return args;
39
+ }
40
+
41
+ // The deck a user named, resolved to its workspace. A deck that is not there
42
+ // yet is the one mistake worth naming precisely — `assets import` is often the
43
+ // first thing an agent runs in a session.
44
+ function deckFor(name) {
45
+ const root = resolveDeckRoot(name);
46
+ if (!fs.existsSync(root)) {
47
+ fail(`no deck at ${root} — create it first with \`slidemind new ${path.basename(root)}\``);
48
+ }
49
+ return resolveDeckArg(name);
50
+ }
51
+
52
+ function failTyped(err, asJson) {
53
+ if (!err?.code) throw err;
54
+ if (asJson) {
55
+ emit({ ok: false, error: err.code, message: err.message, action: err.issue?.action ?? null });
56
+ process.exit(1);
57
+ }
58
+ fail(`${err.code} — ${err.message}${err.issue?.action ? `\n${err.issue.action}` : ""}`);
59
+ }
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // import
63
+ // ---------------------------------------------------------------------------
64
+
65
+ async function cmdImport(rest) {
66
+ const args = parseArgs(rest, "import");
67
+ const [deckArg, source, ...extra] = args.positional;
68
+ if (!source) fail(`slidemind assets import needs a file to import.\n${USAGE}`);
69
+ if (extra.length) fail(`unexpected argument: ${extra[0]}\n${USAGE}`);
70
+ if (URL_ARG.test(source)) {
71
+ fail(
72
+ `assets import takes a file path, not a URL. Download the image first ` +
73
+ `(\`slidemind download-result\` for an engine result), then import the file.`,
74
+ );
75
+ }
76
+ const deckDir = deckFor(deckArg);
77
+
78
+ let result;
79
+ try {
80
+ result = await importImageAsset(deckDir, source, { provenance: { kind: "local" } });
81
+ } catch (err) {
82
+ failTyped(err, args.json);
83
+ }
84
+ const { asset, reused, plan } = result;
85
+
86
+ if (args.json) {
87
+ // Exactly the contract's import result — nothing else is promised here.
88
+ emit({
89
+ assetId: asset.assetId,
90
+ sha256: asset.sha256,
91
+ mimeType: asset.mimeType,
92
+ width: asset.width,
93
+ height: asset.height,
94
+ byteLength: asset.byteLength,
95
+ workspacePath: asset.workspacePath,
96
+ reused,
97
+ });
98
+ } else {
99
+ process.stdout.write(
100
+ `${reused ? "Already imported" : "Imported"} ${asset.displayName} as ${asset.assetId}\n` +
101
+ ` ${asset.workspacePath} ${asset.mimeType} ${formatSize(asset) ?? "size unknown"} ${asset.byteLength} bytes\n` +
102
+ (reused ? " Same bytes as an existing asset — its identity is kept.\n" : ""),
103
+ );
104
+ }
105
+ // Rendering facts, never on stdout: a --json payload stays exactly the
106
+ // contract's shape.
107
+ for (const note of plan.notes) process.stderr.write(`note: ${note}\n`);
108
+ if (!args.json) {
109
+ process.stdout.write(
110
+ `Importing records the bytes only. Give it a use in the plan (assetUses: original | ` +
111
+ `reconstruct | reference) — that is what makes it appear on a slide.\n`,
112
+ );
113
+ }
114
+ }
115
+
116
+ // ---------------------------------------------------------------------------
117
+ // list
118
+ // ---------------------------------------------------------------------------
119
+
120
+ async function cmdList(rest) {
121
+ const args = parseArgs(rest, "list");
122
+ const [deckArg, ...extra] = args.positional;
123
+ if (extra.length) fail(`unexpected argument: ${extra[0]}\n${USAGE}`);
124
+ const deckDir = deckFor(deckArg);
125
+
126
+ let manifest;
127
+ try {
128
+ manifest = readImageAssetManifest(deckDir);
129
+ } catch (err) {
130
+ failTyped(err, args.json);
131
+ }
132
+
133
+ if (args.json) {
134
+ emit({ assets: manifest.assets });
135
+ return;
136
+ }
137
+ if (!manifest.assets.length) {
138
+ process.stdout.write(
139
+ `No image assets in ${deckSlug(deckDir)}. Import one with ` +
140
+ `\`slidemind assets import ${deckSlug(deckDir)} <file>\`.\n`,
141
+ );
142
+ return;
143
+ }
144
+ const lines = manifest.assets.map(
145
+ (a) =>
146
+ ` ${a.assetId} ${a.mimeType} ${formatSize(a) ?? "?"} ${a.byteLength} bytes ` +
147
+ `${a.provenance.kind}${a.provenance.sourceId ? ` ${a.provenance.sourceId}` : ""} ${a.displayName}\n` +
148
+ ` ${a.workspacePath} sha256 ${a.sha256}`,
149
+ );
150
+ process.stdout.write(
151
+ `${manifest.assets.length} image asset(s) in ${deckSlug(deckDir)}:\n${lines.join("\n")}\n`,
152
+ );
153
+ }
154
+
155
+ // ---------------------------------------------------------------------------
156
+
157
+ const SUBCOMMANDS = { import: cmdImport, list: cmdList };
158
+
159
+ export async function cmdAssets(rest) {
160
+ const [sub, ...args] = rest;
161
+ const run = SUBCOMMANDS[sub];
162
+ if (!run) fail(sub ? `unknown assets subcommand: ${sub}\n${USAGE}` : USAGE);
163
+ await run(args);
164
+ }
@@ -3,9 +3,19 @@
3
3
 
4
4
  import { spawn } from "node:child_process";
5
5
  import fs from "node:fs";
6
+ import os from "node:os";
6
7
  import path from "node:path";
7
8
  import { fileURLToPath, pathToFileURL } from "node:url";
8
9
 
10
+ import {
11
+ deckRoot,
12
+ deckSlug,
13
+ resolveWorkspace,
14
+ statePath,
15
+ } from "../lib/deck-paths.mjs";
16
+ import { journalIncomplete } from "../lib/revision.mjs";
17
+ import { addDeck, addRoot, listRoots, migrateBestEffort, registryPath } from "../workspace/registry.mjs";
18
+
9
19
  const __filename = fileURLToPath(import.meta.url);
10
20
  const __dirname = path.dirname(__filename);
11
21
  export const PACKAGE_ROOT = path.resolve(__dirname, "..", "..");
@@ -29,9 +39,10 @@ export function fail(msg, code = 1) {
29
39
  // The two human checkmarks the CLI enforces itself, so no harness timeout,
30
40
  // context loss, or agent rationalisation can walk past a gate: nothing after
31
41
  // the plan gate (concepts, build, view-draft, review) runs on an unapproved or
32
- // since-edited plan, and nothing ships (push, build-pptx) from a preview the
33
- // user has not approved exactly as it stands. Both fast-fail before any
34
- // bridge call with the command that clears them.
42
+ // since-edited plan, and nothing ships (push, build-pptx) from a deck whose
43
+ // preview the user never approved. That second one is one-time per deck: the
44
+ // approval builds the deck, and later changes go straight into the .pptx.
45
+ // Both fast-fail before any bridge call with the command that clears them.
35
46
  export async function requirePlanApproved(deckDir) {
36
47
  const plan = await import(pathToFileURL(path.join(BUILDERS_DIR, "plan.mjs")).href);
37
48
  const why = plan.requirePlanApproved(deckDir);
@@ -44,14 +55,37 @@ export async function requireReviewApproved(deckDir) {
44
55
  if (why) fail(why);
45
56
  }
46
57
 
58
+ // The third precondition on shipping, and the only one that is not a human
59
+ // checkmark: a slide under an open edit scope may not be replaced while what it
60
+ // promised to preserve is unmet or simply unproven. Drafts report those by
61
+ // element id (`[preserve:warn]`); this is where they refuse.
62
+ export async function requireSlidesPreserved(deckDir) {
63
+ const check = await import(pathToFileURL(path.join(BUILDERS_DIR, "draft-check.mjs")).href);
64
+ const { blocker, note } = await check.preservationBlocker(deckDir);
65
+ if (blocker) fail(blocker);
66
+ // Shipped, but not certified: an obligation only the final package can prove
67
+ // is a stated limitation, never a silent pass.
68
+ if (note) process.stderr.write(` note: ${note}\n`);
69
+ }
70
+
71
+ // The fourth: nothing reaches the user that `view-draft` has not checked. Every
72
+ // slide this delivery renders (`files`; default every authored slide) needs a
73
+ // check of its current bytes with no defect standing — the review is one-time
74
+ // per deck, so this is what holds an edit made after it.
75
+ export async function requireSlidesDraftChecked(deckDir, { files = null } = {}) {
76
+ const check = await import(pathToFileURL(path.join(BUILDERS_DIR, "draft-check.mjs")).href);
77
+ const why = check.draftCheckBlocker(deckDir, { files });
78
+ if (why) fail(`${deckSlug(deckDir)} cannot be delivered yet — ${why}`);
79
+ }
80
+
47
81
  // A render that stalls on "Multiple PowerPoint windows are open" is, for an
48
82
  // UNBOUND deck, really a binding problem: bound, the deck renders pane-free
49
83
  // and never asks. Name that fix first — the window pick is the workaround.
50
84
  export function withUnboundHint(deckDir, err) {
51
85
  const msg = err?.message ?? String(err);
52
86
  if (!/Multiple PowerPoint windows are open/.test(msg)) return msg;
53
- if (fs.existsSync(path.join(deckDir, ".bundle-ref.json"))) return msg;
54
- const slug = path.basename(deckDir);
87
+ if (fs.existsSync(statePath(deckDir, "bundle-ref.json"))) return msg;
88
+ const slug = deckSlug(deckDir);
55
89
  return (
56
90
  `deck ${slug} is not bound to a corporate design, so the render has to go through an open window. ` +
57
91
  `Bind it once and no window is needed: \`slidemind ready ${slug}\` (or \`--brand <presFP>\` to name the design). ` +
@@ -75,9 +109,9 @@ export function resolveDecksDir() {
75
109
  return path.resolve(process.cwd(), "decks");
76
110
  }
77
111
 
78
- export function resolveDeckArg(arg) {
79
- // A user can pass either "decks/foo" (relative to CWD), "/abs/path/foo",
80
- // or just "foo" — in which case we look it up under the resolved decks dir.
112
+ // The deck ROOT a user named — "decks/foo" (relative to CWD), "/abs/path/foo",
113
+ // or just "foo", which is looked up under the resolved decks dir.
114
+ export function resolveDeckRoot(arg) {
81
115
  if (!arg) return null;
82
116
  if (path.isAbsolute(arg)) return arg;
83
117
  if (fs.existsSync(path.resolve(process.cwd(), arg))) {
@@ -87,16 +121,140 @@ export function resolveDeckArg(arg) {
87
121
  return path.join(decksDir, arg);
88
122
  }
89
123
 
124
+ // What every command works on: the deck's WORKSPACE. `<deck>/.slidemind` when
125
+ // it exists, the deck directory itself otherwise — see src/lib/deck-paths.mjs.
126
+ // The resolved path is announced once (stderr, so --json payloads stay clean)
127
+ // whenever it is not the directory the user typed: `.slidemind/` is hidden in
128
+ // Finder and in most editors, so a silent redirect is a path nobody can find.
129
+ const announced = new Set();
130
+ export function resolveDeckArg(arg, { allowUnfinished = false } = {}) {
131
+ const root = resolveDeckRoot(arg);
132
+ if (!root) return root;
133
+ // A flat pre-workspace deck moves into `.slidemind/` before anything resolves
134
+ // against it, so this command and the library see one layout.
135
+ if (workspaceEnabled()) migrateBestEffort(root);
136
+ const workspace = resolveWorkspace(root);
137
+ // An interrupted adopt or rebase leaves half a generation on disk, and a build
138
+ // that silently ships it. The journal is the only record that the operation was
139
+ // in flight, so every command refuses until it is finished — except the ones
140
+ // that finish it.
141
+ if (!allowUnfinished) {
142
+ const unfinished = journalIncomplete(workspace);
143
+ if (unfinished) {
144
+ fail(
145
+ `deck ${deckSlug(workspace)} has an unfinished ${unfinished.op} (started ${unfinished.startedAt}) — ` +
146
+ `nothing on disk is a complete generation yet. Finish it first: ` +
147
+ (unfinished.op === "rebase"
148
+ ? `\`slidemind rebase ${arg} --incoming ${unfinished.detail?.revisionId ?? "<revisionId>"}\``
149
+ : `re-run the \`slidemind ${unfinished.op}\` that was interrupted`),
150
+ );
151
+ }
152
+ }
153
+ if (workspace !== root && !announced.has(workspace)) {
154
+ announced.add(workspace);
155
+ process.stderr.write(`slidemind: workspace ${workspace}\n`);
156
+ }
157
+ registerDeckHook(workspace);
158
+ return workspace;
159
+ }
160
+
161
+ // The one place every deck command feeds the persistent workspace library
162
+ // (src/workspace/registry.mjs): a deck resolved here, adopted, or created is
163
+ // listed the next time the user opens `slidemind workspace`. Off until the
164
+ // user has opted in — SLIDEMIND_WORKSPACE=1 or a registry already on disk —
165
+ // so nothing changes for people who never launched it. SLIDEMIND_WORKSPACE=0
166
+ // opts out even with a registry (headless replays, a user who wants the
167
+ // standalone review pages back). Best effort: a deck
168
+ // that is not yet a deck (open creates `.slidemind/` first) is skipped
169
+ // silently, a registry that cannot be written warns on stderr, and neither
170
+ // ever fails the command. Scratch decks — a deck nested inside another
171
+ // workspace, or one under the OS temp dir — are registered hidden so the
172
+ // library never shows a designer's throwaway or the pack's reference adopt.
173
+ const registered = new Set();
174
+ export function workspaceEnabled() {
175
+ if (process.env.SLIDEMIND_WORKSPACE === "0") return false;
176
+ return process.env.SLIDEMIND_WORKSPACE === "1" || fs.existsSync(registryPath());
177
+ }
178
+ export function registerDeckHook(deckDir, { hidden = null } = {}) {
179
+ if (!deckDir || registered.has(deckDir)) return null;
180
+ if (!workspaceEnabled()) return null;
181
+ registered.add(deckDir);
182
+ try {
183
+ const entry = addDeck(deckDir, { hidden: hidden ?? isScratchDeck(deckDir) });
184
+ if (!entry.hidden) rememberDecksRoot(entry.path);
185
+ return entry;
186
+ } catch (err) {
187
+ if (err?.code === "not_a_deck" || err?.code === "missing_path") return null;
188
+ // An agent's sandbox may not write the library: expected, and harmless — the
189
+ // workspace (outside the sandbox) registers the deck when a question is asked.
190
+ // A line about it only reads to the agent as a failure.
191
+ if (["EPERM", "EACCES", "EROFS"].includes(err?.code) && (process.env.CODEX_SANDBOX || process.env.CODEX_SANDBOX_NETWORK_DISABLED)) return null;
192
+ process.stderr.write(`slidemind: workspace registry skipped ${deckDir}: ${err.message}\n`);
193
+ return null;
194
+ }
195
+ }
196
+
197
+ // A deck under the configured decks dir that no remembered root covers makes
198
+ // that decks dir a root, so the next CLI version's auto-discovery finds its
199
+ // siblings too. Only the resolver's decks dir qualifies — never $HOME, never an
200
+ // arbitrary parent.
201
+ function rememberDecksRoot(deckReal) {
202
+ try {
203
+ const decksDir = fs.realpathSync(resolveDecksDir());
204
+ if (path.dirname(deckReal) !== decksDir || decksDir === fs.realpathSync(os.homedir())) return;
205
+ if (listRoots().some((r) => deckReal === r || deckReal.startsWith(r + path.sep))) return;
206
+ addRoot(decksDir);
207
+ } catch {
208
+ // best effort, like the registration itself
209
+ }
210
+ }
211
+
212
+ function isScratchDeck(deckDir) {
213
+ const root = path.resolve(deckRoot(deckDir));
214
+ if (path.dirname(root).split(path.sep).includes(".slidemind")) return true;
215
+ const tmp = os.tmpdir();
216
+ let real = tmp;
217
+ try {
218
+ real = fs.realpathSync(tmp);
219
+ } catch {}
220
+ return root.startsWith(tmp + path.sep) || root.startsWith(real + path.sep);
221
+ }
222
+
223
+ export { deckRoot, deckSlug, resolveWorkspace, statePath };
224
+
225
+ // A harness that stops a command signals the pid it started — this one. The
226
+ // script must stop with it: an orphaned gate keeps waiting (the page says an
227
+ // agent is listening while nobody is) and a standalone gate keeps its port.
228
+ const FORWARDED_SIGNALS = ["SIGTERM", "SIGINT", "SIGHUP"];
229
+
90
230
  export function runNode(script, args) {
91
231
  return new Promise((resolve, reject) => {
92
232
  const child = spawn(process.execPath, [script, ...args], {
93
233
  stdio: "inherit",
94
234
  env: process.env,
95
235
  });
96
- child.on("error", reject);
97
- child.on("exit", (code) => {
236
+ const forward = (sig) => {
237
+ try {
238
+ child.kill(sig);
239
+ } catch {
240
+ // already gone
241
+ }
242
+ };
243
+ for (const sig of FORWARDED_SIGNALS) process.on(sig, forward);
244
+ const release = () => {
245
+ for (const sig of FORWARDED_SIGNALS) process.off(sig, forward);
246
+ };
247
+ child.on("error", (err) => {
248
+ release();
249
+ reject(err);
250
+ });
251
+ child.on("exit", (code, signal) => {
252
+ release();
253
+ // stopped by a signal: end this process the same way, as if it had no child
254
+ if (signal) return process.kill(process.pid, signal);
98
255
  if (code === 0) resolve();
99
- else reject(new Error(`${path.basename(script)} exited with code ${code}`));
256
+ // the script's own code is the command's (a gate's BLOCKED is 2, not 1)
257
+ else reject(Object.assign(new Error(`${path.basename(script)} exited with code ${code}`), { exitCode: code }));
100
258
  });
101
259
  });
102
260
  }
@@ -159,7 +317,10 @@ export async function freshenDeckBundle(deckDir) {
159
317
  );
160
318
  const sync = await bundle.refreshDeckBundle(deckDir);
161
319
  if (sync?.changed) {
162
- process.stdout.write(
320
+ // stderr: every command that refreshes the bundle (`view-draft`,
321
+ // `build-pptx`, `concepts`, `push`) also has a `--json` mode, and a bundle
322
+ // that happened to move mid-session must not land in front of the payload.
323
+ process.stderr.write(
163
324
  ` design bundle updated (${sync.result.downloaded} file(s) changed) — building against the current corporate design\n`,
164
325
  );
165
326
  }
@@ -0,0 +1,76 @@
1
+ // What `concepts` and `transcribe` say when the paired add-in cannot read
2
+ // mockups into concept specs (it does not advertise `conceptSpecs`).
3
+ //
4
+ // The mockups are still generated, so nothing looks broken — and that was the
5
+ // failure: the only signal used to be a `specsSkipped` string inside the
6
+ // `--json` payload. In the 2026-10-06 proposal sim the prod add-in predated
7
+ // the spec reader, `transcribe` had nothing to work from, and the designer
8
+ // agent hand-built two slides that looked nothing like their mockups without
9
+ // anything on screen saying why. So both commands now name the add-in that
10
+ // said no, say what it means for `transcribe`, and use the same words.
11
+
12
+ export const SPECS_UNSUPPORTED_CODE = "concept_specs_unsupported";
13
+ /** The `specsSkipped` reason `concepts` has always printed for this case. */
14
+ export const SPECS_UNSUPPORTED = "this add-in does not extract concept specs";
15
+
16
+ const slideList = (slides) =>
17
+ slides.length === 1 ? `slide ${slides[0]}` : `slides ${slides.join(", ")}`;
18
+
19
+ /** "the 'prod' add-in (https://…)" — named even with one backend paired. */
20
+ function addin({ env, backendURL }) {
21
+ if (!backendURL) return "the paired add-in";
22
+ let host = null;
23
+ try {
24
+ host = new URL(backendURL).hostname;
25
+ } catch {}
26
+ // An unrecognised host derives no env name, only the host itself.
27
+ return env && env !== host ? `the '${env}' add-in (${backendURL})` : `the add-in at ${backendURL}`;
28
+ }
29
+
30
+ function headline(support) {
31
+ return support.status === "unknown"
32
+ ? `could not confirm that ${addin(support)} extracts concept specs — it did not answer the capability probe and returned none`
33
+ : `${addin(support)} does not extract concept specs (it does not advertise \`conceptSpecs\`: its release predates the spec reader)`;
34
+ }
35
+
36
+ const REBUILD =
37
+ "Rebuild each of them from its mockup PNG by hand (workflow/create.md): count the mockup's cards, connectors, " +
38
+ "icons, bands and numbers first and build every one; or pair an add-in that reads specs (`slidemind whoami` " +
39
+ "lists the paired environments).";
40
+
41
+ /** `concepts --json` → `specsUnsupported`: null unless the add-in refused. */
42
+ export function specsUnsupportedPayload(support, deck) {
43
+ if (!support) return null;
44
+ const slides = support.slides ?? [];
45
+ return {
46
+ code: SPECS_UNSUPPORTED_CODE,
47
+ env: support.env ?? null,
48
+ backendURL: support.backendURL ?? null,
49
+ confirmed: support.status !== "unknown",
50
+ slides,
51
+ transcribe: "refuses",
52
+ message: `${headline(support)}. ${slides.length ? `${slideList(slides)} ${slides.length === 1 ? "has" : "have"} a mockup and no spec, so \`slidemind transcribe ${deck}\` cannot build ${slides.length === 1 ? "it" : "them"}.` : ""}`.trim(),
53
+ };
54
+ }
55
+
56
+ /** The stderr block `concepts` prints — always, `--json` included. */
57
+ export function specsUnsupportedWarning(support, deck) {
58
+ const slides = support.slides ?? [];
59
+ const tag = "slidemind: [specs:warning]";
60
+ return [
61
+ `${tag} NO CONCEPT SPECS — ${headline(support)}.`,
62
+ `${tag} ${slides.length ? `${slideList(slides)} ${slides.length === 1 ? "has" : "have"} a mockup but no spec` : "these mockups have no spec"}, so \`slidemind transcribe ${deck}\` has nothing to build ${slides.length === 1 ? "it" : "them"} from and refuses; re-running \`concepts\` against this add-in will not change that.`,
63
+ `${tag} ${REBUILD}`,
64
+ ];
65
+ }
66
+
67
+ /** `transcribe`'s refusal: the slides it cannot build, and why no re-run helps. */
68
+ export function transcribeSpecsRefusal(support, { deck, slides }) {
69
+ const which = slides.length
70
+ ? `${slideList(slides)} ${slides.length === 1 ? "has" : "have"} no concept spec`
71
+ : "this deck has no concept spec";
72
+ return (
73
+ `[specs:error] ${headline(support)}, and ${which} — there is nothing to transcribe, and re-running ` +
74
+ `\`slidemind concepts ${deck}\` against this add-in will not change that. ${REBUILD} Nothing was written.`
75
+ );
76
+ }