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,686 @@
|
|
|
1
|
+
// `slidemind revision <sub>` and `slidemind rebase <deck>` — the user-facing
|
|
2
|
+
// half of the local revision protocol (src/lib/revision.mjs).
|
|
3
|
+
//
|
|
4
|
+
// Two things live here that nothing else in the CLI does:
|
|
5
|
+
//
|
|
6
|
+
// • OWNERSHIP that outlives one command. A skill editing a deck writes raw
|
|
7
|
+
// .jsx files, runs several commands and fans work out to parallel lanes;
|
|
8
|
+
// a per-command lock protects none of that. `revision begin` hands back an
|
|
9
|
+
// owner token, the session exports it as SLIDEMIND_OWNER, and every
|
|
10
|
+
// command underneath re-enters under it instead of blocking.
|
|
11
|
+
// • ACCEPTANCE as a deliberate act. A build produces a candidate; it does not
|
|
12
|
+
// decide that the candidate is the deck. `revision accept --candidate <id>`
|
|
13
|
+
// is where a local-only deck moves its accepted base, and it refuses a
|
|
14
|
+
// stale one, a conflicted deck, and a deck bound to a remote copy (there,
|
|
15
|
+
// only a confirmed publish receipt may advance the base).
|
|
16
|
+
|
|
17
|
+
import fs from "node:fs";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import { randomUUID } from "node:crypto";
|
|
20
|
+
|
|
21
|
+
import { fail, resolveDeckArg, deckSlug, splitDeckArg, requireReviewApproved } from "./common.mjs";
|
|
22
|
+
import {
|
|
23
|
+
acquireOwner,
|
|
24
|
+
candidateStale,
|
|
25
|
+
journalIncomplete,
|
|
26
|
+
listCandidates,
|
|
27
|
+
readBusy,
|
|
28
|
+
readCandidate,
|
|
29
|
+
readConflicts,
|
|
30
|
+
readRevision,
|
|
31
|
+
releaseStaleOwner,
|
|
32
|
+
revisionStatus,
|
|
33
|
+
stageIncoming,
|
|
34
|
+
acceptCandidate,
|
|
35
|
+
} from "../lib/revision.mjs";
|
|
36
|
+
import {
|
|
37
|
+
SESSION_OPERATION_KINDS,
|
|
38
|
+
applyRequirementAmendment,
|
|
39
|
+
checkRedesignTransition,
|
|
40
|
+
closeOpenOperations,
|
|
41
|
+
migrateEditState,
|
|
42
|
+
openOperations,
|
|
43
|
+
openRedesigns,
|
|
44
|
+
parseSlideSelection,
|
|
45
|
+
recordOperation,
|
|
46
|
+
resolveSlideTargets,
|
|
47
|
+
} from "../lib/edit-state.mjs";
|
|
48
|
+
import { EDIT_OPERATION_VERSION } from "../lib/edit-operations.mjs";
|
|
49
|
+
import { editTransitionRefusals } from "../lib/edit-transitions.mjs";
|
|
50
|
+
|
|
51
|
+
const json = (data) => process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Refuse a deck whose slides changed in a way the open scopes do not authorize —
|
|
55
|
+
* another slide or a sibling beside a target, an import's pragma dropped outside
|
|
56
|
+
* a redesign. The add-in refuses the same transitions at the write; here the
|
|
57
|
+
* first command that reads the deck does (view-draft, build-pptx, push).
|
|
58
|
+
*/
|
|
59
|
+
export function requireAuthorizedEdits(deckDir) {
|
|
60
|
+
const refusals = editTransitionRefusals(deckDir);
|
|
61
|
+
if (refusals.length === 0) return;
|
|
62
|
+
fail(`nothing was rendered — this change is outside what the edit was authorized to do:\n${refusals.map((r) => ` ${r}`).join("\n")}`, 2);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function requireAdopted(deckDir) {
|
|
66
|
+
const revision = readRevision(deckDir);
|
|
67
|
+
if (!revision) {
|
|
68
|
+
fail(
|
|
69
|
+
`${deckSlug(deckDir)} has no accepted base — it was authored here rather than adopted from a .pptx. ` +
|
|
70
|
+
`\`slidemind adopt <file.pptx> --deck ${deckSlug(deckDir)}\` establishes one.`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
return revision;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Every open redesign on this deck must have replaced its target, not sat next
|
|
77
|
+
// to it. Reports every violation at once — an agent that has to be told one
|
|
78
|
+
// problem per run will fix them one per run.
|
|
79
|
+
function assertRedesignsReplaced(deckDir) {
|
|
80
|
+
for (const operation of openRedesigns(deckDir)) {
|
|
81
|
+
const { ok, violations } = checkRedesignTransition(deckDir, operation);
|
|
82
|
+
if (ok) continue;
|
|
83
|
+
fail(
|
|
84
|
+
`redesign ${operation.operationId} did not replace what it was scoped to:\n` +
|
|
85
|
+
violations.map((v) => ` [${v.code}] ${v.detail}`).join("\n"),
|
|
86
|
+
2,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Seed a preservation record for every rendered slide that has none, from its
|
|
93
|
+
* current evidence — the add-in seeds at every render the same way
|
|
94
|
+
* (`preservation-seed.ts`), so what a slide owes is checked from its first
|
|
95
|
+
* draft on, not only once a scope or an adopt happened to capture it.
|
|
96
|
+
*/
|
|
97
|
+
export function seedRenderedSlides(deckDir, files) {
|
|
98
|
+
const selection = (files ?? []).filter((f) => /\.jsx$/i.test(f));
|
|
99
|
+
if (selection.length === 0) return;
|
|
100
|
+
try {
|
|
101
|
+
migrateEditState(deckDir, { selection });
|
|
102
|
+
} catch {
|
|
103
|
+
// An unreadable record is never rewritten; the diagnostics name it and
|
|
104
|
+
// delivery refuses on it.
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// revision
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
export async function cmdRevision(rest) {
|
|
113
|
+
const sub = rest[0];
|
|
114
|
+
const args = rest.slice(1);
|
|
115
|
+
switch (sub) {
|
|
116
|
+
case "begin":
|
|
117
|
+
return revisionBegin(args);
|
|
118
|
+
case "finish":
|
|
119
|
+
return revisionFinish(args);
|
|
120
|
+
case "status":
|
|
121
|
+
return revisionStatusCmd(args);
|
|
122
|
+
case "accept":
|
|
123
|
+
return revisionAccept(args);
|
|
124
|
+
default:
|
|
125
|
+
fail(
|
|
126
|
+
`usage: slidemind revision <begin|finish|status|accept> <deck> [options]\n` +
|
|
127
|
+
` begin take local ownership of the deck for a whole edit session\n` +
|
|
128
|
+
` --operation ${SESSION_OPERATION_KINDS.join("|")} scope this session to one kind of edit\n` +
|
|
129
|
+
` --slides <sel> which slides it may touch — '2,7', '2-4', 'all' or a file name\n` +
|
|
130
|
+
` (required with --operation; resolved to durable slide ids at once)\n` +
|
|
131
|
+
` --scope <file.json> per-slide declared changes / live identity / repair budget\n` +
|
|
132
|
+
` --summary <text> what the user asked for, in one line\n` +
|
|
133
|
+
` --command <name> label for the ownership record (default: session)\n` +
|
|
134
|
+
` finish release it (work and candidates are kept; open edit scopes are closed)\n` +
|
|
135
|
+
` status accepted base, generation, dirty slides, candidates, staged snapshots, open scopes\n` +
|
|
136
|
+
` accept --candidate <id>: make a validated local candidate the accepted base\n` +
|
|
137
|
+
` --json machine-readable result (begin | status | accept)`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// A structured scope file is how an operation says more than "these slides":
|
|
143
|
+
// the declared changes, the live slide it was read from, a repair budget. Keyed
|
|
144
|
+
// by slide key, file name, stem or 1-based number — whatever the caller already
|
|
145
|
+
// has — and merged onto the resolved target.
|
|
146
|
+
const SCOPE_TARGET_KEYS = [
|
|
147
|
+
"liveSlideId",
|
|
148
|
+
"provenance",
|
|
149
|
+
"baseLiveFingerprint",
|
|
150
|
+
"sourceRevision",
|
|
151
|
+
"repairPolicy",
|
|
152
|
+
"changes",
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
function readScopeFile(file) {
|
|
156
|
+
if (!fs.existsSync(file)) fail(`no such file: ${file}`);
|
|
157
|
+
let parsed;
|
|
158
|
+
try {
|
|
159
|
+
parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
160
|
+
} catch (err) {
|
|
161
|
+
fail(`${file} is not readable JSON: ${err.message}`);
|
|
162
|
+
}
|
|
163
|
+
if (!parsed || typeof parsed !== "object") fail(`${file} must be a JSON object`);
|
|
164
|
+
const targets = parsed.targets ?? {};
|
|
165
|
+
if (typeof targets !== "object" || Array.isArray(targets)) {
|
|
166
|
+
fail(`${file}: "targets" must be an object keyed by slide id, file name or slide number`);
|
|
167
|
+
}
|
|
168
|
+
for (const [key, entry] of Object.entries(targets)) {
|
|
169
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
170
|
+
fail(`${file}: targets["${key}"] must be an object`);
|
|
171
|
+
}
|
|
172
|
+
for (const k of Object.keys(entry)) {
|
|
173
|
+
if (!SCOPE_TARGET_KEYS.includes(k)) {
|
|
174
|
+
fail(`${file}: targets["${key}"].${k} is not part of an operation target (${SCOPE_TARGET_KEYS.join(", ")})`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return { requestSummary: parsed.requestSummary ?? null, targets };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function revisionBegin(args) {
|
|
182
|
+
// Parsed here rather than through splitDeckArg: every flag below takes a
|
|
183
|
+
// value, and a positional-scanning split would read `--operation repair`'s
|
|
184
|
+
// value as the deck.
|
|
185
|
+
let deck = null;
|
|
186
|
+
let command = "session";
|
|
187
|
+
let asJson = false;
|
|
188
|
+
let operation = null;
|
|
189
|
+
let slides = null;
|
|
190
|
+
let scopeFile = null;
|
|
191
|
+
let summary = null;
|
|
192
|
+
for (let i = 0; i < args.length; i++) {
|
|
193
|
+
const a = args[i];
|
|
194
|
+
if (a === "--command") command = args[++i] ?? command;
|
|
195
|
+
else if (a === "--operation") operation = args[++i] ?? null;
|
|
196
|
+
else if (a === "--slides") slides = args[++i] ?? null;
|
|
197
|
+
else if (a === "--scope") scopeFile = args[++i] ?? null;
|
|
198
|
+
else if (a === "--summary") summary = args[++i] ?? null;
|
|
199
|
+
else if (a === "--json") asJson = true;
|
|
200
|
+
else if (a.startsWith("-")) fail(`unknown flag: ${a}`);
|
|
201
|
+
else if (deck === null) deck = a;
|
|
202
|
+
else fail(`unexpected argument: ${a}`);
|
|
203
|
+
}
|
|
204
|
+
if (!deck) {
|
|
205
|
+
fail(
|
|
206
|
+
`usage: slidemind revision begin <deck> [--operation ${SESSION_OPERATION_KINDS.join("|")} --slides <sel>]\n` +
|
|
207
|
+
` [--scope <file.json>] [--summary <text>] [--command <name>] [--json]`,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
if (operation && !SESSION_OPERATION_KINDS.includes(operation)) {
|
|
211
|
+
fail(
|
|
212
|
+
`--operation must be one of ${SESSION_OPERATION_KINDS.join(", ")} (got '${operation}'). ` +
|
|
213
|
+
`\`create\` is what a build infers for a genuinely new slide, not something a session claims.`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
// A scope with no slides is not a scope: it would silently claim the deck.
|
|
217
|
+
if (operation && !slides) fail(`--operation ${operation} needs --slides <sel> — the slides it may touch`);
|
|
218
|
+
if (!operation && (slides || scopeFile)) fail(`--slides/--scope describe an operation; add --operation <kind>`);
|
|
219
|
+
const scope = scopeFile ? readScopeFile(scopeFile) : null;
|
|
220
|
+
|
|
221
|
+
const deckDir = resolveDeckArg(deck, { allowUnfinished: true });
|
|
222
|
+
let held;
|
|
223
|
+
try {
|
|
224
|
+
held = acquireOwner(deckDir, { command: operation ? `${command} (${operation})` : command, session: true });
|
|
225
|
+
} catch (err) {
|
|
226
|
+
if (err?.code === "deck_busy") fail(err.message, 2);
|
|
227
|
+
throw err;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
let recorded = null;
|
|
231
|
+
let migration = null;
|
|
232
|
+
let amended = null;
|
|
233
|
+
if (operation) {
|
|
234
|
+
try {
|
|
235
|
+
// Capture what the slides owe BEFORE the first safeguarded edit: a deck
|
|
236
|
+
// that predates these records stays editable, its obligations read from
|
|
237
|
+
// the source actually on disk.
|
|
238
|
+
migration = migrateEditState(deckDir);
|
|
239
|
+
recorded = recordOperation(deckDir, buildOperation({ deckDir, kind: operation, slides, scope, summary }));
|
|
240
|
+
// A declared removal or addition amends the record HERE, against the base
|
|
241
|
+
// the scope was just written for — not at commit, when the source has
|
|
242
|
+
// moved and nothing can be tied to it any more.
|
|
243
|
+
amended = applyRequirementAmendment(deckDir, recorded);
|
|
244
|
+
} catch (err) {
|
|
245
|
+
// Nothing was opened, so nothing stays held: a session token for a scope
|
|
246
|
+
// that was refused would lock the deck behind an edit that never began.
|
|
247
|
+
held.release();
|
|
248
|
+
if (
|
|
249
|
+
[
|
|
250
|
+
"operation_conflict",
|
|
251
|
+
"operation_invalid",
|
|
252
|
+
"state_invalid",
|
|
253
|
+
"unsupported_version",
|
|
254
|
+
"state_unreadable",
|
|
255
|
+
"bad_selection",
|
|
256
|
+
"no_preservation_record",
|
|
257
|
+
"source_moved",
|
|
258
|
+
"source_missing",
|
|
259
|
+
].includes(err?.code)
|
|
260
|
+
) {
|
|
261
|
+
fail(err.message, 2);
|
|
262
|
+
}
|
|
263
|
+
throw err;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// The scope IS the unit of repair, and this command is the only thing that
|
|
268
|
+
// records one — so the next step is named here rather than left to be
|
|
269
|
+
// guessed. `--repair` runs the whole scope: it refuses a targeted-edit scope
|
|
270
|
+
// by name, and refuses `--only NN` inside a wider one (a partial run would
|
|
271
|
+
// mark the operation done for slides it never touched).
|
|
272
|
+
if (recorded?.kind === "repair") {
|
|
273
|
+
process.stderr.write(
|
|
274
|
+
`slidemind: run the repair inside this session — \`slidemind transcribe ${deck} --repair\` ` +
|
|
275
|
+
`(the whole scope; re-open with --slides NN to repair one slide alone).\n`,
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
const status = revisionStatus(deckDir);
|
|
279
|
+
if (asJson) {
|
|
280
|
+
// The token LAST: revisionStatus also reports an `owner` (who holds the
|
|
281
|
+
// deck), and spreading it over the token would hand the caller an object
|
|
282
|
+
// where it asked for a string.
|
|
283
|
+
json({
|
|
284
|
+
...status,
|
|
285
|
+
heldBy: status.owner,
|
|
286
|
+
owner: held.token,
|
|
287
|
+
nested: held.nested,
|
|
288
|
+
operation: recorded,
|
|
289
|
+
migration,
|
|
290
|
+
amended,
|
|
291
|
+
});
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
process.stdout.write(
|
|
295
|
+
`Owner token for ${deckSlug(deckDir)}: ${held.token}\n` +
|
|
296
|
+
`Export it so every command in this session runs under it:\n` +
|
|
297
|
+
` export SLIDEMIND_OWNER=${held.token}\n` +
|
|
298
|
+
`Release it when the session ends: slidemind revision finish ${deck}\n` +
|
|
299
|
+
`Generation ${status.generation}${status.localDirty ? `, ${status.dirtySlides.length} slide(s) with unpublished edits` : ", clean"}\n` +
|
|
300
|
+
(recorded
|
|
301
|
+
? `Scope ${recorded.operationId}: ${recorded.kind} on ${recorded.targets.length} slide(s)\n` +
|
|
302
|
+
(recorded.kind === "redesign"
|
|
303
|
+
? `Redesign replaces these slides in place: rewrite each file listed below, keep its ` +
|
|
304
|
+
`native placeholders, and do not add a slide beside it.\n`
|
|
305
|
+
: ``) +
|
|
306
|
+
recorded.targets
|
|
307
|
+
.map((t) => ` ${t.jsxPath} ${t.slideKey} base ${String(t.baseSourceHash ?? "").slice(0, 12)}\n`)
|
|
308
|
+
.join("") +
|
|
309
|
+
(amended?.superseded.length || amended?.added.length
|
|
310
|
+
? `Amended on request: ${amended.superseded.length} obligation(s) retired, ` +
|
|
311
|
+
`${amended.added.length} required addition(s).\n`
|
|
312
|
+
: ``) +
|
|
313
|
+
(migration?.seeded.length
|
|
314
|
+
? `Captured preservation records for ${migration.seeded.length} slide(s) from their current source ` +
|
|
315
|
+
`(${[...new Set(migration.seeded.map((e) => e.origin))].join(", ")}); evidence: unknown until a render verdict.\n` +
|
|
316
|
+
migration.seeded
|
|
317
|
+
.flatMap((e) => e.limitations.map((l) => ` limitation ${path.basename(e.jsxPath)}: ${l}\n`))
|
|
318
|
+
.join("")
|
|
319
|
+
: ``)
|
|
320
|
+
: ``),
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// One operation record from the resolved targets. `changes` stays whatever the
|
|
325
|
+
// scope file declared — empty when it declared none, which authorizes no
|
|
326
|
+
// deletion and no property change.
|
|
327
|
+
function buildOperation({ deckDir, kind, slides, scope, summary }) {
|
|
328
|
+
// The whole deck first: `preparedSlideKeys` is what later lets a redesign
|
|
329
|
+
// tell a slide that appeared since from one that was always there
|
|
330
|
+
// (checkRedesignTransition). Resolved here because this is the moment the
|
|
331
|
+
// scope is taken — afterwards nobody can reconstruct it.
|
|
332
|
+
const deckSlides = resolveSlideTargets(deckDir);
|
|
333
|
+
const selection = parseSlideSelection(slides);
|
|
334
|
+
const targets = selection
|
|
335
|
+
? resolveSlideTargets(deckDir, { selection })
|
|
336
|
+
: deckSlides;
|
|
337
|
+
if (targets.length === 0) fail(`--slides matched no slide in ${deckSlug(deckDir)}`);
|
|
338
|
+
const scoped = (target) => {
|
|
339
|
+
const entry =
|
|
340
|
+
scope?.targets?.[target.slideKey] ??
|
|
341
|
+
scope?.targets?.[target.file] ??
|
|
342
|
+
scope?.targets?.[target.file.replace(/\.(jsx|pptx)$/i, "")] ??
|
|
343
|
+
scope?.targets?.[String(target.number)] ??
|
|
344
|
+
null;
|
|
345
|
+
return entry ?? {};
|
|
346
|
+
};
|
|
347
|
+
return {
|
|
348
|
+
version: EDIT_OPERATION_VERSION,
|
|
349
|
+
operationId: `op-${randomUUID()}`,
|
|
350
|
+
kind,
|
|
351
|
+
preparedSlideKeys: deckSlides.map((t) => t.slideKey),
|
|
352
|
+
// Every slide's bytes as the scope opened: what tells a slide changed outside
|
|
353
|
+
// the scope (or a sibling written beside a target) from one left alone
|
|
354
|
+
// (src/lib/edit-transitions.mjs).
|
|
355
|
+
preparedSourceHashes: Object.fromEntries(deckSlides.map((t) => [t.slideKey, t.sourceHash])),
|
|
356
|
+
requestSummary:
|
|
357
|
+
summary ?? scope?.requestSummary ?? `${kind} on ${targets.map((t) => t.file).join(", ")}`,
|
|
358
|
+
status: "prepared",
|
|
359
|
+
targets: targets.map((t) => ({
|
|
360
|
+
slideKey: t.slideKey,
|
|
361
|
+
jsxPath: t.jsxPath,
|
|
362
|
+
baseSourceHash: t.sourceHash,
|
|
363
|
+
provenance: "local",
|
|
364
|
+
changes: [],
|
|
365
|
+
...scoped(t),
|
|
366
|
+
})),
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function revisionFinish(args) {
|
|
371
|
+
const { deck, passthrough } = splitDeckArg(args, "revision finish");
|
|
372
|
+
let owner = process.env.SLIDEMIND_OWNER ?? null;
|
|
373
|
+
let force = false;
|
|
374
|
+
for (let i = 0; i < passthrough.length; i++) {
|
|
375
|
+
const a = passthrough[i];
|
|
376
|
+
if (a === "--owner") owner = passthrough[++i] ?? owner;
|
|
377
|
+
else if (a === "--force") force = true;
|
|
378
|
+
else fail(`unknown flag: ${a}`);
|
|
379
|
+
}
|
|
380
|
+
const deckDir = resolveDeckArg(deck, { allowUnfinished: true });
|
|
381
|
+
const busy = readBusy(deckDir);
|
|
382
|
+
if (!busy) {
|
|
383
|
+
process.stdout.write(`${deckSlug(deckDir)} has no owner — nothing to release.\n`);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
const released = releaseStaleOwner(deckDir, { owner, force });
|
|
387
|
+
if (!released) {
|
|
388
|
+
fail(
|
|
389
|
+
(busy.kind === "session"
|
|
390
|
+
? `${deckSlug(deckDir)} is held by an edit session (\`${busy.command}\`, started ${busy.startedAt}).`
|
|
391
|
+
: `${deckSlug(deckDir)} is owned by a running \`${busy.command}\` (pid ${busy.pid}, since ${busy.startedAt}).`) +
|
|
392
|
+
` Pass --owner <token> to release your own session (export SLIDEMIND_OWNER and it is read from there), ` +
|
|
393
|
+
`or --force to release it anyway.`,
|
|
394
|
+
2,
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
// Releasing ownership is NOT discarding work: the dirty files, the candidates
|
|
398
|
+
// and any journaled operation are all exactly where they were. What DOES end
|
|
399
|
+
// is the edit scope — a prepared operation whose session is over authorizes
|
|
400
|
+
// nothing, and leaving it open would lease those slides forever.
|
|
401
|
+
let closed = [];
|
|
402
|
+
try {
|
|
403
|
+
closed = closeOpenOperations(deckDir);
|
|
404
|
+
} catch (err) {
|
|
405
|
+
// An unreadable scope file is not a reason to refuse releasing the deck.
|
|
406
|
+
process.stderr.write(`slidemind: open edit scopes were left as they are — ${err.message}\n`);
|
|
407
|
+
}
|
|
408
|
+
const status = revisionStatus(deckDir);
|
|
409
|
+
process.stdout.write(
|
|
410
|
+
`Released ${deckSlug(deckDir)} (was ${released.command}).\n` +
|
|
411
|
+
(closed.length ? `Closed ${closed.length} open edit scope(s): ${closed.join(", ")}\n` : ``) +
|
|
412
|
+
(status.localDirty
|
|
413
|
+
? `${status.dirtySlides.length} slide(s) still carry unpublished edits — they are untouched.\n`
|
|
414
|
+
: `Nothing unpublished.\n`),
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Reporting must survive a record it cannot read: `status` is the command
|
|
419
|
+
// somebody runs BECAUSE the deck is in a strange state.
|
|
420
|
+
function openScopes(deckDir) {
|
|
421
|
+
try {
|
|
422
|
+
return openOperations(deckDir);
|
|
423
|
+
} catch {
|
|
424
|
+
return [];
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function revisionStatusCmd(args) {
|
|
429
|
+
const { deck, passthrough } = splitDeckArg(args, "revision status");
|
|
430
|
+
const asJson = passthrough.includes("--json");
|
|
431
|
+
const deckDir = resolveDeckArg(deck, { allowUnfinished: true });
|
|
432
|
+
const status = revisionStatus(deckDir);
|
|
433
|
+
if (asJson) return json(status);
|
|
434
|
+
if (!status.adopted) {
|
|
435
|
+
// A deck authored here still has edit scopes, and they are the only thing
|
|
436
|
+
// this command can report about it.
|
|
437
|
+
process.stdout.write(
|
|
438
|
+
`${deckSlug(deckDir)} has no accepted base (authored here, not adopted from a .pptx).\n` +
|
|
439
|
+
openScopes(deckDir)
|
|
440
|
+
.map((op) => ` scope ${op.operationId} ${op.kind} (${op.status}) on ${op.targets.length} slide(s)\n`)
|
|
441
|
+
.join(""),
|
|
442
|
+
);
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
const lines = [
|
|
446
|
+
`${deckSlug(deckDir)} — generation ${status.generation}`,
|
|
447
|
+
status.base
|
|
448
|
+
? ` base ${status.base.sha256.slice(0, 12)} · ${status.base.slideCount} slide(s) · accepted ${status.base.acceptedAt}`
|
|
449
|
+
: ` base none — authored here; the first confirmed publish (or \`revision accept\`) sets it`,
|
|
450
|
+
` origin ${status.origin?.kind ?? "unknown"}${status.origin?.file ? ` (${status.origin.file})` : ""}`,
|
|
451
|
+
` local ${status.localDirty ? `${status.dirtySlides.length} slide(s) changed: ${status.dirtySlides.join(", ")}` : "clean"}`,
|
|
452
|
+
];
|
|
453
|
+
if (status.added.length) lines.push(` added ${status.added.join(", ")}`);
|
|
454
|
+
if (status.removed.length) lines.push(` removed ${status.removed.join(", ")}`);
|
|
455
|
+
if (status.orderChanged) lines.push(` order changed locally`);
|
|
456
|
+
if (status.designMoved) {
|
|
457
|
+
lines.push(` design moved since the accepted base — untouched slides keep the old design until \`build-pptx --refresh-design\``);
|
|
458
|
+
}
|
|
459
|
+
if (status.unverified.length) {
|
|
460
|
+
lines.push(
|
|
461
|
+
` unverified ${status.unverified.length} slide(s) carry a chart, whose data this CLI cannot hash — ` +
|
|
462
|
+
`"unchanged" is not proved for them`,
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
lines.push(
|
|
466
|
+
` candidate ${
|
|
467
|
+
status.candidate
|
|
468
|
+
? `${status.candidate.candidateId}${status.candidate.stale ? ` (stale: ${status.candidate.stale})` : " (ready to accept)"}`
|
|
469
|
+
: "none"
|
|
470
|
+
}`,
|
|
471
|
+
);
|
|
472
|
+
if (status.staged.length) {
|
|
473
|
+
lines.push(` staged ${status.staged.map((s) => s.revisionId).join(", ")}`);
|
|
474
|
+
}
|
|
475
|
+
if (status.conflicts) lines.push(` conflicts ${status.conflicts} unresolved`);
|
|
476
|
+
if (status.owner) {
|
|
477
|
+
lines.push(` owner ${status.owner.command} (pid ${status.owner.pid}, since ${status.owner.startedAt})`);
|
|
478
|
+
}
|
|
479
|
+
if (status.journal) lines.push(` journal an unfinished ${status.journal} — re-run it to recover`);
|
|
480
|
+
for (const op of openScopes(deckDir)) {
|
|
481
|
+
lines.push(` scope ${op.operationId} ${op.kind} (${op.status}) on ${op.targets.length} slide(s)`);
|
|
482
|
+
}
|
|
483
|
+
process.stdout.write(`${lines.join("\n")}\n`);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async function revisionAccept(args) {
|
|
487
|
+
const { deck, passthrough } = splitDeckArg(args, "revision accept");
|
|
488
|
+
let candidateId = null;
|
|
489
|
+
let asJson = false;
|
|
490
|
+
for (let i = 0; i < passthrough.length; i++) {
|
|
491
|
+
const a = passthrough[i];
|
|
492
|
+
if (a === "--candidate") candidateId = passthrough[++i] ?? null;
|
|
493
|
+
else if (a === "--json") asJson = true;
|
|
494
|
+
else fail(`unknown flag: ${a}`);
|
|
495
|
+
}
|
|
496
|
+
const deckDir = resolveDeckArg(deck, { allowUnfinished: true });
|
|
497
|
+
requireAdopted(deckDir);
|
|
498
|
+
if (!candidateId) {
|
|
499
|
+
const available = listCandidates(deckDir);
|
|
500
|
+
fail(
|
|
501
|
+
`usage: slidemind revision accept <deck> --candidate <id>\n` +
|
|
502
|
+
(available.length
|
|
503
|
+
? `candidates:\n${available
|
|
504
|
+
.map((c) => {
|
|
505
|
+
const why = candidateStale(deckDir, c);
|
|
506
|
+
return ` ${c.candidateId} ${c.createdAt} ${why ? `stale — ${why}` : "ready"}`;
|
|
507
|
+
})
|
|
508
|
+
.join("\n")}`
|
|
509
|
+
: `this deck has no candidates yet — run \`slidemind build-pptx ${deck}\` first`),
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
const held = acquireOwner(deckDir, { command: "revision accept" });
|
|
513
|
+
try {
|
|
514
|
+
await requireReviewApproved(deckDir);
|
|
515
|
+
// An explicit redesign is authorized by the request — it is never asked
|
|
516
|
+
// about again here. What IS checked is that it replaced what it was pointed
|
|
517
|
+
// at: same identity, required content carried forward, native packages
|
|
518
|
+
// still backed, and no second slide left beside the original (E18). The
|
|
519
|
+
// base may not move onto a candidate that failed that.
|
|
520
|
+
assertRedesignsReplaced(deckDir);
|
|
521
|
+
const next = acceptCandidate(deckDir, candidateId, { owner: held.token });
|
|
522
|
+
const status = revisionStatus(deckDir);
|
|
523
|
+
if (asJson) return json({ accepted: candidateId, ...status });
|
|
524
|
+
process.stdout.write(
|
|
525
|
+
`Accepted ${candidateId} as the base of ${deckSlug(deckDir)} (generation ${next.generation}).\n` +
|
|
526
|
+
(status.localDirty
|
|
527
|
+
? `${status.dirtySlides.length} slide(s) still carry edits made since that build.\n`
|
|
528
|
+
: `The workspace matches the accepted base.\n`),
|
|
529
|
+
);
|
|
530
|
+
} catch (err) {
|
|
531
|
+
if (["remote_bound", "conflicted", "candidate_stale", "candidate_invalid", "generation_moved", "ownership_lost"].includes(err?.code)) {
|
|
532
|
+
fail(err.message, 2);
|
|
533
|
+
}
|
|
534
|
+
throw err;
|
|
535
|
+
} finally {
|
|
536
|
+
held.release();
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// ---------------------------------------------------------------------------
|
|
541
|
+
// rebase
|
|
542
|
+
// ---------------------------------------------------------------------------
|
|
543
|
+
|
|
544
|
+
export async function cmdRebase(rest) {
|
|
545
|
+
const { deck, passthrough } = splitDeckArg(rest, "rebase");
|
|
546
|
+
let incoming = null;
|
|
547
|
+
let resolve = null;
|
|
548
|
+
let dryRun = false;
|
|
549
|
+
let asJson = false;
|
|
550
|
+
for (let i = 0; i < passthrough.length; i++) {
|
|
551
|
+
const a = passthrough[i];
|
|
552
|
+
if (a === "--incoming") {
|
|
553
|
+
incoming = passthrough[++i] ?? null;
|
|
554
|
+
if (!incoming) fail("--incoming expects a .pptx file or a staged revision id");
|
|
555
|
+
} else if (a === "--resolve") {
|
|
556
|
+
resolve = passthrough[++i] ?? null;
|
|
557
|
+
if (!resolve) fail("--resolve expects a resolution .json file");
|
|
558
|
+
} else if (a === "--dry-run") {
|
|
559
|
+
dryRun = true;
|
|
560
|
+
} else if (a === "--json") {
|
|
561
|
+
asJson = true;
|
|
562
|
+
} else {
|
|
563
|
+
fail(`unknown flag: ${a}`);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
const deckDir = resolveDeckArg(deck, { allowUnfinished: true });
|
|
567
|
+
requireAdopted(deckDir);
|
|
568
|
+
const { applyRebase, classifyRebase, resolutionTemplate, resumeRebase } = await import(
|
|
569
|
+
"../lib/rebase.mjs"
|
|
570
|
+
);
|
|
571
|
+
|
|
572
|
+
// No flags, but an unfinished rebase on disk: that is the recovery call. Every
|
|
573
|
+
// other command refuses this deck until the half-applied generation resolves,
|
|
574
|
+
// and re-running from the staged bytes is how it resolves.
|
|
575
|
+
const open = journalIncomplete(deckDir);
|
|
576
|
+
if (!incoming && !resolve && open?.op === "rebase") {
|
|
577
|
+
const held = acquireOwner(deckDir, { command: "rebase" });
|
|
578
|
+
try {
|
|
579
|
+
const resumed = resumeRebase(deckDir, open, { owner: held.token });
|
|
580
|
+
if (asJson) return json(resumed);
|
|
581
|
+
process.stdout.write(
|
|
582
|
+
resumed.resumed
|
|
583
|
+
? `Finished the interrupted rebase of ${deckSlug(deckDir)} (generation ${resumed.generation}).\n` +
|
|
584
|
+
(resumed.stillDirty.length
|
|
585
|
+
? `Still unpublished: ${resumed.stillDirty.join(", ")}\n`
|
|
586
|
+
: `The workspace matches the accepted base.\n`)
|
|
587
|
+
: `Nothing to finish: ${resumed.reason}\n`,
|
|
588
|
+
);
|
|
589
|
+
return;
|
|
590
|
+
} finally {
|
|
591
|
+
held.release();
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
if (!incoming && !resolve) {
|
|
595
|
+
fail(
|
|
596
|
+
`usage: slidemind rebase <deck> --incoming <file.pptx | revisionId> [--dry-run]\n` +
|
|
597
|
+
` slidemind rebase <deck> --resolve <resolution.json>\n` +
|
|
598
|
+
` slidemind rebase <deck> (finish an interrupted rebase)`,
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Resolving continues the rebase the conflict manifest belongs to.
|
|
603
|
+
let revisionId = null;
|
|
604
|
+
let resolutions = null;
|
|
605
|
+
if (resolve) {
|
|
606
|
+
if (!fs.existsSync(resolve)) fail(`no such file: ${resolve}`);
|
|
607
|
+
try {
|
|
608
|
+
resolutions = JSON.parse(fs.readFileSync(resolve, "utf8"));
|
|
609
|
+
} catch (err) {
|
|
610
|
+
fail(`${resolve} is not readable JSON: ${err.message}`);
|
|
611
|
+
}
|
|
612
|
+
revisionId = resolutions.revisionId ?? readConflicts(deckDir)?.revisionId ?? null;
|
|
613
|
+
if (!revisionId) fail(`${resolve} names no revisionId, and this deck has no open conflict manifest`);
|
|
614
|
+
} else if (/\.(pptx|pptm|potx|potm|ppsx|ppsm)$/i.test(incoming)) {
|
|
615
|
+
const file = path.resolve(incoming);
|
|
616
|
+
if (!fs.existsSync(file)) fail(`no such file: ${incoming}`);
|
|
617
|
+
revisionId = stageIncoming(deckDir, {
|
|
618
|
+
bytes: fs.readFileSync(file),
|
|
619
|
+
source: { kind: "file", path: file, name: path.basename(file) },
|
|
620
|
+
}).revisionId;
|
|
621
|
+
} else {
|
|
622
|
+
revisionId = incoming;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (dryRun) {
|
|
626
|
+
const classified = classifyRebase(deckDir, revisionId);
|
|
627
|
+
if (asJson) return json(classified);
|
|
628
|
+
process.stdout.write(
|
|
629
|
+
`${deckSlug(deckDir)} vs ${revisionId}:\n` +
|
|
630
|
+
` incoming-only ${classified.remoteOnly.length}\n` +
|
|
631
|
+
` local-only ${classified.localOnly.length}\n` +
|
|
632
|
+
` unchanged ${classified.unchanged.length}\n` +
|
|
633
|
+
` conflicts ${classified.conflicts.length}\n` +
|
|
634
|
+
classified.notes.map((n) => ` note: ${n}\n`).join("") +
|
|
635
|
+
classified.conflicts.map((c) => ` [${c.id}] ${c.kind}${c.file ? ` ${c.file}` : ""} — ${c.detail}\n`).join(""),
|
|
636
|
+
);
|
|
637
|
+
if (classified.conflicts.length > 0) {
|
|
638
|
+
const out = path.join(deckDir, "state", "staged", revisionId, "resolution.json");
|
|
639
|
+
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
640
|
+
fs.writeFileSync(out, `${JSON.stringify(resolutionTemplate(classified), null, 2)}\n`);
|
|
641
|
+
process.stdout.write(
|
|
642
|
+
` Fill in one choice per conflict and apply it:\n slidemind rebase ${deck} --resolve ${out}\n`,
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const held = acquireOwner(deckDir, { command: "rebase" });
|
|
649
|
+
try {
|
|
650
|
+
const result = applyRebase(deckDir, revisionId, { owner: held.token, resolutions });
|
|
651
|
+
if (asJson) return json(result);
|
|
652
|
+
process.stdout.write(
|
|
653
|
+
`Rebased ${deckSlug(deckDir)} onto ${revisionId} (generation ${result.generation}).\n` +
|
|
654
|
+
` refreshed from the incoming file: ${result.refreshed.length}\n` +
|
|
655
|
+
` removed (deleted there): ${result.removed.length}\n` +
|
|
656
|
+
` kept as unpublished local work: ${result.localOnly.length}\n` +
|
|
657
|
+
result.notes.map((n) => ` note: ${n}\n`).join("") +
|
|
658
|
+
(result.stillDirty.length
|
|
659
|
+
? `Still unpublished: ${result.stillDirty.join(", ")}\n`
|
|
660
|
+
: `The workspace matches the new accepted base.\n`),
|
|
661
|
+
);
|
|
662
|
+
} catch (err) {
|
|
663
|
+
if (err?.code === "rebase_conflicts") {
|
|
664
|
+
const classified = classifyRebase(deckDir, revisionId);
|
|
665
|
+
const out = path.join(deckDir, "state", "staged", revisionId, "resolution.json");
|
|
666
|
+
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
667
|
+
fs.writeFileSync(out, `${JSON.stringify(resolutionTemplate(classified), null, 2)}\n`);
|
|
668
|
+
process.stderr.write(
|
|
669
|
+
`slidemind: ${err.message}\n` +
|
|
670
|
+
err.conflicts.map((c) => ` [${c.id}] ${c.kind}${c.file ? ` ${c.file}` : ""} — ${c.detail}\n`).join("") +
|
|
671
|
+
`A resolution skeleton with every hash already filled in is at:\n ${out}\n` +
|
|
672
|
+
`Set each "choice" to local, incoming or merged, then:\n` +
|
|
673
|
+
` slidemind rebase ${deck} --resolve ${out}\n`,
|
|
674
|
+
);
|
|
675
|
+
process.exit(2);
|
|
676
|
+
}
|
|
677
|
+
if (["resolution_stale", "no_such_revision", "no_base", "generation_moved", "ownership_lost"].includes(err?.code)) {
|
|
678
|
+
fail(err.message, 2);
|
|
679
|
+
}
|
|
680
|
+
throw err;
|
|
681
|
+
} finally {
|
|
682
|
+
held.release();
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
export { readCandidate, revisionStatus };
|