slidemind 0.3.20 → 0.4.0
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 +591 -15
- package/README.md +202 -7
- package/bin/slidemind.mjs +50 -2
- package/package.json +2 -2
- 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/pptx/tags.mjs
CHANGED
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
relsFor,
|
|
19
19
|
relsPathFor,
|
|
20
20
|
resolvePartPath,
|
|
21
|
+
tagAttr,
|
|
22
|
+
tagElements,
|
|
21
23
|
} from "./ooxml.mjs";
|
|
22
24
|
|
|
23
25
|
export const CHUNK_BYTES = 12288;
|
|
@@ -75,7 +77,34 @@ function xmlEsc(s) {
|
|
|
75
77
|
// input: { name, jsx, guid, bundle:{presFP,etag}, renderer, assets:[{name,bytes}],
|
|
76
78
|
// baseline?, pushedAt, metaExtra? }
|
|
77
79
|
// `metaExtra` merges extra properties into SRC_META (e.g. kind:"template-fill");
|
|
78
|
-
// decoders ignore unknown properties.
|
|
80
|
+
// decoders ignore unknown properties. Two keys are in use:
|
|
81
|
+
// kind: "template-fill" — the pack-filled dialect (src/pptx/fill/engine.mjs)
|
|
82
|
+
// imageIntent: {v, uses, assets}
|
|
83
|
+
// the slide's approved image obligations and the registry entries they name
|
|
84
|
+
// (src/lib/image-intent-tags.mjs). Their BYTES need no arrangement here: the
|
|
85
|
+
// jsx references `./assets/local/<assetId>.<ext>`, so `referencedAssets`
|
|
86
|
+
// already embeds them like any other deck asset. A slide with no image use
|
|
87
|
+
// carries the key not at all, so its tags are unchanged.
|
|
88
|
+
// ONE writer, on purpose (decided 2026-09-21, spec §2.2): the server bakes
|
|
89
|
+
// this deck's tags on the render path but only ever receives the TRANSPORT
|
|
90
|
+
// projection of an asset, which omits `provenance` BY DESIGN (design
|
|
91
|
+
// decision 5: the envelope is a projection, not the registry — `provenance`
|
|
92
|
+
// is client-side knowledge no model endpoint has a use for, and adding it
|
|
93
|
+
// would churn the frozen contract fixture in both repos for a field only
|
|
94
|
+
// the client reads back). So the server's `assets` would be lossy exactly
|
|
95
|
+
// where `adopt` needs them. Do NOT fix that by teaching the server to write
|
|
96
|
+
// a provenance-less copy: the CLI holds the registry, so the CLI writes the
|
|
97
|
+
// key on BOTH bake paths, topping the server's meta up through
|
|
98
|
+
// `augmentSourceMeta` rather than letting two writers answer one key
|
|
99
|
+
// differently.
|
|
100
|
+
// preservation: the slide's `state/slide-preservation.json` entry
|
|
101
|
+
// — what it must still contain, its `active` requirements AND the ones an
|
|
102
|
+
// operation superseded (src/lib/preservation-tags.mjs). The state file does
|
|
103
|
+
// not travel and the .pptx does, so without this a deck adopted into a fresh
|
|
104
|
+
// workspace comes back owing nothing, and a deletion the user asked for
|
|
105
|
+
// comes back owed again. Restored only for a slide adopt proved current, and
|
|
106
|
+
// never over a record the target deck already holds. A slide with no record
|
|
107
|
+
// carries the key not at all.
|
|
79
108
|
export function encodeSourceTags(input) {
|
|
80
109
|
const tags = { [TAG_GUID]: input.guid };
|
|
81
110
|
|
|
@@ -263,9 +292,9 @@ export function readSlideTags(files, slidePath) {
|
|
|
263
292
|
const partPath = tagRels[ref[1]];
|
|
264
293
|
const tagXml = partPath ? readText(files, partPath) : null;
|
|
265
294
|
if (!tagXml) continue;
|
|
266
|
-
for (const t of tagXml
|
|
267
|
-
const name = t[1]
|
|
268
|
-
const val = t[1]
|
|
295
|
+
for (const t of tagElements(tagXml)) {
|
|
296
|
+
const name = tagAttr(t[1], "name");
|
|
297
|
+
const val = tagAttr(t[1], "val");
|
|
269
298
|
if (name) out[decodeXmlEntities(name)] = decodeXmlEntities(val ?? "");
|
|
270
299
|
}
|
|
271
300
|
}
|
|
@@ -273,6 +302,76 @@ export function readSlideTags(files, slidePath) {
|
|
|
273
302
|
return out;
|
|
274
303
|
}
|
|
275
304
|
|
|
305
|
+
/**
|
|
306
|
+
* Rewrite ONE existing slide tag's value, in the part that already holds it.
|
|
307
|
+
*
|
|
308
|
+
* The bake next door is additive: handed a tag name a slide already carries, it
|
|
309
|
+
* appends a SECOND `<p:tag>` under the same name. For a fresh slide that is
|
|
310
|
+
* right — nothing is ever renumbered or overwritten (rule C1). For amending a
|
|
311
|
+
* tag the SERVER wrote it is not: two `SLIDEMIND_SRC_META` tags in one tagLst
|
|
312
|
+
* let one reader take the first and another the last, so the same file says two
|
|
313
|
+
* different things. This rewrites the value where it stands instead.
|
|
314
|
+
*
|
|
315
|
+
* Attribute-order tolerant on purpose — a PowerPoint round trip may reorder
|
|
316
|
+
* them, and the server's writer is a different implementation of the same
|
|
317
|
+
* format. Returns true when a tag was found and rewritten.
|
|
318
|
+
*/
|
|
319
|
+
export function updateSlideTag(files, slidePath, name, value) {
|
|
320
|
+
const xml = readText(files, slidePath);
|
|
321
|
+
if (!xml) return false;
|
|
322
|
+
const dir = slidePath.slice(0, slidePath.lastIndexOf("/"));
|
|
323
|
+
const tagRels = {};
|
|
324
|
+
for (const rel of relsFor(files, slidePath)) {
|
|
325
|
+
if (rel.id && rel.type === TAGS_REL_TYPE) tagRels[rel.id] = resolvePartPath(dir, rel.target);
|
|
326
|
+
}
|
|
327
|
+
for (const cust of xml.matchAll(/<p:custDataLst\b[\s\S]*?<\/p:custDataLst>/g)) {
|
|
328
|
+
for (const ref of cust[0].matchAll(/<p:tags\b[^>]*\br:id="([^"]+)"/g)) {
|
|
329
|
+
const partPath = tagRels[ref[1]];
|
|
330
|
+
const tagXml = partPath ? readText(files, partPath) : null;
|
|
331
|
+
if (!tagXml) continue;
|
|
332
|
+
for (const t of tagElements(tagXml)) {
|
|
333
|
+
if (decodeXmlEntities(tagAttr(t[1], "name") ?? "") !== name) continue;
|
|
334
|
+
const replaced = t[0].replace(/\bval=(?:"[^"]*"|'[^']*')/, `val="${xmlEsc(value)}"`);
|
|
335
|
+
files[partPath] = enc(tagXml.replace(t[0], replaced));
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Add SRC_META extras to a slide whose source was baked by SOMEONE ELSE.
|
|
345
|
+
*
|
|
346
|
+
* The server bakes the tags on the render path, and there are extras it cannot
|
|
347
|
+
* write: `imageIntent` needs the deck's registry (the transport descriptor the
|
|
348
|
+
* server holds deliberately carries no `provenance`), and the local records
|
|
349
|
+
* beside it are the CLI's own. Rather than a second writer emitting a lossy
|
|
350
|
+
* payload under the same key, the CLI tops the server's meta up in place.
|
|
351
|
+
*
|
|
352
|
+
* Additive and idempotent: only the named keys are touched, an extra already
|
|
353
|
+
* equal to what is there is not rewritten, and a slide with no SRC_META at all
|
|
354
|
+
* is left alone. Returns true when the part changed.
|
|
355
|
+
*/
|
|
356
|
+
export function augmentSourceMeta(files, slidePath, extras) {
|
|
357
|
+
if (!extras || Object.keys(extras).length === 0) return false;
|
|
358
|
+
const raw = readSlideTags(files, slidePath)[TAG_SRC_META];
|
|
359
|
+
if (!raw) return false;
|
|
360
|
+
let meta;
|
|
361
|
+
try {
|
|
362
|
+
meta = JSON.parse(raw);
|
|
363
|
+
} catch {
|
|
364
|
+
return false; // not ours to repair — a corrupt payload stays corrupt
|
|
365
|
+
}
|
|
366
|
+
let changed = false;
|
|
367
|
+
for (const [key, value] of Object.entries(extras)) {
|
|
368
|
+
if (JSON.stringify(meta[key]) === JSON.stringify(value)) continue;
|
|
369
|
+
meta[key] = value;
|
|
370
|
+
changed = true;
|
|
371
|
+
}
|
|
372
|
+
return changed ? updateSlideTag(files, slidePath, TAG_SRC_META, JSON.stringify(meta)) : false;
|
|
373
|
+
}
|
|
374
|
+
|
|
276
375
|
// True when a slide part carries any tag key OUTSIDE the SLIDEMIND_ namespace
|
|
277
376
|
// (another add-in marked it — think-cell tags every shape it owns).
|
|
278
377
|
export function slideHasForeignTags(files, slidePath) {
|
|
@@ -345,6 +444,24 @@ export function bakeSlideSourceTags(files, entries) {
|
|
|
345
444
|
const slideXml = readText(files, entry.partName);
|
|
346
445
|
if (!slideXml || !slideXml.includes("</p:cSld>")) continue;
|
|
347
446
|
|
|
447
|
+
// One p:tags per customer-data list (CT_CustomerDataList): a slide that
|
|
448
|
+
// already carries one gets our tags merged into that part.
|
|
449
|
+
const sharedRef = slideXml.match(/<\/p:spTree>\s*<p:custDataLst\b[^>]*>[\s\S]*?<p:tags\b[^>]*\br:id="([^"]+)"/);
|
|
450
|
+
if (sharedRef) {
|
|
451
|
+
const slideDir = entry.partName.slice(0, entry.partName.lastIndexOf("/"));
|
|
452
|
+
const rel = relsFor(files, entry.partName).find((r) => r.id === sharedRef[1] && r.type === TAGS_REL_TYPE);
|
|
453
|
+
const sharedPart = rel ? resolvePartPath(slideDir, rel.target) : null;
|
|
454
|
+
const sharedXml = sharedPart ? readText(files, sharedPart) : null;
|
|
455
|
+
if (sharedXml && sharedXml.includes("</p:tagLst>")) {
|
|
456
|
+
const ours = Object.entries(entry.tags)
|
|
457
|
+
.map(([name, val]) => `<p:tag name="${xmlEsc(name)}" val="${xmlEsc(val)}"/>`)
|
|
458
|
+
.join("");
|
|
459
|
+
files[sharedPart] = enc(sharedXml.replace("</p:tagLst>", `${ours}</p:tagLst>`));
|
|
460
|
+
changed = true;
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
348
465
|
const partName = `ppt/tags/tag${tagIndex}.xml`;
|
|
349
466
|
files[partName] = enc(tagPartXml(entry.tags));
|
|
350
467
|
if (!contentTypes.includes(`PartName="/${partName}"`)) {
|
|
@@ -448,7 +565,13 @@ const TOP_SHAPE_RE =
|
|
|
448
565
|
const UNIT_BLOCK_KIND = "graphicFrame";
|
|
449
566
|
|
|
450
567
|
// Split the spTree inner XML into top-level shape subtrees (depth-aware).
|
|
451
|
-
|
|
568
|
+
//
|
|
569
|
+
// Exported for `src/pptx/package-evidence.mjs`, which needs the same units this
|
|
570
|
+
// projection reports — in the same order, with the same MC-block rule — to bind
|
|
571
|
+
// each one to its `p:cNvPr/@id` and its package dependencies. Re-deriving the
|
|
572
|
+
// split there would be a second normalizer that WILL drift from this one. This
|
|
573
|
+
// export adds no behaviour, so the cross-repo parity fixture is untouched.
|
|
574
|
+
export function topLevelShapes(spTreeInner) {
|
|
452
575
|
const shapes = [];
|
|
453
576
|
let depth = 0;
|
|
454
577
|
let start = -1;
|
|
@@ -630,9 +753,9 @@ function parseProv(files, slidePath, shapeXml) {
|
|
|
630
753
|
if (!rel?.type.endsWith("/tags")) continue;
|
|
631
754
|
const tagXml = readText(files, resolvePartPath(dir, rel.target));
|
|
632
755
|
if (!tagXml) continue;
|
|
633
|
-
for (const t of tagXml
|
|
634
|
-
const name = t[1]
|
|
635
|
-
const val = t[1]
|
|
756
|
+
for (const t of tagElements(tagXml)) {
|
|
757
|
+
const name = tagAttr(t[1], "name");
|
|
758
|
+
const val = tagAttr(t[1], "val");
|
|
636
759
|
if (name) tags[xmlUnescape(name)] = xmlUnescape(val ?? "");
|
|
637
760
|
}
|
|
638
761
|
}
|
|
@@ -649,6 +772,34 @@ function parseProv(files, slidePath, shapeXml) {
|
|
|
649
772
|
return null;
|
|
650
773
|
}
|
|
651
774
|
|
|
775
|
+
// Every leaf shape inside a group subtree, document order (nested groups walked).
|
|
776
|
+
function groupLeaves(groupXml) {
|
|
777
|
+
const inner = groupXml.slice(groupXml.indexOf(">") + 1, groupXml.lastIndexOf("</p:grpSp>"));
|
|
778
|
+
const out = [];
|
|
779
|
+
for (const child of topLevelShapes(inner)) {
|
|
780
|
+
if (child.kind === "grpSp") out.push(...groupLeaves(child.xml));
|
|
781
|
+
else out.push(child);
|
|
782
|
+
}
|
|
783
|
+
return out;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// A group's parts (`ProjectionShape.parts`): a card moved or recoloured INSIDE
|
|
787
|
+
// a group is a manual edit the group's own box and first fill cannot show.
|
|
788
|
+
// LOCKSTEP with the add-in's `slide-source-projection.ts` `groupParts`.
|
|
789
|
+
function groupParts(raw) {
|
|
790
|
+
return groupLeaves(raw.xml).map((leaf) => {
|
|
791
|
+
const paint = parseFillAndLine(leaf.xml);
|
|
792
|
+
const runs = parseTextRuns(leaf.xml);
|
|
793
|
+
return {
|
|
794
|
+
kind: leaf.kind,
|
|
795
|
+
geom: parseXfrm(leaf.xml),
|
|
796
|
+
fill: paint.fill,
|
|
797
|
+
line: paint.line,
|
|
798
|
+
t: runs ? runs.map((r) => r.t).join("") : null,
|
|
799
|
+
};
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
|
|
652
803
|
// Structural projection of a slide in an unzipped package (the FIRST slide when
|
|
653
804
|
// `slidePathOverride` is omitted — the single-slide case). Deterministic;
|
|
654
805
|
// excludes every id/name/theme-resolved churn source. Groups are one entry each
|
|
@@ -675,6 +826,7 @@ export function computeProjection(files, slidePathOverride) {
|
|
|
675
826
|
// are byte-compared); the VALUE is the one thing this mirror cannot
|
|
676
827
|
// reproduce.
|
|
677
828
|
chart: null,
|
|
829
|
+
...(raw.kind === "grpSp" ? { parts: groupParts(raw) } : {}),
|
|
678
830
|
});
|
|
679
831
|
}
|
|
680
832
|
return { v: 1, shapes };
|
|
@@ -701,9 +853,9 @@ export function computeProjection(files, slidePathOverride) {
|
|
|
701
853
|
// or styling changed. Everything else about the slide — geometry, text, fill,
|
|
702
854
|
// line, media — still compares exactly. The consumers below therefore drop the
|
|
703
855
|
// field from BOTH sides before comparing, so a chart can never produce a FALSE
|
|
704
|
-
// "someone edited this" refusal
|
|
705
|
-
//
|
|
706
|
-
// the
|
|
856
|
+
// "someone edited this" refusal. A chart-ONLY edit is invisible to that
|
|
857
|
+
// comparison, so manual-edit reconcile (src/lib/reconcile.mjs) sends every
|
|
858
|
+
// chart-holding slide to the add-in as its package and lets its reader decide.
|
|
707
859
|
//
|
|
708
860
|
// Closing it properly means giving this repo the reader as a GENERATED artifact
|
|
709
861
|
// built from the add-in's TypeScript — never a hand-written copy.
|
package/src/update-check.mjs
CHANGED
|
@@ -19,7 +19,11 @@ import { fileURLToPath } from "node:url";
|
|
|
19
19
|
|
|
20
20
|
import { compareCliVersions } from "./bridge/client.mjs";
|
|
21
21
|
import { sessionConfigDir } from "./bridge/config.mjs";
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
globalInstallBlock,
|
|
24
|
+
isClaudePluginInstall,
|
|
25
|
+
updateCliInstruction,
|
|
26
|
+
} from "./install-channel.mjs";
|
|
23
27
|
|
|
24
28
|
const PACKAGE_ROOT = path.resolve(
|
|
25
29
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
@@ -117,9 +121,15 @@ function autoUpdateEnabled(channel) {
|
|
|
117
121
|
|
|
118
122
|
// The message, as one pure function — everything above it decides WHETHER to
|
|
119
123
|
// call it, this decides WHAT it says. `auto` is what we actually kicked off.
|
|
120
|
-
export function updateNotice({ current, latest, channel, auto, stalled }) {
|
|
124
|
+
export function updateNotice({ current, latest, channel, auto, stalled, blocked }) {
|
|
121
125
|
if (!latest || compareCliVersions(current, latest) >= 0) return null;
|
|
122
126
|
const head = `slidemind ${current} is out of date — ${latest} is on npm.`;
|
|
127
|
+
// The running process can't replace the global install itself (a sandbox, or
|
|
128
|
+
// a root-owned prefix): say so and ask for escalated rights, never claim a
|
|
129
|
+
// background update that silently failed.
|
|
130
|
+
if (blocked) {
|
|
131
|
+
return `${head} ${updateCliInstruction(blocked)}`;
|
|
132
|
+
}
|
|
123
133
|
if (stalled) {
|
|
124
134
|
return (
|
|
125
135
|
`${head} A background auto-update to ${latest} was started earlier but this ` +
|
|
@@ -207,7 +217,14 @@ export async function announceUpdate({ command, current, force = false }) {
|
|
|
207
217
|
prior?.version === latest &&
|
|
208
218
|
Date.now() - (Number(prior.at) || 0) < intervalMs();
|
|
209
219
|
let auto = false;
|
|
210
|
-
|
|
220
|
+
// Will a background `npm install -g` even stick? In a Codex sandbox (and
|
|
221
|
+
// behind a root-owned prefix) it cannot: the install fails, and with the
|
|
222
|
+
// config dir unwritable too the attempt record never persists — so a doomed
|
|
223
|
+
// install would respawn on every single command while the nudge kept
|
|
224
|
+
// claiming success. Pre-flight it. When the global install is unwritable we
|
|
225
|
+
// don't spawn; the notice asks for escalated rights instead.
|
|
226
|
+
const blocked = autoUpdateEnabled(channel) ? globalInstallBlock() : null;
|
|
227
|
+
if (autoUpdateEnabled(channel) && !blocked && !attempted) {
|
|
211
228
|
auto = startBackgroundUpdate(latest);
|
|
212
229
|
if (auto) {
|
|
213
230
|
writeCache({
|
|
@@ -222,7 +239,8 @@ export async function announceUpdate({ command, current, force = false }) {
|
|
|
222
239
|
latest,
|
|
223
240
|
channel,
|
|
224
241
|
auto,
|
|
225
|
-
stalled: attempted,
|
|
242
|
+
stalled: attempted && !blocked,
|
|
243
|
+
blocked,
|
|
226
244
|
});
|
|
227
245
|
if (notice) process.stderr.write(`slidemind: ${notice}\n`);
|
|
228
246
|
} catch {
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// Trusted workspace adapter — protocol `slidemind.workspace-adapter` v1.
|
|
2
|
+
//
|
|
3
|
+
// The persistent workspace shows a pack deck's plan through the pack that owns
|
|
4
|
+
// it. The pack is a separate, privately installed program; this is the wire.
|
|
5
|
+
// The single rule, inherited from src/lib/extensions.mjs: **the deck names an
|
|
6
|
+
// adapter id, this machine's registry names what runs.** No deck file, remote
|
|
7
|
+
// record or HTTP request ever supplies an executable or an argument.
|
|
8
|
+
//
|
|
9
|
+
// One JSON request on stdin, one JSON response on stdout, stderr is the pack's
|
|
10
|
+
// diagnostics. Bounded in time (30s, 120s for a workbook operation) and in
|
|
11
|
+
// output (8 MiB — read incrementally and killed past the bound, so the daemon
|
|
12
|
+
// never buffers an unbounded child). A failure on the other side comes back
|
|
13
|
+
// as `{ status: "error", error }`, never as an exception: the caller decides
|
|
14
|
+
// what a missing pack means for THIS view, and it is not a stack trace.
|
|
15
|
+
|
|
16
|
+
import { spawn } from "node:child_process";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
|
|
19
|
+
import { readRegistry } from "../lib/extensions.mjs";
|
|
20
|
+
|
|
21
|
+
export const PROTOCOL = "slidemind.workspace-adapter";
|
|
22
|
+
export const PROTOCOL_VERSION = 1;
|
|
23
|
+
export const METHODS = ["describe", "read", "render", "save-local", "import-workbook", "validate", "approve", "apply-remote"];
|
|
24
|
+
export const STATUSES = ["ok", "blocked", "error"];
|
|
25
|
+
|
|
26
|
+
export const DEFAULT_TIMEOUT_MS = 30_000;
|
|
27
|
+
export const WORKBOOK_TIMEOUT_MS = 120_000;
|
|
28
|
+
export const MAX_OUTPUT_BYTES = 8 * 1024 * 1024;
|
|
29
|
+
const MAX_STDERR_BYTES = 64 * 1024;
|
|
30
|
+
// How long after the process exited we still wait for its pipes to reach EOF
|
|
31
|
+
// before answering anyway. Real drain is immediate; only a leaked helper holding
|
|
32
|
+
// the adapter's stdout takes the whole grace.
|
|
33
|
+
const EXIT_GRACE_MS = 250;
|
|
34
|
+
|
|
35
|
+
const error = (code, message, { retryable = false, details } = {}) => ({
|
|
36
|
+
status: "error",
|
|
37
|
+
data: null,
|
|
38
|
+
error: { code, message, retryable, ...(details ? { details } : {}) },
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Which installed adapter serves an id. `{ id, entry }` when usable —
|
|
43
|
+
* `entry.workspace.command` is the argv — or `{ id, blocker }` with the
|
|
44
|
+
* remedy spelled out: `extension_missing` (install it) vs
|
|
45
|
+
* `extension_incompatible` (the installed copy predates the workspace, or
|
|
46
|
+
* speaks another protocol version).
|
|
47
|
+
*/
|
|
48
|
+
export function resolveWorkspaceAdapter(id, registry = readRegistry(), { protocolVersion = PROTOCOL_VERSION } = {}) {
|
|
49
|
+
if (typeof id !== "string" || !id) {
|
|
50
|
+
return { id: null, blocker: { code: "extension_incompatible", message: "no adapter id", actor: "user" } };
|
|
51
|
+
}
|
|
52
|
+
const entry = registry.extensions[id];
|
|
53
|
+
if (!entry) {
|
|
54
|
+
return {
|
|
55
|
+
id,
|
|
56
|
+
blocker: {
|
|
57
|
+
code: "extension_missing",
|
|
58
|
+
message: `this deck's plan is owned by the '${id}' pack, which is not installed on this machine. Install it to edit the plan here; the deck's files are untouched.`,
|
|
59
|
+
actor: "user",
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (!entry.workspace) {
|
|
64
|
+
return {
|
|
65
|
+
id,
|
|
66
|
+
blocker: {
|
|
67
|
+
code: "extension_incompatible",
|
|
68
|
+
message: `the installed '${id}' pack has no workspace adapter. Update it to a version that registers one.`,
|
|
69
|
+
actor: "user",
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (!entry.workspace.protocolVersions.includes(protocolVersion)) {
|
|
74
|
+
return {
|
|
75
|
+
id,
|
|
76
|
+
blocker: {
|
|
77
|
+
code: "extension_incompatible",
|
|
78
|
+
message: `the workspace speaks '${id}' adapter protocol v${protocolVersion}; the installed copy speaks v${entry.workspace.protocolVersions.join(", v")}. Update one of them.`,
|
|
79
|
+
actor: "user",
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return { id, entry };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function buildRequest({ method, deckRoot, payload = {}, operationId = null }) {
|
|
87
|
+
return { protocol: PROTOCOL, version: PROTOCOL_VERSION, operationId, method, deckRoot, payload };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Run one adapter method. Resolves to `{ status, data, error }`; rejects only
|
|
92
|
+
* for a programming error (unknown method, entry without a workspace command).
|
|
93
|
+
*
|
|
94
|
+
* `ownerToken` is the deck lock token of the operation this call executes
|
|
95
|
+
* inside (src/workspace/operations.mjs) — it goes to the child as
|
|
96
|
+
* `SLIDEMIND_OWNER` and nowhere else. Without one the child gets NO token, not
|
|
97
|
+
* the daemon's inherited environment. `SLIDEMIND_WORKSPACE_ADAPTER=1` marks
|
|
98
|
+
* the child so the pack can refuse to call back into the daemon.
|
|
99
|
+
*/
|
|
100
|
+
export function dispatchAdapter(entry, { method, deckRoot, payload = {}, operationId = null, ownerToken = null, timeoutMs = null } = {}) {
|
|
101
|
+
if (!METHODS.includes(method)) throw new Error(`unknown workspace adapter method: ${method}`);
|
|
102
|
+
const argv = entry?.workspace?.command;
|
|
103
|
+
if (!Array.isArray(argv) || argv.length === 0) throw new Error("adapter entry has no workspace command");
|
|
104
|
+
if (typeof deckRoot !== "string" || !path.isAbsolute(deckRoot)) {
|
|
105
|
+
return Promise.resolve(error("validation_failed", "deckRoot must be an absolute local path"));
|
|
106
|
+
}
|
|
107
|
+
const timeout = timeoutMs ?? (method === "import-workbook" ? WORKBOOK_TIMEOUT_MS : DEFAULT_TIMEOUT_MS);
|
|
108
|
+
const request = buildRequest({ method, deckRoot, payload, operationId });
|
|
109
|
+
|
|
110
|
+
const env = { ...process.env, SLIDEMIND_WORKSPACE_ADAPTER: "1" };
|
|
111
|
+
delete env.SLIDEMIND_OWNER;
|
|
112
|
+
if (ownerToken) env.SLIDEMIND_OWNER = ownerToken;
|
|
113
|
+
|
|
114
|
+
return new Promise((resolve) => {
|
|
115
|
+
let child;
|
|
116
|
+
try {
|
|
117
|
+
child = spawn(argv[0], argv.slice(1), { env, shell: false, stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
|
|
118
|
+
} catch (err) {
|
|
119
|
+
resolve(error("extension_incompatible", `the adapter could not be started (${err.code ?? err.message})`));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const out = [];
|
|
123
|
+
let outBytes = 0;
|
|
124
|
+
let err = "";
|
|
125
|
+
let settled = false;
|
|
126
|
+
let why = null; // "timeout" | "oversize" once we killed it
|
|
127
|
+
let exitGrace = null;
|
|
128
|
+
const finish = (value) => {
|
|
129
|
+
if (settled) return;
|
|
130
|
+
settled = true;
|
|
131
|
+
clearTimeout(timer);
|
|
132
|
+
clearTimeout(exitGrace);
|
|
133
|
+
resolve(value);
|
|
134
|
+
};
|
|
135
|
+
const kill = (reason) => {
|
|
136
|
+
why = reason;
|
|
137
|
+
child.kill("SIGKILL");
|
|
138
|
+
};
|
|
139
|
+
const timer = setTimeout(() => kill("timeout"), timeout);
|
|
140
|
+
|
|
141
|
+
child.on("error", (e) =>
|
|
142
|
+
finish(error("extension_incompatible", `the adapter could not be run (${e.code ?? e.message})`)),
|
|
143
|
+
);
|
|
144
|
+
child.stdout.on("data", (chunk) => {
|
|
145
|
+
outBytes += chunk.length;
|
|
146
|
+
if (outBytes > MAX_OUTPUT_BYTES) {
|
|
147
|
+
out.length = 0;
|
|
148
|
+
kill("oversize");
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
out.push(chunk);
|
|
152
|
+
});
|
|
153
|
+
child.stderr.on("data", (chunk) => {
|
|
154
|
+
if (err.length < MAX_STDERR_BYTES) err += chunk.toString("utf8");
|
|
155
|
+
});
|
|
156
|
+
const done = (code, signal) => {
|
|
157
|
+
const lastLine = err.trim().split("\n").filter(Boolean).slice(-1)[0] ?? "";
|
|
158
|
+
if (why === "timeout") {
|
|
159
|
+
finish(error("extension_incompatible", `the adapter did not answer ${method} within ${timeout} ms`, { retryable: true }));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (why === "oversize") {
|
|
163
|
+
finish(error("extension_incompatible", `the adapter's ${method} answer exceeded ${MAX_OUTPUT_BYTES} bytes`));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (code !== 0) {
|
|
167
|
+
finish(error("extension_incompatible", `the adapter exited ${code ?? signal}${lastLine ? `: ${lastLine}` : ""}`));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
finish(parseResponse(Buffer.concat(out).toString("utf8"), method));
|
|
171
|
+
};
|
|
172
|
+
child.on("close", done);
|
|
173
|
+
// `close` needs stdio EOF, which never arrives when the adapter left a helper
|
|
174
|
+
// holding its stdout open — not even after SIGKILL, since the pipe belongs to
|
|
175
|
+
// the helper. This promise would then never settle, so `runDeckOperation` would
|
|
176
|
+
// never release the deck's busy.json and every later save, task and sync on that
|
|
177
|
+
// deck would answer `deck_busy` until the daemon restarts. The process itself is
|
|
178
|
+
// gone here: give the pipes a moment to drain, then answer with what arrived.
|
|
179
|
+
child.on("exit", (code, signal) => {
|
|
180
|
+
if (settled || exitGrace) return;
|
|
181
|
+
exitGrace = setTimeout(() => done(code, signal), EXIT_GRACE_MS);
|
|
182
|
+
exitGrace.unref?.();
|
|
183
|
+
});
|
|
184
|
+
child.stdin.on("error", () => {}); // child gone before it read: `close` reports it
|
|
185
|
+
child.stdin.end(JSON.stringify(request));
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** One JSON object, a known status, data as an object, version 1 when stated. */
|
|
190
|
+
export function parseResponse(text, method) {
|
|
191
|
+
let parsed;
|
|
192
|
+
try {
|
|
193
|
+
parsed = JSON.parse(text);
|
|
194
|
+
} catch {
|
|
195
|
+
return error("extension_incompatible", `the adapter did not answer ${method} with JSON`);
|
|
196
|
+
}
|
|
197
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
198
|
+
return error("extension_incompatible", `the adapter's ${method} answer is not an object`);
|
|
199
|
+
}
|
|
200
|
+
if (parsed.version !== undefined && parsed.version !== PROTOCOL_VERSION) {
|
|
201
|
+
return error("extension_incompatible", `the adapter answered with protocol version ${parsed.version}; this workspace speaks v${PROTOCOL_VERSION}`);
|
|
202
|
+
}
|
|
203
|
+
if (parsed.protocol !== undefined && parsed.protocol !== PROTOCOL) {
|
|
204
|
+
return error("extension_incompatible", `the adapter answered protocol '${parsed.protocol}'`);
|
|
205
|
+
}
|
|
206
|
+
if (!STATUSES.includes(parsed.status)) {
|
|
207
|
+
return error("extension_incompatible", `the adapter's ${method} status '${parsed.status}' is not ok/blocked/error`);
|
|
208
|
+
}
|
|
209
|
+
const data = parsed.data && typeof parsed.data === "object" ? parsed.data : null;
|
|
210
|
+
if (parsed.status === "ok") return { status: "ok", data, error: null };
|
|
211
|
+
const e = parsed.error && typeof parsed.error === "object" ? parsed.error : {};
|
|
212
|
+
return {
|
|
213
|
+
status: parsed.status,
|
|
214
|
+
data,
|
|
215
|
+
error: {
|
|
216
|
+
code: typeof e.code === "string" ? e.code : parsed.status === "blocked" ? "blocked" : "adapter_error",
|
|
217
|
+
message: typeof e.message === "string" ? e.message : `${method} did not pass`,
|
|
218
|
+
retryable: e.retryable === true,
|
|
219
|
+
...(e.details !== undefined ? { details: e.details } : {}),
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|