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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/AGENTS.md +592 -16
- package/LICENSE +4 -0
- package/README.md +203 -8
- package/THIRD-PARTY-NOTICES.md +201 -0
- package/bin/slidemind.mjs +50 -2
- package/package.json +5 -3
- package/skills/create-slides/SKILL.md +484 -886
- package/skills/search/SKILL.md +46 -23
- package/src/bridge/build-and-push.mjs +548 -73
- package/src/bridge/bundle.mjs +100 -31
- package/src/bridge/capture.mjs +14 -17
- package/src/bridge/client/brands.mjs +157 -18
- package/src/bridge/client/engine-routing.mjs +48 -13
- package/src/bridge/client/engine-search.mjs +24 -1
- package/src/bridge/client/render.mjs +27 -1
- package/src/bridge/client/transport.mjs +499 -23
- package/src/bridge/client.mjs +10 -0
- package/src/bridge/config.mjs +67 -6
- package/src/bridge/engine-download.mjs +0 -10
- package/src/bridge/graph-auth.mjs +55 -3
- package/src/bridge/image-intent-envelope.mjs +239 -0
- package/src/bridge/package-preview.mjs +67 -0
- package/src/bridge/sharepoint.mjs +914 -0
- package/src/builders/asset-fetch.mjs +203 -0
- package/src/builders/block-dividers.mjs +83 -0
- package/src/builders/build.mjs +171 -87
- package/src/builders/card-height.mjs +294 -0
- package/src/builders/chart-hole.mjs +183 -0
- package/src/builders/chart-jsx.mjs +410 -0
- package/src/builders/chart-labels.mjs +326 -0
- package/src/builders/concept-geometry.mjs +370 -0
- package/src/builders/concept-intent.mjs +200 -0
- package/src/builders/concept-layout.mjs +79 -0
- package/src/builders/concept-lines.mjs +509 -0
- package/src/builders/concept-lists.mjs +270 -0
- package/src/builders/concept-marks.mjs +84 -0
- package/src/builders/concept-relationships.mjs +12 -0
- package/src/builders/concept-spec.mjs +645 -0
- package/src/builders/concept-text.mjs +140 -0
- package/src/builders/concept-waves.mjs +57 -0
- package/src/builders/copy-fit.mjs +139 -0
- package/src/builders/design-tokens.mjs +347 -0
- package/src/builders/draft-check.mjs +552 -340
- package/src/builders/draft-lines.mjs +369 -0
- package/src/builders/edit-route.mjs +29 -0
- package/src/builders/figure-sketch.mjs +375 -0
- package/src/builders/fold-sets.mjs +639 -0
- package/src/builders/gate-server.mjs +9 -4
- package/src/builders/gate-session.js +273 -12
- package/src/builders/gate-shell.js +1494 -0
- package/src/builders/image-intent-check.mjs +308 -0
- package/src/builders/image-use-reconcile.mjs +107 -0
- package/src/builders/kit-helpers.mjs +121 -0
- package/src/builders/layout-names.mjs +32 -0
- package/src/builders/legacy-reference-uses.mjs +179 -0
- package/src/builders/line-fit.mjs +224 -0
- package/src/builders/line-native.mjs +895 -0
- package/src/builders/line-relations.mjs +230 -0
- package/src/builders/lint-fix.mjs +117 -9
- package/src/builders/lint.mjs +25 -20
- package/src/builders/list-jsx.mjs +88 -0
- package/src/builders/placement.mjs +133 -0
- package/src/builders/plan-checks.mjs +135 -0
- package/src/builders/plan-comments.mjs +19 -0
- package/src/builders/plan-handlers.mjs +95 -0
- package/src/builders/plan-markdown-edit.mjs +3 -3
- package/src/builders/plan-overlay.js +1530 -1608
- package/src/builders/plan.mjs +689 -186
- package/src/builders/preset-definitions.json +1 -0
- package/src/builders/preset-eval.mjs +244 -0
- package/src/builders/preview.html +51 -120
- package/src/builders/red-thread-backbone.mjs +135 -36
- package/src/builders/repair.mjs +205 -0
- package/src/builders/repeat-groups.mjs +540 -0
- package/src/builders/resolve-skill.mjs +5 -4
- package/src/builders/review-overlay.js +296 -293
- package/src/builders/review.mjs +305 -84
- package/src/builders/rule-shift.mjs +96 -0
- package/src/builders/seat.mjs +354 -0
- package/src/builders/settle-driver.mjs +236 -0
- package/src/builders/settle.mjs +1448 -0
- package/src/builders/slide-prep.mjs +67 -0
- package/src/builders/spec-assets.mjs +236 -0
- package/src/builders/spec-match.mjs +56 -0
- package/src/builders/stack.mjs +413 -0
- package/src/builders/storyboard-markdown.mjs +70 -27
- package/src/builders/storyboard-schema.mjs +54 -114
- package/src/builders/text-blocks.mjs +212 -0
- package/src/builders/text-card-fit.mjs +552 -0
- package/src/builders/text-stack.mjs +276 -0
- package/src/builders/transcribe.mjs +1069 -0
- package/src/cli/assets.mjs +164 -0
- package/src/cli/common.mjs +173 -12
- package/src/cli/concept-spec-support.mjs +76 -0
- package/src/cli/concepts.mjs +1002 -103
- package/src/cli/deck.mjs +401 -65
- package/src/cli/diagnose.mjs +4 -2
- package/src/cli/engine-helpers.mjs +17 -10
- package/src/cli/engine-results.mjs +76 -3
- package/src/cli/help.mjs +232 -28
- package/src/cli/install.mjs +9 -0
- package/src/cli/maintenance.mjs +104 -0
- package/src/cli/read.mjs +2 -1
- package/src/cli/ready.mjs +6 -3
- package/src/cli/record.mjs +522 -0
- package/src/cli/remote.mjs +818 -0
- package/src/cli/revision.mjs +686 -0
- package/src/cli/search.mjs +30 -11
- package/src/cli/session.mjs +56 -13
- package/src/cli/setup.mjs +46 -2
- package/src/cli/status.mjs +142 -33
- package/src/cli/tasks.mjs +246 -0
- package/src/cli/transcribe.mjs +854 -0
- package/src/cli/workspace-ui.mjs +260 -0
- package/src/cli/workspace.mjs +212 -34
- package/src/install-channel.mjs +64 -7
- package/src/lib/adopt.mjs +346 -25
- package/src/lib/concept-spec-binding.mjs +96 -0
- package/src/lib/conversions.mjs +70 -6
- package/src/lib/convert-folded.mjs +107 -0
- package/src/lib/deck-paths.mjs +418 -0
- package/src/lib/durable-ids.mjs +47 -0
- package/src/lib/edit-operations.mjs +316 -0
- package/src/lib/edit-state.mjs +1244 -0
- package/src/lib/edit-transitions.mjs +120 -0
- package/src/lib/extensions.mjs +501 -0
- package/src/lib/image-assets.mjs +465 -0
- package/src/lib/image-derivatives.mjs +234 -0
- package/src/lib/image-intent-fingerprint.mjs +70 -0
- package/src/lib/image-intent-tags.mjs +176 -0
- package/src/lib/image-intent.mjs +802 -0
- package/src/lib/image-use-overrides.mjs +279 -0
- package/src/lib/inline-components.mjs +613 -0
- package/src/lib/lint-converted.mjs +68 -4
- package/src/lib/merge-json.mjs +163 -0
- package/src/lib/overwrite-guard.mjs +3 -3
- package/src/lib/presentation-context.mjs +531 -0
- package/src/lib/preservation-tags.mjs +157 -0
- package/src/lib/preservation-verdict.mjs +1342 -0
- package/src/lib/publish.mjs +1557 -0
- package/src/lib/rebase.mjs +945 -0
- package/src/lib/reconcile-ack.mjs +4 -6
- package/src/lib/reconcile.mjs +62 -24
- package/src/lib/record-client.mjs +175 -0
- package/src/lib/record-state.mjs +151 -0
- package/src/lib/remote.mjs +661 -0
- package/src/lib/revision.mjs +1210 -0
- package/src/lib/slide-guids.mjs +4 -6
- package/src/lib/slide-preservation.mjs +423 -0
- package/src/lib/slide-selection.mjs +9 -0
- package/src/lib/workspace-transaction.mjs +117 -0
- package/src/pptx/content-digest.mjs +334 -0
- package/src/pptx/deck-assemble.mjs +1278 -0
- package/src/pptx/fill/engine.mjs +2 -3
- package/src/pptx/fill/freeform.mjs +6 -3
- package/src/pptx/ooxml.mjs +20 -0
- package/src/pptx/package-evidence.mjs +383 -0
- package/src/pptx/tags.mjs +163 -11
- package/src/update-check.mjs +22 -4
- package/src/workspace/adapters.mjs +222 -0
- package/src/workspace/auth.mjs +256 -0
- package/src/workspace/autostart.mjs +204 -0
- package/src/workspace/daemon.mjs +360 -0
- package/src/workspace/discovery.mjs +180 -0
- package/src/workspace/edit.mjs +167 -0
- package/src/workspace/events.mjs +52 -0
- package/src/workspace/gate-client.mjs +290 -0
- package/src/workspace/handback.mjs +73 -0
- package/src/workspace/lifecycle.mjs +635 -0
- package/src/workspace/operations.mjs +200 -0
- package/src/workspace/pick-folder.mjs +152 -0
- package/src/workspace/registry.mjs +359 -0
- package/src/workspace/remote-links.mjs +397 -0
- package/src/workspace/remote-status.mjs +359 -0
- package/src/workspace/routes.mjs +1331 -0
- package/src/workspace/server.mjs +159 -0
- package/src/workspace/sync.mjs +1119 -0
- package/src/workspace/task-actions.mjs +215 -0
- package/src/workspace/tasks.mjs +507 -0
- package/src/workspace/ui/app.js +1886 -0
- package/src/workspace/ui/index.html +141 -0
- package/src/workspace/ui/plan-bridge.js +120 -0
- package/src/workspace/ui/sharepoint.svg +59 -0
- package/src/workspace/ui/styles.css +423 -0
- package/src/builders/visual-families.mjs +0 -46
package/src/lib/conversions.mjs
CHANGED
|
@@ -83,6 +83,28 @@ export function writeConversionRecord(
|
|
|
83
83
|
return full;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Drop one slide's record AND its preserve package. Used when a slide's local
|
|
88
|
+
* hydration is withdrawn — a rebase took it back to its package form, or an
|
|
89
|
+
* update replaced it — because a record left behind names a slide that is not
|
|
90
|
+
* there any more, which `conversionIntegrityErrors` correctly refuses to build.
|
|
91
|
+
* Returns the names it removed.
|
|
92
|
+
*/
|
|
93
|
+
export function removeConversionRecord(deckDir, slideFileName) {
|
|
94
|
+
// Callers name the slide FILE, which may already have been replaced by its
|
|
95
|
+
// native form — so both extensions are stripped, not just `.jsx`.
|
|
96
|
+
const stem = path.basename(String(slideFileName)).replace(/\.(jsx|pptx)$/i, "");
|
|
97
|
+
const dir = conversionsDir(deckDir);
|
|
98
|
+
const removed = [];
|
|
99
|
+
for (const name of [`${stem}.json`, `${stem}.slide.pptx`]) {
|
|
100
|
+
const file = path.join(dir, name);
|
|
101
|
+
if (!fs.existsSync(file)) continue;
|
|
102
|
+
fs.rmSync(file, { force: true });
|
|
103
|
+
removed.push(name);
|
|
104
|
+
}
|
|
105
|
+
return removed;
|
|
106
|
+
}
|
|
107
|
+
|
|
86
108
|
/**
|
|
87
109
|
* Every conversion record of a deck, keyed by slide file name, in the shape the
|
|
88
110
|
* render request carries: identity + evidence + the base64 preserve source.
|
|
@@ -90,8 +112,17 @@ export function writeConversionRecord(
|
|
|
90
112
|
* A record whose `slides/<stem>.jsx` no longer exists is skipped — the slide was
|
|
91
113
|
* deleted or renamed, and shipping its baseline would bind conversion identity
|
|
92
114
|
* to a slide that is no longer in the deck. Unreadable/corrupt records are
|
|
93
|
-
* skipped too
|
|
94
|
-
*
|
|
115
|
+
* skipped too — and a skipped record is INDISTINGUISHABLE here from one that
|
|
116
|
+
* was never written. Nobody warns about it either: every warning path
|
|
117
|
+
* (`conversionIntegrityErrors`, `conversionsMissingPreserveSource`) iterates
|
|
118
|
+
* this same map, so the slide they would name is already gone from it.
|
|
119
|
+
*
|
|
120
|
+
* That is fine for a renderer — the slide previews as ordinary jsx — and wrong
|
|
121
|
+
* for anything that REWRITES source or DELIVERS a deck, where absence reads as
|
|
122
|
+
* "not converted" and the answer is really "nobody could tell". Those callers
|
|
123
|
+
* ask {@link conversionProvenance} per slide instead, which distinguishes the
|
|
124
|
+
* two; `build-and-push` does, and refuses rather than shipping a slide whose
|
|
125
|
+
* preserved natives (SmartArt, OLE, an unmodeled chart) would silently be gone.
|
|
95
126
|
*
|
|
96
127
|
* `includeSource: false` skips reading the preserve packages — for callers that
|
|
97
128
|
* only need to know WHICH slides are converted (they can be megabytes).
|
|
@@ -108,6 +139,11 @@ export function loadConversions(deckDir, { includeSource = true } = {}) {
|
|
|
108
139
|
try {
|
|
109
140
|
record = JSON.parse(fs.readFileSync(path.join(dir, name), "utf8"));
|
|
110
141
|
} catch {
|
|
142
|
+
// Deliberate, and ONLY safe for the render path: a slide missing from
|
|
143
|
+
// this map renders as ordinary jsx. It is not safe for anything that
|
|
144
|
+
// decides whether a slide may be REWRITTEN, because absence here reads
|
|
145
|
+
// as "not converted" when the truth is "nobody could tell". Those
|
|
146
|
+
// callers ask `conversionProvenance` below, which does not skip.
|
|
111
147
|
continue;
|
|
112
148
|
}
|
|
113
149
|
const entry = {
|
|
@@ -135,6 +171,34 @@ export function loadConversions(deckDir, { includeSource = true } = {}) {
|
|
|
135
171
|
return out;
|
|
136
172
|
}
|
|
137
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Is ONE slide converted? Three answers, and the third is the whole point:
|
|
176
|
+
*
|
|
177
|
+
* the record a `conversions/<stem>.json` that parsed: converted, and this
|
|
178
|
+
* is what it says
|
|
179
|
+
* null looked, there is no record file: not converted
|
|
180
|
+
* undefined a record file IS there and could not be read, so the question
|
|
181
|
+
* is unanswered
|
|
182
|
+
*
|
|
183
|
+
* The third is what {@link loadConversions} cannot express — a map has no way
|
|
184
|
+
* to say "something was here and I could not read it", so a corrupt record
|
|
185
|
+
* arrives at its callers as absence. For a renderer that is a degraded slide;
|
|
186
|
+
* for anything that rewrites source it is the difference between refusing and
|
|
187
|
+
* quietly rewriting a converted slide whose pragma someone removed (E15). The
|
|
188
|
+
* three states line up with `validateSettleTarget`'s own contract
|
|
189
|
+
* (src/builders/settle.mjs): undefined refuses as `provenance_unknown`, a
|
|
190
|
+
* record refuses as `converted_provenance`, null proceeds.
|
|
191
|
+
*/
|
|
192
|
+
export function conversionProvenance(deckDir, slideFileName) {
|
|
193
|
+
const file = path.join(conversionsDir(deckDir), `${stemOf(slideFileName)}.json`);
|
|
194
|
+
if (!fs.existsSync(file)) return null;
|
|
195
|
+
try {
|
|
196
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
197
|
+
} catch {
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
138
202
|
/**
|
|
139
203
|
* Follow the deck's reconciled GUIDs: move every conversion record whose slide
|
|
140
204
|
* file was renamed (a reorder renames every slide — the NN- prefix IS the play
|
|
@@ -237,9 +301,9 @@ export function applyConversionGates(deckDir, conversions, gates) {
|
|
|
237
301
|
reasons,
|
|
238
302
|
note:
|
|
239
303
|
`${file}: this converted slide was NOT confirmed for replacement ` +
|
|
240
|
-
`(${gate.route}: ${reasons.join(", ")}) — it is in the rendered deck, but
|
|
241
|
-
`it over the original
|
|
242
|
-
`
|
|
304
|
+
`(${gate.route}: ${reasons.join(", ")}) — it is in the rendered deck, but nobody ` +
|
|
305
|
+
`has reviewed it over the original: replacing the original (\`push --replace\`) ` +
|
|
306
|
+
`waits for the user's go-ahead (\`--accept-conversions\`).`,
|
|
243
307
|
});
|
|
244
308
|
}
|
|
245
309
|
stampFirstReplace(deckDir, file, gate);
|
|
@@ -377,7 +441,7 @@ export function conversionIntegrityErrors(deckDir, conversions) {
|
|
|
377
441
|
errors.push(
|
|
378
442
|
`${file}: its conversion record still claims "slide": "${record.slide}" — the file was renamed ` +
|
|
379
443
|
`by hand. Rename it back, or re-run 'slidemind adopt' (a plan-driven reorder belongs to ` +
|
|
380
|
-
`'
|
|
444
|
+
`the pack's own renumber command, which moves the jsx, the brief, the record and the GUID together).`,
|
|
381
445
|
);
|
|
382
446
|
}
|
|
383
447
|
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Convert ONE folded slide of a deck — `slides/NN-*.pptx`, written by
|
|
2
|
+
// `slidemind add-slide` (or kept native by an adoption) — into editable jsx IN
|
|
3
|
+
// PLACE, when the user asks to change it (workflow/search.md: a reused slide
|
|
4
|
+
// stays native until then, and is then edited like any other deck slide).
|
|
5
|
+
//
|
|
6
|
+
// Same machinery as adoption: the add-in's convert-pptx route for a slide with
|
|
7
|
+
// no current embedded source, a verbatim restore for one whose source is proved
|
|
8
|
+
// current, and `adoptDeckFromPptx` to materialize either. The jsx keeps the
|
|
9
|
+
// .pptx's file stem, so play order does not move, and replaces it — the deck
|
|
10
|
+
// never carries the slide twice. A slide that cannot be converted leaves the
|
|
11
|
+
// deck untouched and says why.
|
|
12
|
+
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
|
|
16
|
+
import { adoptDeckFromPptx, convertUntaggedSlides, hydrationRoutes, readSourceDeck } from "./adopt.mjs";
|
|
17
|
+
import { stageWorkspace } from "./workspace-transaction.mjs";
|
|
18
|
+
|
|
19
|
+
function refuse(code, message) {
|
|
20
|
+
return Object.assign(new Error(message), { code });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Only a deck's own folded slide: any folder on disk can be called `slides`.
|
|
24
|
+
// The CLI asks this before it takes the deck's lock, so a refusal writes nothing.
|
|
25
|
+
export function notFoldedReason(pptxPath) {
|
|
26
|
+
const file = path.basename(pptxPath);
|
|
27
|
+
return path.basename(path.dirname(pptxPath)) === "slides" && /^\d{1,3}-.+\.pptx$/i.test(file)
|
|
28
|
+
? null
|
|
29
|
+
: `${file} is not a deck's folded slide (<deck>/slides/<NN-name>.pptx); nothing was changed.`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function convertFoldedSlide({ pptxPath, deckDir, brandId = null, note = () => {}, owner = null }) {
|
|
33
|
+
const file = path.basename(pptxPath);
|
|
34
|
+
const notFolded = notFoldedReason(pptxPath);
|
|
35
|
+
if (notFolded) throw refuse("not_folded", notFolded);
|
|
36
|
+
const stem = file.replace(/\.pptx$/i, "");
|
|
37
|
+
const source = readSourceDeck(pptxPath);
|
|
38
|
+
if (source.order.length !== 1) {
|
|
39
|
+
throw refuse("not_one_slide", `${file} holds ${source.order.length} slides; a folded slide file holds exactly one.`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const revisionLib = await import("./revision.mjs");
|
|
43
|
+
const revision = revisionLib.readRevision(deckDir);
|
|
44
|
+
const dirty = revision ? revisionLib.computeDirty(deckDir, revision) : null;
|
|
45
|
+
// In the accepted baseline: the conversion is hydration, not an edit, so the
|
|
46
|
+
// jsx joins the baseline in the .pptx's place. Added since: it stays an added
|
|
47
|
+
// slide, just editable. Edited since: converting would drop that edit.
|
|
48
|
+
const inBaseline = revision?.baseline?.inputs?.[`slides/${file}`] !== undefined;
|
|
49
|
+
if (inBaseline && dirty.dirtySlides.includes(file)) {
|
|
50
|
+
throw refuse("local_dirty", `${file} has unpublished changes; publish or reconcile them before converting it.`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const heard = [];
|
|
54
|
+
const [route] = hydrationRoutes(source, { slides: [1] });
|
|
55
|
+
const converted =
|
|
56
|
+
route === "convert"
|
|
57
|
+
? await convertUntaggedSlides({
|
|
58
|
+
pptxPath,
|
|
59
|
+
deckDir,
|
|
60
|
+
brandId,
|
|
61
|
+
source,
|
|
62
|
+
slides: [1],
|
|
63
|
+
note: (line) => {
|
|
64
|
+
heard.push(line);
|
|
65
|
+
note(line);
|
|
66
|
+
},
|
|
67
|
+
})
|
|
68
|
+
: null;
|
|
69
|
+
if (route === "convert" && !converted?.slides?.some((s) => Number(s?.index) === 1)) {
|
|
70
|
+
const why = converted?.skipped?.find((s) => Number(s?.index) === 1)?.reason ?? heard.at(-1) ?? "the add-in returned no jsx for it";
|
|
71
|
+
throw refuse("not_converted", `${file} was not converted (${why}); it stays native and nothing was changed.`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const tx = stageWorkspace(deckDir);
|
|
75
|
+
try {
|
|
76
|
+
const ws = tx.workspace;
|
|
77
|
+
fs.rmSync(path.join(ws, "slides", file), { force: true });
|
|
78
|
+
const result = adoptDeckFromPptx({
|
|
79
|
+
pptxPath,
|
|
80
|
+
deckDir: ws,
|
|
81
|
+
converted,
|
|
82
|
+
source,
|
|
83
|
+
slides: [1],
|
|
84
|
+
only: [1],
|
|
85
|
+
stems: new Map([[1, stem]]),
|
|
86
|
+
});
|
|
87
|
+
const jsx = `${stem}.jsx`;
|
|
88
|
+
if (inBaseline) {
|
|
89
|
+
revisionLib.writeRevision(ws, {
|
|
90
|
+
...revision,
|
|
91
|
+
base: {
|
|
92
|
+
...revision.base,
|
|
93
|
+
slides: (revision.base?.slides ?? []).map((s) => (s.file === file ? { ...s, file: jsx, hydrated: true } : s)),
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
revisionLib.noteHydration(ws, [jsx], [file]);
|
|
97
|
+
}
|
|
98
|
+
tx.activate({
|
|
99
|
+
checkpoint: () =>
|
|
100
|
+
revision &&
|
|
101
|
+
revisionLib.assertCheckpoint(deckDir, { owner, generation: revision.generation, digest: dirty.inputDigest }),
|
|
102
|
+
});
|
|
103
|
+
return { file: jsx, replaced: file, route, joinedBaseline: inBaseline, warnings: result.warnings, skipped: result.skipped };
|
|
104
|
+
} finally {
|
|
105
|
+
tx.dispose();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
// Where a deck keeps its two halves.
|
|
2
|
+
//
|
|
3
|
+
// decks/<slug>/ the DELIVERABLE root — only what the user asked for
|
|
4
|
+
// <slug>.pptx the built presentation
|
|
5
|
+
// plan.html the persisted plan page
|
|
6
|
+
// .slidemind/ the WORKSPACE — everything the CLI works with
|
|
7
|
+
// slides/ assets/ conversions/ the render contract (unchanged, and named
|
|
8
|
+
// by the add-in's bundle-shipped workflow docs)
|
|
9
|
+
// concepts/ plan.md authoring inputs
|
|
10
|
+
// state/*.json the eight sidecars
|
|
11
|
+
// preview/ presentation.html, fonts/, draft/, build/
|
|
12
|
+
//
|
|
13
|
+
// `.slidemind/` holds the deck contract that used to sit at the root, so the
|
|
14
|
+
// CLI needs ONE resolver rather than a path hunt: every command runs on the
|
|
15
|
+
// workspace, and a deck without `.slidemind/` (decks predating this layout, the
|
|
16
|
+
// designer's scratch deck) is its own workspace and keeps working unchanged.
|
|
17
|
+
|
|
18
|
+
import fs from "node:fs";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
|
|
21
|
+
export const WORKSPACE_DIRNAME = ".slidemind";
|
|
22
|
+
|
|
23
|
+
// `plan.json` / `<something>-plan.json` — a plan record the core does not own.
|
|
24
|
+
// Both the deck test and the page-ownership test key off it.
|
|
25
|
+
function planRecordIn(dir) {
|
|
26
|
+
try {
|
|
27
|
+
return fs.readdirSync(dir).find((n) => /^(.+-)?plan\.json$/i.test(n)) ?? null;
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isDir(p) {
|
|
34
|
+
try {
|
|
35
|
+
return fs.statSync(p).isDirectory();
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Does this directory stand on its own as a deck? An extension pack applies the
|
|
42
|
+
// same test, so both sides answer "is `.slidemind/scratch` a deck or part of its
|
|
43
|
+
// host?" identically. A plan record alone counts: a planned-but-never-authored
|
|
44
|
+
// deck has nothing else yet.
|
|
45
|
+
export function isDeckDir(dir) {
|
|
46
|
+
if (isDir(path.join(dir, WORKSPACE_DIRNAME))) return true;
|
|
47
|
+
if (isDir(path.join(dir, "slides")) || isDir(path.join(dir, "conversions"))) return true;
|
|
48
|
+
if (fs.existsSync(path.join(dir, "plan.md"))) return true;
|
|
49
|
+
return Boolean(planRecordIn(dir));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// layout generation
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
// The layout before `.slidemind/` existed kept everything at the deck root:
|
|
57
|
+
// slides/, assets/, concepts/, plan.md, the sidecars as dotfiles or bare JSON,
|
|
58
|
+
// `.draft-preview/` and `presentation.html`. This CLI no longer reads those —
|
|
59
|
+
// `resolveWorkspace` treats such a root as its own workspace and looks for the
|
|
60
|
+
// sidecars under `state/`, so an old deck silently loses its binding, its plan
|
|
61
|
+
// and review sign-offs and its reconcile ack. `migrateLegacyDeck` moves such a
|
|
62
|
+
// deck into the current layout; the workspace runs it on every registration.
|
|
63
|
+
export const LEGACY_MARKERS = [
|
|
64
|
+
".bundle-ref.json",
|
|
65
|
+
"plan-review.json",
|
|
66
|
+
"review.json",
|
|
67
|
+
"draft-check.json",
|
|
68
|
+
"reconcile-ack.json",
|
|
69
|
+
"slide-guids.json",
|
|
70
|
+
".draft-preview",
|
|
71
|
+
"presentation.html",
|
|
72
|
+
"asset-plan.md",
|
|
73
|
+
"plan.md",
|
|
74
|
+
"slides",
|
|
75
|
+
"assets",
|
|
76
|
+
"concepts",
|
|
77
|
+
"conversions",
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* `current` (a `.slidemind/` workspace), `legacy` (the pre-workspace flat
|
|
82
|
+
* layout: any LEGACY_MARKERS entry at the root) or `unknown` (no layout
|
|
83
|
+
* evidence — e.g. a plan-only record). One stat for a current deck.
|
|
84
|
+
*/
|
|
85
|
+
export function deckFormat(root) {
|
|
86
|
+
if (isDir(path.join(root, WORKSPACE_DIRNAME))) return "current";
|
|
87
|
+
if (LEGACY_MARKERS.some((n) => fs.existsSync(path.join(root, n)))) return "legacy";
|
|
88
|
+
return "unknown";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Where each root entry of the flat layout lives in a `.slidemind/` workspace.
|
|
92
|
+
// Every sign-off survives the move: the review hash is workspace-relative path +
|
|
93
|
+
// bytes (`slides/01-x.jsx` is the same string in both layouts), the plan hash is
|
|
94
|
+
// plan.md's bytes, the draft check hashes per slide file — nothing stores an
|
|
95
|
+
// absolute path. The two stylesheet copies are the bundle's, nothing reads them.
|
|
96
|
+
const LEGACY_MOVES = [
|
|
97
|
+
["slides", "slides"],
|
|
98
|
+
["assets", "assets"],
|
|
99
|
+
["concepts", "concepts"],
|
|
100
|
+
["conversions", "conversions"],
|
|
101
|
+
["plan.md", "plan.md"],
|
|
102
|
+
["asset-plan.md", "asset-plan.md"],
|
|
103
|
+
[".bundle-ref.json", "state/bundle-ref.json"],
|
|
104
|
+
["plan-review.json", "state/plan-review.json"],
|
|
105
|
+
["review.json", "state/review.json"],
|
|
106
|
+
["draft-check.json", "state/draft-check.json"],
|
|
107
|
+
["reconcile-ack.json", "state/reconcile-ack.json"],
|
|
108
|
+
["slide-guids.json", "state/slide-guids.json"],
|
|
109
|
+
["presentation.html", "preview/presentation.html"],
|
|
110
|
+
[".draft-preview", "preview/draft"],
|
|
111
|
+
[".draft-preview/draft-check.json", "state/draft-check.json"],
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
function mtime(p) {
|
|
115
|
+
try {
|
|
116
|
+
return fs.statSync(p).mtimeMs;
|
|
117
|
+
} catch {
|
|
118
|
+
return -1;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Move a flat pre-workspace deck into `<root>/.slidemind/`. Idempotent: a deck
|
|
124
|
+
* that already has a workspace, or carries no legacy marker, is left alone
|
|
125
|
+
* (returns null). A root `state/` — written by this CLI when it ran on the flat
|
|
126
|
+
* deck as its own workspace — merges into the new `state/`, newer file wins.
|
|
127
|
+
* Returns the workspace-relative paths that were written.
|
|
128
|
+
*/
|
|
129
|
+
export function migrateLegacyDeck(root, { bundleDirFor = null } = {}) {
|
|
130
|
+
// A workspace dir itself looks flat from the inside; never nest one in it.
|
|
131
|
+
if (path.basename(path.resolve(root)) === WORKSPACE_DIRNAME) return null;
|
|
132
|
+
if (deckFormat(root) !== "legacy") return null;
|
|
133
|
+
const ws = path.join(root, WORKSPACE_DIRNAME);
|
|
134
|
+
const moved = [];
|
|
135
|
+
const move = (from, to) => {
|
|
136
|
+
if (!fs.existsSync(from)) return;
|
|
137
|
+
if (to === null) return fs.rmSync(from, { force: true });
|
|
138
|
+
if (fs.existsSync(to)) {
|
|
139
|
+
// Both layouts wrote this sidecar: keep the newer, drop the other.
|
|
140
|
+
if (mtime(from) <= mtime(to)) return fs.rmSync(from, { force: true, recursive: true });
|
|
141
|
+
fs.rmSync(to, { force: true, recursive: true });
|
|
142
|
+
}
|
|
143
|
+
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
144
|
+
fs.renameSync(from, to);
|
|
145
|
+
moved.push(path.relative(ws, to).split(path.sep).join("/"));
|
|
146
|
+
};
|
|
147
|
+
fs.mkdirSync(path.join(ws, "state"), { recursive: true });
|
|
148
|
+
for (const [from, to] of LEGACY_MOVES) {
|
|
149
|
+
// `.draft-preview/draft-check.json` moves after the dir itself did.
|
|
150
|
+
const src = from.startsWith(".draft-preview/") ? path.join(ws, "preview/draft", path.basename(from)) : path.join(root, from);
|
|
151
|
+
move(src, to === null ? null : path.join(ws, to));
|
|
152
|
+
}
|
|
153
|
+
const rootState = path.join(root, "state");
|
|
154
|
+
if (isDir(rootState)) {
|
|
155
|
+
for (const n of fs.readdirSync(rootState)) move(path.join(rootState, n), path.join(ws, "state", n));
|
|
156
|
+
fs.rmSync(rootState, { recursive: true, force: true });
|
|
157
|
+
}
|
|
158
|
+
// The two stylesheets the flat layout kept at the root are the bundle's, and a
|
|
159
|
+
// deck no longer carries them — but ownership here is the same rule as
|
|
160
|
+
// everywhere else: MATCHING BYTES, never the filename. A copy the user edited
|
|
161
|
+
// is theirs and stays. Runs last, so the bundle ref has already moved into
|
|
162
|
+
// `state/` and `bundleDirFor` can resolve it; without a resolver nothing is
|
|
163
|
+
// deleted, because an unprovable delete is the one outcome worth avoiding.
|
|
164
|
+
const bundleDir = bundleDirFor ? bundleDirFor(ws) : null;
|
|
165
|
+
if (bundleDir) {
|
|
166
|
+
for (const [deckRel, bundleRel] of BUNDLE_SOURCES) {
|
|
167
|
+
const at = path.join(root, deckRel);
|
|
168
|
+
if (sameBytes(at, path.join(bundleDir, ...bundleRel))) fs.rmSync(at, { force: true });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return moved;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// The workspace a path lies inside, up to and including its last `.slidemind`
|
|
175
|
+
// segment — null when the path is not inside one.
|
|
176
|
+
function hostWorkspace(p) {
|
|
177
|
+
const parts = p.split(path.sep);
|
|
178
|
+
const i = parts.lastIndexOf(WORKSPACE_DIRNAME);
|
|
179
|
+
return i === -1 ? null : parts.slice(0, i + 1).join(path.sep);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* `<deck>/.slidemind` when it exists, else `<deck>` (a flat deck is its own
|
|
184
|
+
* workspace). A path INSIDE someone else's workspace is either a deck of its own
|
|
185
|
+
* — the designer's scratch deck at `.slidemind/scratch` — or part of its host, so
|
|
186
|
+
* `.slidemind/slides` and `.slidemind/plan/` resolve to the workspace that owns them.
|
|
187
|
+
*/
|
|
188
|
+
export function resolveWorkspace(deckDir) {
|
|
189
|
+
if (!deckDir) return deckDir;
|
|
190
|
+
const nested = path.join(deckDir, WORKSPACE_DIRNAME);
|
|
191
|
+
if (isDir(nested)) return nested;
|
|
192
|
+
const host = hostWorkspace(deckDir);
|
|
193
|
+
if (host && host !== deckDir && !isDeckDir(deckDir)) return host;
|
|
194
|
+
return deckDir;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** The deliverable root of a workspace — one level up from `.slidemind`, else itself. */
|
|
198
|
+
export function deckRoot(workspaceDir) {
|
|
199
|
+
if (!workspaceDir) return workspaceDir;
|
|
200
|
+
if (path.basename(workspaceDir) === WORKSPACE_DIRNAME) return path.dirname(workspaceDir);
|
|
201
|
+
const host = hostWorkspace(workspaceDir);
|
|
202
|
+
// Inside a workspace and not a deck itself: the deck is the one that owns it.
|
|
203
|
+
if (host && !isDeckDir(workspaceDir)) return path.dirname(host);
|
|
204
|
+
return workspaceDir;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** The deck's name, from the ROOT — `path.basename(workspaceDir)` would say ".slidemind". */
|
|
208
|
+
export function deckSlug(workspaceDir) {
|
|
209
|
+
return path.basename(path.resolve(deckRoot(workspaceDir)));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** The built presentation, at the deck root: `<deck>/<slug>.pptx`. */
|
|
213
|
+
export function deckPptxPath(workspaceDir) {
|
|
214
|
+
return path.join(deckRoot(workspaceDir), `${deckSlug(workspaceDir)}.pptx`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** The persisted plan page, at the deck root. */
|
|
218
|
+
export function planPagePath(workspaceDir) {
|
|
219
|
+
return path.join(deckRoot(workspaceDir), "plan.html");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// The eight sidecars, grouped under `state/`.
|
|
223
|
+
export const STATE_FILES = [
|
|
224
|
+
"slide-guids.json",
|
|
225
|
+
"bundle-ref.json",
|
|
226
|
+
"plan-review.json",
|
|
227
|
+
"review.json",
|
|
228
|
+
"draft-check.json",
|
|
229
|
+
"reconcile-ack.json",
|
|
230
|
+
"layout-context.json",
|
|
231
|
+
// The deck's image registry (src/lib/image-assets.mjs): immutable {assetId,
|
|
232
|
+
// sha256, …} entries for the bytes under `assets/local/`.
|
|
233
|
+
"image-assets.json",
|
|
234
|
+
// Image uses a TARGETED EDIT recorded outside a plan stage
|
|
235
|
+
// (src/lib/image-use-overrides.mjs), keyed by durable slide GUID. Folded back
|
|
236
|
+
// into the plan by the next approved revision.
|
|
237
|
+
"image-use-overrides.json",
|
|
238
|
+
// The intent fingerprint each concept mockup was generated under
|
|
239
|
+
// (src/builders/concept-intent.mjs) — what makes a stale mockup visible
|
|
240
|
+
// instead of silently reused.
|
|
241
|
+
"concept-intent.json",
|
|
242
|
+
];
|
|
243
|
+
|
|
244
|
+
/** `<workspace>/state/<name>`. */
|
|
245
|
+
export function statePath(workspaceDir, name) {
|
|
246
|
+
return path.join(workspaceDir, "state", name);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Write a sidecar, creating `state/` on the way. */
|
|
250
|
+
export function writeStateFile(workspaceDir, name, contents) {
|
|
251
|
+
const file = statePath(workspaceDir, name);
|
|
252
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
253
|
+
fs.writeFileSync(file, contents);
|
|
254
|
+
return file;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** `<workspace>/preview[/draft|/build]` — the one class of output nothing reads back. */
|
|
258
|
+
export function previewPath(workspaceDir, ...sub) {
|
|
259
|
+
return path.join(workspaceDir, "preview", ...sub);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
// bundle ownership
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
// Which files in a deck are COPIES of the design bundle's own. Some decks carry
|
|
267
|
+
// the two stylesheets and the chrome/master images; nothing reads them any more,
|
|
268
|
+
// so they are never uploaded and never hashed into a review approval.
|
|
269
|
+
//
|
|
270
|
+
// Ownership is the materialized bundle (the manifest of what the design owns)
|
|
271
|
+
// PLUS byte identity — never the filename. A user's own `assets/master-data.png`,
|
|
272
|
+
// and a same-named file whose bytes differ from the bundle's, stay deck-owned and
|
|
273
|
+
// keep their place in the preview, the upload and the review hash. With no bundle
|
|
274
|
+
// on hand nothing is bundle-owned, which is the safe direction: a deck file is
|
|
275
|
+
// only ever set aside on positive evidence.
|
|
276
|
+
//
|
|
277
|
+
// Returns workspace-relative POSIX paths.
|
|
278
|
+
//
|
|
279
|
+
// ponytail: byte compare after a size check, no content index — an assets dir is
|
|
280
|
+
// tens of files. Hash them if a deck ever carries hundreds.
|
|
281
|
+
const BUNDLE_SOURCES = [
|
|
282
|
+
["corporate-design.css", ["corporate-design.css"]],
|
|
283
|
+
["layouts.css", ["layouts", "layouts.css"]],
|
|
284
|
+
];
|
|
285
|
+
|
|
286
|
+
function sameBytes(a, b) {
|
|
287
|
+
try {
|
|
288
|
+
if (fs.statSync(a).size !== fs.statSync(b).size) return false;
|
|
289
|
+
return fs.readFileSync(a).equals(fs.readFileSync(b));
|
|
290
|
+
} catch {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function bundleOwnedFiles(workspaceDir, bundleDir) {
|
|
296
|
+
const owned = new Set();
|
|
297
|
+
if (!workspaceDir || !bundleDir) return owned;
|
|
298
|
+
for (const [deckRel, bundleRel] of BUNDLE_SOURCES) {
|
|
299
|
+
if (sameBytes(path.join(workspaceDir, deckRel), path.join(bundleDir, ...bundleRel))) {
|
|
300
|
+
owned.add(deckRel);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
let entries = [];
|
|
304
|
+
try {
|
|
305
|
+
entries = fs.readdirSync(path.join(workspaceDir, "assets"), { withFileTypes: true });
|
|
306
|
+
} catch {
|
|
307
|
+
return owned;
|
|
308
|
+
}
|
|
309
|
+
for (const e of entries) {
|
|
310
|
+
if (!e.isFile()) continue;
|
|
311
|
+
const deckFile = path.join(workspaceDir, "assets", e.name);
|
|
312
|
+
if (sameBytes(deckFile, path.join(bundleDir, "assets", e.name))) {
|
|
313
|
+
owned.add(`assets/${e.name}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return owned;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ---------------------------------------------------------------------------
|
|
320
|
+
// who owns plan.html
|
|
321
|
+
// ---------------------------------------------------------------------------
|
|
322
|
+
|
|
323
|
+
// One deck root, one plan page, one writer. The core CLI renders it from
|
|
324
|
+
// `plan.md`; an extension (a pack) renders its own from its own
|
|
325
|
+
// record. Whoever owns it says so in `state/plan-owner.json`:
|
|
326
|
+
//
|
|
327
|
+
// { version: 1, owner: "core" | "<extension id>", reviewCommand: "<what a human runs>",
|
|
328
|
+
// recordPath: "<workspace-relative record>", updatedAt: "<ISO>" }
|
|
329
|
+
//
|
|
330
|
+
// Generic gates never overwrite a page they do not own — they name the owner's
|
|
331
|
+
// review command instead. This is a protocol, not a list: the CLI knows nothing
|
|
332
|
+
// about any particular extension.
|
|
333
|
+
export const PLAN_OWNER_FILE = "plan-owner.json";
|
|
334
|
+
|
|
335
|
+
/** In the <head> of every page the CORE renderer wrote — an unmarked page is someone else's. */
|
|
336
|
+
export const PLAN_PAGE_MARKER = '<meta name="slidemind-plan-owner" content="core">';
|
|
337
|
+
|
|
338
|
+
export function readPlanOwner(workspaceDir) {
|
|
339
|
+
try {
|
|
340
|
+
const data = JSON.parse(fs.readFileSync(statePath(workspaceDir, PLAN_OWNER_FILE), "utf8"));
|
|
341
|
+
return data && typeof data.owner === "string" ? data : null;
|
|
342
|
+
} catch {
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function writePlanOwner(workspaceDir, { owner, reviewCommand, recordPath }) {
|
|
348
|
+
return writeStateFile(
|
|
349
|
+
workspaceDir,
|
|
350
|
+
PLAN_OWNER_FILE,
|
|
351
|
+
JSON.stringify(
|
|
352
|
+
{ version: 1, owner, reviewCommand, recordPath, updatedAt: new Date().toISOString() },
|
|
353
|
+
null,
|
|
354
|
+
2,
|
|
355
|
+
) + "\n",
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// First `bytes` of a file as text, or null — the marker sits in the <head>, and
|
|
360
|
+
// a rendered plan page is a few hundred KB of inlined modules.
|
|
361
|
+
function headOf(file, bytes = 4096) {
|
|
362
|
+
let fd;
|
|
363
|
+
try {
|
|
364
|
+
fd = fs.openSync(file, "r");
|
|
365
|
+
} catch {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
try {
|
|
369
|
+
const buf = Buffer.alloc(bytes);
|
|
370
|
+
const n = fs.readSync(fd, buf, 0, bytes, 0);
|
|
371
|
+
return buf.subarray(0, n).toString("utf8");
|
|
372
|
+
} finally {
|
|
373
|
+
fs.closeSync(fd);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// A plan record the core does not own: `plan.json` or `<something>-plan.json`,
|
|
378
|
+
// in the workspace or at the deck root. Decks written before the ownership file
|
|
379
|
+
// existed carry one of these and no declaration, so the evidence stands in for it.
|
|
380
|
+
function foreignPlanRecord(workspaceDir) {
|
|
381
|
+
for (const dir of new Set([workspaceDir, deckRoot(workspaceDir)])) {
|
|
382
|
+
const hit = planRecordIn(dir);
|
|
383
|
+
if (hit) return path.relative(workspaceDir, path.join(dir, hit)).split(path.sep).join("/");
|
|
384
|
+
}
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Who may write `<root>/plan.html`: the declaration when there is one, else the
|
|
390
|
+
* evidence on disk (an unmarked page, or a plan record the core does not own),
|
|
391
|
+
* else core. `declared` is false when the owner was inferred.
|
|
392
|
+
*/
|
|
393
|
+
export function planPageOwner(workspaceDir) {
|
|
394
|
+
const declared = readPlanOwner(workspaceDir);
|
|
395
|
+
if (declared) return { ...declared, declared: true };
|
|
396
|
+
|
|
397
|
+
// Transition: decks written before the declaration existed. A record the core
|
|
398
|
+
// does not own, or a page the core renderer did not write, is an extension's.
|
|
399
|
+
const extension = (recordPath) => ({
|
|
400
|
+
version: 1,
|
|
401
|
+
owner: "extension",
|
|
402
|
+
reviewCommand: null,
|
|
403
|
+
recordPath,
|
|
404
|
+
declared: false,
|
|
405
|
+
});
|
|
406
|
+
const record = foreignPlanRecord(workspaceDir);
|
|
407
|
+
if (record) return extension(record);
|
|
408
|
+
const head = headOf(planPagePath(workspaceDir));
|
|
409
|
+
if (head !== null && !head.includes(PLAN_PAGE_MARKER)) return extension(null);
|
|
410
|
+
|
|
411
|
+
return {
|
|
412
|
+
version: 1,
|
|
413
|
+
owner: "core",
|
|
414
|
+
reviewCommand: `slidemind plan ${deckSlug(workspaceDir)}`,
|
|
415
|
+
recordPath: "plan.md",
|
|
416
|
+
declared: false,
|
|
417
|
+
};
|
|
418
|
+
}
|