slidemind 0.3.19 → 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/engine-slides.mjs +1 -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
|
@@ -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
|
+
}
|
package/src/cli/common.mjs
CHANGED
|
@@ -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
|
|
33
|
-
// user
|
|
34
|
-
//
|
|
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(
|
|
54
|
-
const slug =
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|