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
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// `slidemind workspace …` — the persistent local workspace (browser UI, deck
|
|
2
|
+
// library, discovery). Adoption/push/new live in workspace.mjs; this module is
|
|
3
|
+
// the UI/process surface only.
|
|
4
|
+
//
|
|
5
|
+
// Exit codes (contracts §1): 0 done, 1 operational error, 2 blocked / busy /
|
|
6
|
+
// incompatible, 3 conflict. Under --json exactly one JSON value goes to stdout;
|
|
7
|
+
// everything else to stderr. The code is set on process.exitCode so bin's
|
|
8
|
+
// post-command hooks still run.
|
|
9
|
+
|
|
10
|
+
import { addDeck, listDecks } from "../workspace/registry.mjs";
|
|
11
|
+
import { deckRoot } from "../lib/deck-paths.mjs";
|
|
12
|
+
import { resolveDeckArg } from "./common.mjs";
|
|
13
|
+
import { startDiscovery } from "../workspace/discovery.mjs";
|
|
14
|
+
import { daemonRequest, ensureRunning, readDescriptor, registerDeck, stopWorkspace, workspaceStatus } from "../workspace/lifecycle.mjs";
|
|
15
|
+
import { resolveRemoteLink, selectChoice } from "../workspace/remote-links.mjs";
|
|
16
|
+
import { connectDestination, remoteStatus, uploadToDestination } from "../workspace/remote-status.mjs";
|
|
17
|
+
|
|
18
|
+
const EXIT_BY_CODE = {
|
|
19
|
+
deck_busy: 2,
|
|
20
|
+
registry_busy: 2,
|
|
21
|
+
workspace_upgrade_required: 2,
|
|
22
|
+
workspace_start_failed: 1,
|
|
23
|
+
// the workspace cannot run here (an agent sandbox) or is wedged: blocked, not broken
|
|
24
|
+
workspace_sandboxed: 2,
|
|
25
|
+
workspace_unresponsive: 2,
|
|
26
|
+
cli_update_required: 2,
|
|
27
|
+
path_registered: 3,
|
|
28
|
+
// remote association: blocked (2) vs a conflict with existing work (3)
|
|
29
|
+
backend_ambiguous: 2,
|
|
30
|
+
select_deck: 2,
|
|
31
|
+
login_required: 2,
|
|
32
|
+
access_denied: 2,
|
|
33
|
+
write_disabled: 2,
|
|
34
|
+
selection_expired: 2,
|
|
35
|
+
unsupported_link: 2,
|
|
36
|
+
offline: 2,
|
|
37
|
+
dirty_target: 3,
|
|
38
|
+
already_bound: 3,
|
|
39
|
+
name_exists: 3,
|
|
40
|
+
cross_tenant: 3,
|
|
41
|
+
};
|
|
42
|
+
const VALUE_FLAGS = ["--deck", "--backend", "--folder", "--selection", "--choice", "--name"];
|
|
43
|
+
|
|
44
|
+
function parse(rest) {
|
|
45
|
+
const flags = { json: false, open: true, foreground: false };
|
|
46
|
+
const positional = [];
|
|
47
|
+
for (let i = 0; i < rest.length; i++) {
|
|
48
|
+
const a = rest[i];
|
|
49
|
+
if (VALUE_FLAGS.includes(a)) {
|
|
50
|
+
const v = rest[++i];
|
|
51
|
+
if (v === undefined || v.startsWith("--")) throw Object.assign(new Error(`${a} needs a value`), { code: "usage" });
|
|
52
|
+
flags[a.slice(2)] = v;
|
|
53
|
+
} else if (a === "--json") flags.json = true;
|
|
54
|
+
else if (a === "--no-open") flags.open = false;
|
|
55
|
+
else if (a === "--foreground") flags.foreground = true;
|
|
56
|
+
else if (a.startsWith("--")) throw Object.assign(new Error(`unknown flag ${a}`), { code: "usage" });
|
|
57
|
+
else positional.push(a);
|
|
58
|
+
}
|
|
59
|
+
return { flags, positional };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function emit(flags, payload, human) {
|
|
63
|
+
const write = flags.write ?? ((s) => process.stdout.write(s));
|
|
64
|
+
write((flags.json ? JSON.stringify(payload) : human) + "\n");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// A settled link job on stdout; blocked/failed set the exit code by its error.
|
|
68
|
+
function finishJob(flags, job) {
|
|
69
|
+
const base = { jobId: job.id, state: job.state, result: job.result, error: job.error, selection: job.selection ? { selectionId: job.selection.selectionId, expiresAt: job.selection.expiresAt, choices: job.selection.choices, nextCursor: job.selection.cursor } : null };
|
|
70
|
+
let human;
|
|
71
|
+
if (job.state === "completed" && job.result?.type === "folder") {
|
|
72
|
+
human = `select one (selection ${job.result.selectionId}):\n` + job.result.choices.map((c) => ` ${c.id}\t${c.kind}\t${c.name}`).join("\n") + `\n slidemind workspace select ${job.id} --selection ${job.result.selectionId} --choice <id>`;
|
|
73
|
+
} else if (job.state === "completed") {
|
|
74
|
+
human = `${job.result.type} ${job.result.name ?? ""}: ${job.result.existing ? "already registered as" : "opened as"} deck ${job.result.deckId}`;
|
|
75
|
+
} else {
|
|
76
|
+
human = `${job.state}: ${job.error?.code} — ${job.error?.message}` + (job.error?.choices ? `\n choices: ${JSON.stringify(job.error.choices)}` : "");
|
|
77
|
+
}
|
|
78
|
+
emit(flags, base, human);
|
|
79
|
+
if (job.state !== "completed") process.exitCode = EXIT_BY_CODE[job.error?.code] ?? 1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// A deck argument the way every deck command takes it (a slug under ./decks, a
|
|
83
|
+
// relative or absolute path); its ROOT is what the library registers.
|
|
84
|
+
function deckRootArg(arg) {
|
|
85
|
+
const ws = resolveDeckArg(arg, { allowUnfinished: true });
|
|
86
|
+
return deckRoot(ws);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Ask the open workspace page to show this deck (SSE `focus`); false when it could not be told. */
|
|
90
|
+
async function focusDeck(deckId) {
|
|
91
|
+
try {
|
|
92
|
+
const { status } = await daemonRequest(`/decks/${deckId}/attention`, { method: "POST", body: { focus: true }, timeoutMs: 3000 });
|
|
93
|
+
return status === 200;
|
|
94
|
+
} catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function deckUrl(base, deckId) {
|
|
100
|
+
return `${base}/deck/${deckId}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* A signed-in browser link into the live workspace, or null when none is live:
|
|
105
|
+
* `<url>[/deck/<id>[/plan|/preview|/tasks/<id>]]#bootstrap=<key>`. The key is
|
|
106
|
+
* the workspace's link key (auth.mjs): the link brings the user there every
|
|
107
|
+
* time it is clicked, in any browser, and never expires. Asking for it is no
|
|
108
|
+
* new trust boundary: whoever runs this already holds the CLI bearer from the
|
|
109
|
+
* user-private config dir. A question's link names its VIEW, never the task, so
|
|
110
|
+
* it stays right when the agent asks again.
|
|
111
|
+
*/
|
|
112
|
+
export async function browserUrl({ deckId = null, taskId = null, view = null } = {}) {
|
|
113
|
+
const d = readDescriptor();
|
|
114
|
+
if (!d || d.malformed) return null;
|
|
115
|
+
let answer;
|
|
116
|
+
try {
|
|
117
|
+
answer = await daemonRequest("/bootstrap", { method: "POST", timeoutMs: 3000 });
|
|
118
|
+
} catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
const body = answer.status === 200 ? answer.body : null;
|
|
122
|
+
if (!body?.token) throw Object.assign(new Error(`workspace refused a sign-in link (HTTP ${answer.status})`), { code: "workspace_start_failed" });
|
|
123
|
+
const p = deckId ? `/deck/${deckId}${view ? `/${view}` : taskId ? `/tasks/${taskId}` : ""}` : "/";
|
|
124
|
+
return `${d.url}${p}#bootstrap=${body.token}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function defaultOpen(url) {
|
|
128
|
+
const { openInBrowser } = await import("../builders/build.mjs");
|
|
129
|
+
return openInBrowser(url, { quiet: true });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function cmdWorkspace(rest, { open = defaultOpen, write = null } = {}) {
|
|
133
|
+
let flags;
|
|
134
|
+
try {
|
|
135
|
+
const parsed = parse(rest);
|
|
136
|
+
flags = parsed.flags;
|
|
137
|
+
if (write) flags.write = write; // test seam: capture stdout without patching the stream
|
|
138
|
+
const [sub, ...args] = parsed.positional;
|
|
139
|
+
switch (sub) {
|
|
140
|
+
case "status": {
|
|
141
|
+
// A headless run (gates auto-approve: no human is there to review)
|
|
142
|
+
// has no workspace to wait for — the agent goes on without one.
|
|
143
|
+
const headless = process.env.SLIDEMIND_GATE_AUTOAPPROVE === "1";
|
|
144
|
+
const s = { ...(await workspaceStatus()), ...(headless ? { headless: true } : {}) };
|
|
145
|
+
emit(flags, s, s.running ? `workspace running at ${s.url} (pid ${s.pid}, ${s.pendingJobs} pending job(s))` : headless ? "workspace is not running — headless run (gates auto-approve): continue without it" : "workspace is not running");
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
case "start": {
|
|
149
|
+
// `slidemind workspace start` — what the login item runs (autostart.mjs): start
|
|
150
|
+
// the workspace unless it runs, silently, and never open a browser
|
|
151
|
+
const run = await ensureRunning({ note: () => {} });
|
|
152
|
+
emit(flags, { url: run.url, started: run.started, pid: run.pid, protocolVersion: run.protocolVersion }, `${run.started ? "started" : "running"} — workspace at ${run.url}`);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
case "stop": {
|
|
156
|
+
const r = await stopWorkspace();
|
|
157
|
+
emit(flags, r, r.stopped ? `workspace stopped (${r.pendingJobs} pending job(s) kept on disk)` : "workspace was not running");
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
case "add": {
|
|
161
|
+
if (!args[0]) throw Object.assign(new Error("usage: slidemind workspace add <path>"), { code: "usage" });
|
|
162
|
+
const entry = await registerDeck(args[0]);
|
|
163
|
+
const s = await workspaceStatus();
|
|
164
|
+
const url = s.running ? deckUrl(s.url, entry.id) : null;
|
|
165
|
+
emit(flags, { deckId: entry.id, url, path: entry.path, created: entry.created }, `${entry.created ? "registered" : "already registered"} ${entry.path}${url ? `\n${url}` : ""}`);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
case "discover": {
|
|
169
|
+
if (!args[0]) throw Object.assign(new Error("usage: slidemind workspace discover <root>"), { code: "usage" });
|
|
170
|
+
// ponytail: scans in this process until done; the API lane can hand this to the daemon's job route.
|
|
171
|
+
const { jobId, done } = startDiscovery(args[0]);
|
|
172
|
+
const job = await done;
|
|
173
|
+
emit(flags, { jobId, state: job.state, found: job.found.length, warnings: job.warnings }, `${job.state}: ${job.found.length} deck(s) under ${job.root}${job.warnings.length ? ` (${job.warnings.length} warning(s))` : ""}`);
|
|
174
|
+
if (job.state === "failed") process.exitCode = 1;
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
case "open-remote": {
|
|
178
|
+
if (!args[0]) throw Object.assign(new Error("usage: slidemind workspace open-remote <url> [--deck <path>] [--backend <env>]"), { code: "usage" });
|
|
179
|
+
const deckId = flags.deck ? addDeck(flags.deck).id : null;
|
|
180
|
+
finishJob(flags, await resolveRemoteLink({ url: args[0], backendURL: flags.backend ?? null, deckId }));
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
case "select": {
|
|
184
|
+
if (!args[0] || !flags.selection || !flags.choice) throw Object.assign(new Error("usage: slidemind workspace select <jobId> --selection <id> --choice <id> [--deck <path>]"), { code: "usage" });
|
|
185
|
+
const localTarget = flags.deck ? addDeck(flags.deck).id : null;
|
|
186
|
+
finishJob(flags, await selectChoice(args[0], { selectionId: flags.selection, choiceId: flags.choice, localTarget }));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
case "connect": {
|
|
190
|
+
if (!args[0] || !flags.folder) throw Object.assign(new Error("usage: slidemind workspace connect <deck> --folder <url> [--name <file.pptx>] [--backend <env>]"), { code: "usage" });
|
|
191
|
+
const entry = addDeck(args[0]);
|
|
192
|
+
const r = await connectDestination({ deckRoot: entry.path, folderRef: flags.folder, pptxName: flags.name ?? null, backendURL: flags.backend ?? null });
|
|
193
|
+
// a deck built before its folder was connected: that build goes up now
|
|
194
|
+
const upload = r.shouldPublish ? await uploadToDestination(entry.path) : null;
|
|
195
|
+
const remote = await remoteStatus(entry.path);
|
|
196
|
+
const uploaded = upload ? (upload.state === "done" ? `\n uploaded ${r.destination.pptxName} there` : `\n ${r.destination.pptxName} was not uploaded (${upload.reason}) — \`slidemind publish ${entry.path}\` retries`) : "";
|
|
197
|
+
emit(flags, { deckId: entry.id, ...r, upload, remote }, `${entry.name}: destination ${r.destination.folderName ?? r.destination.webUrl} (${r.destination.state}, plan writes ${r.writes}, presentation writes ${r.publishWrites})${r.shouldCreatePlan ? "\n the plan will be shared there on the next sync" : ""}${uploaded}${r.publishWrites === "disabled" ? "\n this deployment does not save presentations to SharePoint" : "\n every build-pptx publishes the presentation there"}`);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
case "list": {
|
|
201
|
+
const { decks, total } = listDecks();
|
|
202
|
+
emit(flags, { total, decks }, decks.map((d) => `${d.missing ? "! " : " "}${d.name}\t${d.path}`).join("\n") || "no registered decks");
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
case undefined:
|
|
206
|
+
default: {
|
|
207
|
+
// `slidemind workspace [<deck>]` — an agent runs this when the user asks
|
|
208
|
+
// to see the workspace; it starts or reuses the daemon and never strands
|
|
209
|
+
// the user in a terminal.
|
|
210
|
+
const deckArg = sub;
|
|
211
|
+
const run = await ensureRunning({ foreground: flags.foreground });
|
|
212
|
+
let deckId = null;
|
|
213
|
+
if (deckArg) deckId = (await registerDeck(deckRootArg(deckArg))).id;
|
|
214
|
+
const plain = deckId ? deckUrl(run.url, deckId) : run.url;
|
|
215
|
+
// A page already connected is the user's workspace: no second tab per call.
|
|
216
|
+
let pages = 0;
|
|
217
|
+
try {
|
|
218
|
+
pages = Number((await workspaceStatus()).pages) || 0;
|
|
219
|
+
} catch {
|
|
220
|
+
pages = 0;
|
|
221
|
+
}
|
|
222
|
+
// One link for both: the tab we open and the line we print for the user to
|
|
223
|
+
// click, in this browser or any other, as often as they like.
|
|
224
|
+
// `url` stays the plain base other commands build on; `link` is the one to click.
|
|
225
|
+
const link = (await browserUrl({ deckId })) ?? plain;
|
|
226
|
+
let opened = false;
|
|
227
|
+
let focused = false;
|
|
228
|
+
if (flags.open && (run.started || pages === 0)) opened = (await open(link)) !== false;
|
|
229
|
+
else if (flags.open && deckId && pages > 0) focused = await focusDeck(deckId);
|
|
230
|
+
const where = opened
|
|
231
|
+
? "opened in a new browser tab"
|
|
232
|
+
: focused
|
|
233
|
+
? "already open in the user's browser — it now shows this deck (no new tab)"
|
|
234
|
+
: pages > 0
|
|
235
|
+
? "already open in the user's browser (no new tab)"
|
|
236
|
+
: flags.open
|
|
237
|
+
? "could NOT open a browser tab — give the user the link"
|
|
238
|
+
: "not opened (--no-open)";
|
|
239
|
+
emit(
|
|
240
|
+
flags,
|
|
241
|
+
{ url: plain, link, deckId, started: run.started, opened, focused, pages, protocolVersion: run.protocolVersion },
|
|
242
|
+
`${run.started ? "started" : "reusing"} workspace — ${where}\nWorkspace: ${link}`,
|
|
243
|
+
);
|
|
244
|
+
if (run.daemon) {
|
|
245
|
+
process.stderr.write("slidemind: workspace running in the foreground; Ctrl-C drains and stops it\n");
|
|
246
|
+
await run.daemon.stopped;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
} catch (err) {
|
|
251
|
+
const code = err?.code ?? "error";
|
|
252
|
+
const exit = code === "usage" ? 1 : EXIT_BY_CODE[code] ?? 1;
|
|
253
|
+
if (flags?.json) {
|
|
254
|
+
process.stdout.write(JSON.stringify({ ok: false, error: { code, message: err.message, retryable: exit === 2 } }) + "\n");
|
|
255
|
+
} else {
|
|
256
|
+
process.stderr.write(`slidemind: ${err.message}\n`);
|
|
257
|
+
}
|
|
258
|
+
process.exitCode = exit;
|
|
259
|
+
}
|
|
260
|
+
}
|
package/src/cli/workspace.mjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
|
-
import {
|
|
4
|
+
import { requireAuthorizedEdits } from "./revision.mjs";
|
|
5
|
+
import { fail, requireReviewApproved, requireSlidesDraftChecked, requireSlidesPreserved, LOCAL_PPTX_RE, resolveDecksDir, resolveDeckArg, registerDeckHook, splitDeckArg, renderProgressPrinter, freshenDeckBundle, loadBridgeModule, SRC_DIR } from "./common.mjs";
|
|
6
|
+
import { WORKSPACE_DIRNAME } from "../lib/deck-paths.mjs";
|
|
5
7
|
import { readDesignName } from "./designs.mjs";
|
|
6
8
|
|
|
7
9
|
// `slidemind adopt <file.pptx> [--deck <slug>] [--brand <presFP>] [--no-convert]`
|
|
@@ -11,11 +13,34 @@ import { readDesignName } from "./designs.mjs";
|
|
|
11
13
|
// conversion route where it is available (`docs/slidemind-cli-connect/
|
|
12
14
|
// 14-foreign-slide-conversion.md`); whatever it declines folds in as native
|
|
13
15
|
// reused .pptx. See also intern-slidemind docs/deck-embedded-source-spec.md.
|
|
16
|
+
// "2,7,10" / "2-4,9" / "all" → [2,3,4,9] (1-based play order), or null for all.
|
|
17
|
+
function parseSlideSelection(raw) {
|
|
18
|
+
if (!raw || raw === "all") return null;
|
|
19
|
+
const out = new Set();
|
|
20
|
+
for (const chunk of String(raw).split(",")) {
|
|
21
|
+
const range = /^(\d+)\s*-\s*(\d+)$/.exec(chunk.trim());
|
|
22
|
+
if (range) {
|
|
23
|
+
const [from, to] = [Number(range[1]), Number(range[2])];
|
|
24
|
+
if (from < 1 || to < from) fail(`--slides: '${chunk.trim()}' is not a slide range`);
|
|
25
|
+
for (let n = from; n <= to; n++) out.add(n);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const n = Number(chunk.trim());
|
|
29
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
30
|
+
fail(`--slides expects 1-based slide numbers like '2,7' or '2-4', got '${chunk.trim()}'`);
|
|
31
|
+
}
|
|
32
|
+
out.add(n);
|
|
33
|
+
}
|
|
34
|
+
return [...out].sort((a, b) => a - b);
|
|
35
|
+
}
|
|
36
|
+
|
|
14
37
|
export async function cmdAdopt(rest) {
|
|
15
38
|
const args = [...rest];
|
|
16
39
|
let deckSlug = null;
|
|
17
40
|
let brand = null;
|
|
18
41
|
let convert = true;
|
|
42
|
+
let slides = null;
|
|
43
|
+
let inPlace = false;
|
|
19
44
|
const positional = [];
|
|
20
45
|
for (let i = 0; i < args.length; i++) {
|
|
21
46
|
const a = args[i];
|
|
@@ -25,8 +50,14 @@ export async function cmdAdopt(rest) {
|
|
|
25
50
|
} else if (a === "--brand") {
|
|
26
51
|
brand = args[++i] ?? null;
|
|
27
52
|
if (!brand) fail("--brand expects a design id (presFP)");
|
|
53
|
+
} else if (a === "--slides") {
|
|
54
|
+
const next = args[++i] ?? null;
|
|
55
|
+
if (!next) fail("--slides expects slide numbers, e.g. --slides 2,7 or --slides 2-4");
|
|
56
|
+
slides = parseSlideSelection(next);
|
|
28
57
|
} else if (a === "--no-convert") {
|
|
29
58
|
convert = false;
|
|
59
|
+
} else if (a === "--in-place") {
|
|
60
|
+
inPlace = true;
|
|
30
61
|
} else if (a.startsWith("-")) {
|
|
31
62
|
fail(`unknown flag: ${a}`);
|
|
32
63
|
} else {
|
|
@@ -35,20 +66,30 @@ export async function cmdAdopt(rest) {
|
|
|
35
66
|
}
|
|
36
67
|
const pptxArg = positional[0];
|
|
37
68
|
if (!pptxArg) {
|
|
38
|
-
fail(
|
|
69
|
+
fail(
|
|
70
|
+
"usage: slidemind adopt <file.pptx> [--deck <slug>] [--brand <presFP>] [--slides <sel>] [--no-convert]\n" +
|
|
71
|
+
" slidemind adopt <deck>/.slidemind/slides/<NN-name>.pptx --in-place [--brand <presFP>]",
|
|
72
|
+
);
|
|
39
73
|
}
|
|
40
74
|
// v1 is FILE decks only. A presId (or any non-file argument) would mean
|
|
41
75
|
// converting a LIVE presentation, which needs slide-export and tag-read
|
|
42
76
|
// events the relay does not have — say so instead of "no such file".
|
|
43
|
-
const {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
convertUntaggedSlides,
|
|
47
|
-
readSourceDeck,
|
|
48
|
-
} = await import(pathToFileURL(path.join(SRC_DIR, "lib", "adopt.mjs")).href);
|
|
77
|
+
const { LIVE_DECK_UNSUPPORTED, adoptWorkspace } = await import(
|
|
78
|
+
pathToFileURL(path.join(SRC_DIR, "lib", "adopt.mjs")).href
|
|
79
|
+
);
|
|
49
80
|
if (!LOCAL_PPTX_RE.test(pptxArg)) fail(LIVE_DECK_UNSUPPORTED);
|
|
50
81
|
const pptxPath = path.resolve(pptxArg);
|
|
51
82
|
if (!fs.existsSync(pptxPath)) fail(`no such file: ${pptxArg}`);
|
|
83
|
+
// A folded slide of an existing deck (`<deck>/.slidemind/slides/NN-*.pptx`, from
|
|
84
|
+
// add-slide) converts in place only when asked by name: inferring it from
|
|
85
|
+
// the path would turn any `slides/` folder on disk into a deck and replace
|
|
86
|
+
// the user's file.
|
|
87
|
+
if (inPlace) {
|
|
88
|
+
if (deckSlug || slides || !convert) {
|
|
89
|
+
fail("--in-place converts one folded slide on its own: `slidemind adopt <deck>/.slidemind/slides/<NN-name>.pptx --in-place [--brand <presFP>]`");
|
|
90
|
+
}
|
|
91
|
+
return adoptFoldedSlide(pptxPath, brand);
|
|
92
|
+
}
|
|
52
93
|
const slug = deckSlug ?? path.basename(pptxPath).replace(/\.pptx$/i, "");
|
|
53
94
|
// The decks dir, not a hardcoded ./decks — otherwise `adopt` writes where no
|
|
54
95
|
// other subcommand looks whenever SLIDEMIND_DECKS is set. A RELATIVE slug
|
|
@@ -56,41 +97,97 @@ export async function cmdAdopt(rest) {
|
|
|
56
97
|
// every other untrusted name in this feature is escape-guarded, and this one
|
|
57
98
|
// rides agent-authored command lines.
|
|
58
99
|
const decksRoot = path.resolve(resolveDecksDir());
|
|
59
|
-
const
|
|
100
|
+
const rootDir = path.isAbsolute(slug) ? slug : path.resolve(decksRoot, slug);
|
|
101
|
+
// The deck ROOT keeps only the deliverables; the adopted workspace is nested.
|
|
102
|
+
// An existing flat deck (no `.slidemind/`) is adopted into where it already is.
|
|
103
|
+
const deckDir = fs.existsSync(path.join(rootDir, "slides"))
|
|
104
|
+
? rootDir
|
|
105
|
+
: path.join(rootDir, WORKSPACE_DIRNAME);
|
|
60
106
|
if (
|
|
61
107
|
!path.isAbsolute(slug) &&
|
|
62
|
-
(
|
|
108
|
+
(rootDir === decksRoot || !rootDir.startsWith(decksRoot + path.sep))
|
|
63
109
|
) {
|
|
64
110
|
fail(
|
|
65
111
|
`--deck '${slug}' escapes the decks directory (${decksRoot}) — ` +
|
|
66
112
|
"pass an absolute path to adopt somewhere else on purpose",
|
|
67
113
|
);
|
|
68
114
|
}
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
115
|
+
// Adoption is repeatable, so it runs under the deck's own ownership: a second
|
|
116
|
+
// `adopt` while a session is mid-edit must queue behind it, not race it.
|
|
117
|
+
fs.mkdirSync(deckDir, { recursive: true });
|
|
118
|
+
const { acquireOwner } = await import(
|
|
119
|
+
pathToFileURL(path.join(SRC_DIR, "lib", "revision.mjs")).href
|
|
120
|
+
);
|
|
121
|
+
let held;
|
|
122
|
+
try {
|
|
123
|
+
held = acquireOwner(deckDir, { command: "adopt" });
|
|
124
|
+
} catch (err) {
|
|
125
|
+
if (err?.code === "deck_busy") fail(err.message, 2);
|
|
126
|
+
throw err;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let outcome;
|
|
130
|
+
try {
|
|
131
|
+
outcome = await adoptWorkspace({
|
|
132
|
+
pptxPath,
|
|
133
|
+
deckDir,
|
|
134
|
+
brandId: brand,
|
|
135
|
+
slides,
|
|
136
|
+
convert,
|
|
137
|
+
owner: held.token,
|
|
138
|
+
note: (line) => process.stderr.write(`${line}\n`),
|
|
139
|
+
});
|
|
140
|
+
} catch (err) {
|
|
141
|
+
if (err?.code === "needs_reconcile") fail(err.message, 2);
|
|
142
|
+
throw err;
|
|
143
|
+
} finally {
|
|
144
|
+
held.release();
|
|
145
|
+
}
|
|
146
|
+
registerDeckHook(deckDir);
|
|
147
|
+
|
|
82
148
|
// A decks dir outside the cwd (SLIDEMIND_DECKS) relativizes to ../../.. noise
|
|
83
149
|
// — print the absolute path there instead.
|
|
84
|
-
const shown = path.relative(process.cwd(),
|
|
150
|
+
const shown = path.relative(process.cwd(), rootDir);
|
|
151
|
+
const where = shown.startsWith("..") ? rootDir : shown;
|
|
152
|
+
|
|
153
|
+
if (outcome.mode === "unchanged") {
|
|
154
|
+
process.stdout.write(
|
|
155
|
+
`${slug} is already adopted from this exact file — nothing was rewritten.\n` +
|
|
156
|
+
`Hydrate more slides to editable jsx with \`slidemind adopt ${pptxArg} --deck ${slug} --slides <n,m>\`.\n`,
|
|
157
|
+
);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (outcome.mode === "hydrated") {
|
|
161
|
+
process.stdout.write(
|
|
162
|
+
`Hydrated ${outcome.hydrated.length} slide(s) of ${slug} to editable jsx: ${outcome.hydrated.join(", ")}\n` +
|
|
163
|
+
`The rest of the deck, and any edits in progress, were left untouched.\n`,
|
|
164
|
+
);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (outcome.mode === "rebased") {
|
|
168
|
+
const r = outcome.rebase;
|
|
169
|
+
process.stdout.write(
|
|
170
|
+
`${slug} was already adopted, so this file was reconciled onto it (generation ${r.generation}):\n` +
|
|
171
|
+
` refreshed from the newer file: ${r.refreshed.length}\n` +
|
|
172
|
+
` removed (deleted there): ${r.removed.length}\n` +
|
|
173
|
+
(r.stillDirty.length ? ` still unpublished locally: ${r.stillDirty.join(", ")}\n` : ""),
|
|
174
|
+
);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const result = outcome.result;
|
|
85
179
|
process.stdout.write(
|
|
86
180
|
`Adopted ${result.adopted.length} slide(s) with embedded source` +
|
|
87
181
|
(result.converted.length
|
|
88
|
-
? `, converted ${result.converted.length}
|
|
182
|
+
? `, converted ${result.converted.length} slide(s) from their CURRENT content to editable jsx`
|
|
89
183
|
: "") +
|
|
90
184
|
(result.native.length
|
|
91
|
-
? ` and
|
|
185
|
+
? ` and kept ${result.native.length} slide(s) as lossless native .pptx`
|
|
186
|
+
: "") +
|
|
187
|
+
` into ${where}/ (workspace ${path.basename(deckDir) === WORKSPACE_DIRNAME ? `${WORKSPACE_DIRNAME}/` : "."}).\n` +
|
|
188
|
+
(slides
|
|
189
|
+
? `Only slide(s) ${slides.join(", ")} were hydrated; the rest keep their notes, comments and native objects verbatim.\n`
|
|
92
190
|
: "") +
|
|
93
|
-
` into ${shown.startsWith("..") ? deckDir : shown}/.\n` +
|
|
94
191
|
(result.skipped.length
|
|
95
192
|
? `${result.skipped.length} slide(s) stayed native: ` +
|
|
96
193
|
`${result.skipped
|
|
@@ -100,10 +197,53 @@ export async function cmdAdopt(rest) {
|
|
|
100
197
|
(result.bundle?.presFP && result.bundle.presFP !== "unknown"
|
|
101
198
|
? `Deck bound to bundle ${result.bundle.presFP}.\n`
|
|
102
199
|
: `No bundle reference found in the file — run 'slidemind download-bundle ${slug}' before rendering.\n`) +
|
|
200
|
+
`The file you adopted is the accepted base; a build proposes a candidate and never replaces it ` +
|
|
201
|
+
`until \`slidemind revision accept ${slug} --candidate <id>\`.\n` +
|
|
103
202
|
`Edit slides/*.jsx and run 'slidemind push ${slug}' or 'slidemind build-pptx ${slug}'.\n`,
|
|
104
203
|
);
|
|
105
204
|
}
|
|
106
205
|
|
|
206
|
+
async function adoptFoldedSlide(pptxPath, brand) {
|
|
207
|
+
const deckDir = path.dirname(path.dirname(pptxPath));
|
|
208
|
+
const { convertFoldedSlide, notFoldedReason } = await import(
|
|
209
|
+
pathToFileURL(path.join(SRC_DIR, "lib", "convert-folded.mjs")).href
|
|
210
|
+
);
|
|
211
|
+
const notFolded = notFoldedReason(pptxPath);
|
|
212
|
+
if (notFolded) fail(notFolded, 2);
|
|
213
|
+
const { acquireOwner, journalIncomplete } = await import(
|
|
214
|
+
pathToFileURL(path.join(SRC_DIR, "lib", "revision.mjs")).href
|
|
215
|
+
);
|
|
216
|
+
const unfinished = journalIncomplete(deckDir);
|
|
217
|
+
if (unfinished) fail(`this deck has an unfinished ${unfinished.op}; finish it before converting a slide.`);
|
|
218
|
+
let held;
|
|
219
|
+
try {
|
|
220
|
+
held = acquireOwner(deckDir, { command: "adopt" });
|
|
221
|
+
} catch (err) {
|
|
222
|
+
if (err?.code === "deck_busy") fail(err.message, 2);
|
|
223
|
+
throw err;
|
|
224
|
+
}
|
|
225
|
+
let out;
|
|
226
|
+
try {
|
|
227
|
+
out = await convertFoldedSlide({
|
|
228
|
+
pptxPath,
|
|
229
|
+
deckDir,
|
|
230
|
+
brandId: brand,
|
|
231
|
+
owner: held.token,
|
|
232
|
+
note: (line) => process.stderr.write(`${line}\n`),
|
|
233
|
+
});
|
|
234
|
+
} catch (err) {
|
|
235
|
+
if (["not_folded", "not_one_slide", "local_dirty", "not_converted"].includes(err?.code)) fail(err.message, 2);
|
|
236
|
+
throw err;
|
|
237
|
+
} finally {
|
|
238
|
+
held.release();
|
|
239
|
+
}
|
|
240
|
+
process.stdout.write(
|
|
241
|
+
`Converted ${out.replaced} to editable jsx in place: slides/${out.file} (same play position; the .pptx is gone).\n` +
|
|
242
|
+
(out.route === "source" ? "Its embedded slidemind source was restored verbatim.\n" : "It is converted-dialect jsx: edit it per workflow/edit.md.\n") +
|
|
243
|
+
(out.warnings.length ? `Notes: ${out.warnings.join("; ")}\n` : ""),
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
107
247
|
export async function cmdPush(rest) {
|
|
108
248
|
const { deck, passthrough } = splitDeckArg(rest, "push");
|
|
109
249
|
let to = null;
|
|
@@ -149,6 +289,10 @@ export async function cmdPush(rest) {
|
|
|
149
289
|
fail(`expected 'slides/' folder in ${deckDir}`);
|
|
150
290
|
}
|
|
151
291
|
await requireReviewApproved(deckDir);
|
|
292
|
+
requireAuthorizedEdits(deckDir);
|
|
293
|
+
await requireSlidesPreserved(deckDir);
|
|
294
|
+
// A push renders the whole deck, so every authored slide needs its check.
|
|
295
|
+
await requireSlidesDraftChecked(deckDir);
|
|
152
296
|
await freshenDeckBundle(deckDir);
|
|
153
297
|
|
|
154
298
|
const mod = await import(
|
|
@@ -164,9 +308,18 @@ export async function cmdPush(rest) {
|
|
|
164
308
|
force,
|
|
165
309
|
onProgress: renderProgressPrinter(),
|
|
166
310
|
});
|
|
167
|
-
|
|
311
|
+
// Classed by the shared draft-line table, as build-pptx classes them.
|
|
312
|
+
const { splitPackageLines, packageDefectsText } = await import(
|
|
313
|
+
pathToFileURL(path.join(SRC_DIR, "builders", "draft-check.mjs")).href
|
|
314
|
+
);
|
|
315
|
+
const lines = splitPackageLines(result.warnings);
|
|
316
|
+
for (const w of lines.notes) {
|
|
168
317
|
process.stderr.write(` warning: ${w}\n`);
|
|
169
318
|
}
|
|
319
|
+
if (lines.defects.length > 0) {
|
|
320
|
+
process.exitCode = 1;
|
|
321
|
+
process.stdout.write(packageDefectsText(lines.defects, deck));
|
|
322
|
+
}
|
|
170
323
|
for (const s of result.skippedSlides ?? []) {
|
|
171
324
|
process.stderr.write(` skipped ${s.stem}: ${s.reason}\n`);
|
|
172
325
|
}
|
|
@@ -188,10 +341,16 @@ export async function cmdNew(rest) {
|
|
|
188
341
|
let to = null;
|
|
189
342
|
let brand = null;
|
|
190
343
|
let noBind = false;
|
|
344
|
+
// Every deck command takes --json; agents reach for it here too (a failed
|
|
345
|
+
// `new --json` cost a round in three sim sessions). The human lines still
|
|
346
|
+
// print; the payload is appended for the parser.
|
|
347
|
+
let asJson = false;
|
|
191
348
|
const positional = [];
|
|
192
349
|
for (let i = 0; i < rest.length; i++) {
|
|
193
350
|
const a = rest[i];
|
|
194
|
-
if (a === "--
|
|
351
|
+
if (a === "--json") {
|
|
352
|
+
asJson = true;
|
|
353
|
+
} else if (a === "--to") {
|
|
195
354
|
to = rest[++i];
|
|
196
355
|
if (!to) fail("--to expects a presId");
|
|
197
356
|
} else if (a === "--brand") {
|
|
@@ -209,15 +368,18 @@ export async function cmdNew(rest) {
|
|
|
209
368
|
fail("--brand and --to are mutually exclusive (a design id or a window, not both)");
|
|
210
369
|
}
|
|
211
370
|
const name = positional[0];
|
|
212
|
-
if (!name) fail("usage: slidemind new <name> [--brand <presFP> | --to <presId>] [--no-bind]");
|
|
371
|
+
if (!name) fail("usage: slidemind new <name> [--brand <presFP> | --to <presId>] [--no-bind] [--json]");
|
|
213
372
|
if (!/^[a-z0-9][a-z0-9-]*$/i.test(name)) {
|
|
214
373
|
fail(`deck name must be kebab-case alphanumerics, got '${name}'`);
|
|
215
374
|
}
|
|
216
375
|
const decksDir = resolveDecksDir();
|
|
217
|
-
const
|
|
218
|
-
if (fs.existsSync(
|
|
219
|
-
fail(`deck already exists: ${
|
|
376
|
+
const rootDir = path.join(decksDir, name);
|
|
377
|
+
if (fs.existsSync(rootDir)) {
|
|
378
|
+
fail(`deck already exists: ${rootDir}`);
|
|
220
379
|
}
|
|
380
|
+
// Two halves from the first command: the root holds what the user will want
|
|
381
|
+
// (the .pptx, plan.html), `.slidemind/` everything the CLI works with.
|
|
382
|
+
const deckDir = path.join(rootDir, WORKSPACE_DIRNAME);
|
|
221
383
|
fs.mkdirSync(path.join(deckDir, "slides"), { recursive: true });
|
|
222
384
|
fs.writeFileSync(
|
|
223
385
|
path.join(deckDir, "plan.md"),
|
|
@@ -226,10 +388,26 @@ export async function cmdNew(rest) {
|
|
|
226
388
|
`the slidemind-create-slides skill fills this in, then writes slides under ./slides/\n` +
|
|
227
389
|
`as NN-<topic>.jsx (zero-padded prefix — slide order is filename order).\n`,
|
|
228
390
|
);
|
|
229
|
-
|
|
391
|
+
const { writePlanPage } = await import(
|
|
392
|
+
pathToFileURL(path.join(SRC_DIR, "builders", "plan.mjs")).href
|
|
393
|
+
);
|
|
394
|
+
writePlanPage(deckDir);
|
|
395
|
+
registerDeckHook(deckDir);
|
|
396
|
+
process.stdout.write(`Created ${path.relative(process.cwd(), rootDir)} (workspace ${WORKSPACE_DIRNAME}/)\n`);
|
|
230
397
|
process.stdout.write(
|
|
231
398
|
`Next: ask your agent to author "${name}", then \`slidemind view-draft ${name}\`\n`,
|
|
232
399
|
);
|
|
400
|
+
if (asJson) {
|
|
401
|
+
process.on("exit", () => {
|
|
402
|
+
let bound = null;
|
|
403
|
+
try {
|
|
404
|
+
bound = JSON.parse(fs.readFileSync(path.join(deckDir, "state", "bundle-ref.json"), "utf8"));
|
|
405
|
+
} catch {}
|
|
406
|
+
process.stdout.write(
|
|
407
|
+
`${JSON.stringify({ deck: name, dir: rootDir, workspace: deckDir, bound: bound?.presFP ?? null })}\n`,
|
|
408
|
+
);
|
|
409
|
+
});
|
|
410
|
+
}
|
|
233
411
|
|
|
234
412
|
// Stateless auto-bind: bind the fresh deck to whatever design is OPEN so the
|
|
235
413
|
// agent skips a follow-up `download-bundle`. Best-effort — a bind miss (no
|