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,359 @@
|
|
|
1
|
+
// The persistent deck library: `<sessionConfigDir>/workspace/registry.json`.
|
|
2
|
+
//
|
|
3
|
+
// { schemaVersion: 1, cliVersion, roots: [abs...], decks: [{ id, path, name, lastSeenAt, missing, hidden, format }] }
|
|
4
|
+
//
|
|
5
|
+
// `cliVersion` is the CLI that last populated the library; a different one at
|
|
6
|
+
// daemon start re-scans every remembered root (discovery.mjs autoDiscover).
|
|
7
|
+
// `format` is deck-paths `deckFormat` — current / legacy / unknown — refreshed
|
|
8
|
+
// on every registration and listing. Registration migrates a flat pre-workspace
|
|
9
|
+
// deck into `.slidemind/` first (`migrateLegacyDeck`), so `legacy` only survives
|
|
10
|
+
// a migration that failed, and such a deck is flagged, never opened.
|
|
11
|
+
//
|
|
12
|
+
// An entry's `id` is a local UUID; its `path` is the canonical realpath of the
|
|
13
|
+
// deck ROOT (the deliverable dir, never `.slidemind`). Two directories that
|
|
14
|
+
// carry the same portable deck ID stay two entries — the library identifies
|
|
15
|
+
// working copies, not decks. Every function reads the file fresh: the daemon
|
|
16
|
+
// and any CLI process share it through the short-lived registry lock, so there
|
|
17
|
+
// is no in-memory copy to invalidate. The lock is never held while acquiring a
|
|
18
|
+
// deck lock (lock ordering: registry inside deck, never the other way round).
|
|
19
|
+
|
|
20
|
+
import { randomUUID } from "node:crypto";
|
|
21
|
+
import fs from "node:fs";
|
|
22
|
+
import path from "node:path";
|
|
23
|
+
|
|
24
|
+
import { resolveBundleDirForDeck } from "../bridge/bundle.mjs";
|
|
25
|
+
import { sessionConfigDir } from "../bridge/config.mjs";
|
|
26
|
+
import { deckFormat, deckRoot, deckSlug, isDeckDir, migrateLegacyDeck, resolveWorkspace } from "../lib/deck-paths.mjs";
|
|
27
|
+
|
|
28
|
+
export const REGISTRY_SCHEMA_VERSION = 1;
|
|
29
|
+
|
|
30
|
+
export function workspaceConfigDir() {
|
|
31
|
+
return path.join(sessionConfigDir(), "workspace");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function registryPath() {
|
|
35
|
+
return path.join(workspaceConfigDir(), "registry.json");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Write-then-rename, so a kill mid-write never leaves half a JSON file. */
|
|
39
|
+
export function writeJsonAtomic(file, data, { mode } = {}) {
|
|
40
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
41
|
+
const tmp = `${file}.${process.pid}.${randomUUID().slice(0, 8)}.tmp`;
|
|
42
|
+
fs.writeFileSync(tmp, JSON.stringify(data, null, 2) + "\n", mode ? { mode } : undefined);
|
|
43
|
+
fs.renameSync(tmp, file);
|
|
44
|
+
return file;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function coded(code, message, extra = {}) {
|
|
48
|
+
return Object.assign(new Error(message), { code, ...extra });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// lock
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
const LOCK_STALE_MS = 10_000;
|
|
56
|
+
const LOCK_WAIT_MS = 5_000;
|
|
57
|
+
|
|
58
|
+
const LOCK_SLICE_MS = 25;
|
|
59
|
+
|
|
60
|
+
function sleepSync(ms) {
|
|
61
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
62
|
+
}
|
|
63
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
64
|
+
|
|
65
|
+
const lockPath = () => path.join(workspaceConfigDir(), "registry.lock");
|
|
66
|
+
const lockOwner = (lock) => {
|
|
67
|
+
try {
|
|
68
|
+
const pid = Number(fs.readFileSync(path.join(lock, "owner"), "utf8"));
|
|
69
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
70
|
+
} catch {
|
|
71
|
+
return null; // a lock from before this file, or taken a microsecond ago
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
// A pid can be recycled and an unreadable owner cannot be judged, so this only
|
|
75
|
+
// ever SHORTENS a wait — the mtime staleness stays the backstop.
|
|
76
|
+
const ownerAlive = (pid) => {
|
|
77
|
+
if (pid === null) return true;
|
|
78
|
+
try {
|
|
79
|
+
process.kill(pid, 0);
|
|
80
|
+
return true;
|
|
81
|
+
} catch (err) {
|
|
82
|
+
return err?.code === "EPERM";
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* One acquisition loop, driven two ways: it yields the milliseconds to wait
|
|
88
|
+
* before the next attempt and returns once the lock is held.
|
|
89
|
+
*
|
|
90
|
+
* The critical section is one small read-modify-write, so an honest hold is
|
|
91
|
+
* about a millisecond. The only way the wait below ever reached its 5s deadline
|
|
92
|
+
* was a lock nobody holds any more, which is why the owner's pid is written into
|
|
93
|
+
* it and a dead owner's lock is reclaimed at once instead of after the stale TTL.
|
|
94
|
+
*/
|
|
95
|
+
function* acquireLock(lock) {
|
|
96
|
+
fs.mkdirSync(path.dirname(lock), { recursive: true });
|
|
97
|
+
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
98
|
+
for (;;) {
|
|
99
|
+
try {
|
|
100
|
+
fs.mkdirSync(lock);
|
|
101
|
+
try {
|
|
102
|
+
fs.writeFileSync(path.join(lock, "owner"), String(process.pid));
|
|
103
|
+
} catch {
|
|
104
|
+
// an unwritable owner file costs a faster reclaim, never correctness
|
|
105
|
+
}
|
|
106
|
+
return;
|
|
107
|
+
} catch (err) {
|
|
108
|
+
if (err.code !== "EEXIST") throw err;
|
|
109
|
+
let age = 0;
|
|
110
|
+
try {
|
|
111
|
+
age = Date.now() - fs.statSync(lock).mtimeMs;
|
|
112
|
+
} catch {
|
|
113
|
+
continue; // released between our attempt and the stat
|
|
114
|
+
}
|
|
115
|
+
if (age > LOCK_STALE_MS || !ownerAlive(lockOwner(lock))) {
|
|
116
|
+
fs.rmSync(lock, { recursive: true, force: true });
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (Date.now() > deadline) throw coded("registry_busy", "deck registry is locked by another slidemind process");
|
|
120
|
+
yield LOCK_SLICE_MS;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Run `fn` under the registry lock (a mkdir'd directory; exclusive by the OS). */
|
|
126
|
+
export function withRegistryLock(fn) {
|
|
127
|
+
const lock = lockPath();
|
|
128
|
+
const it = acquireLock(lock);
|
|
129
|
+
// A CLI process has nothing else to do while it waits, so it blocks its thread.
|
|
130
|
+
for (let step = it.next(); !step.done; step = it.next()) sleepSync(step.value);
|
|
131
|
+
try {
|
|
132
|
+
return fn();
|
|
133
|
+
} finally {
|
|
134
|
+
fs.rmSync(lock, { recursive: true, force: true });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The same lock, awaited. The daemon serves SSE streams and long polls off one
|
|
140
|
+
* event loop, so `Atomics.wait` there stalls every other request in the process
|
|
141
|
+
* — never spin on the daemon's thread, use this from its request handlers.
|
|
142
|
+
*/
|
|
143
|
+
export async function withRegistryLockAsync(fn) {
|
|
144
|
+
const lock = lockPath();
|
|
145
|
+
const it = acquireLock(lock);
|
|
146
|
+
for (let step = it.next(); !step.done; step = it.next()) await sleep(step.value);
|
|
147
|
+
try {
|
|
148
|
+
return await fn();
|
|
149
|
+
} finally {
|
|
150
|
+
fs.rmSync(lock, { recursive: true, force: true });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
// read / write
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
const EMPTY = () => ({ schemaVersion: REGISTRY_SCHEMA_VERSION, roots: [], decks: [] });
|
|
159
|
+
|
|
160
|
+
/** The registry as on disk; a missing file is an empty one, a malformed one throws `registry_malformed` and is retained. */
|
|
161
|
+
export function readRegistry() {
|
|
162
|
+
const file = registryPath();
|
|
163
|
+
let raw;
|
|
164
|
+
try {
|
|
165
|
+
raw = fs.readFileSync(file, "utf8");
|
|
166
|
+
} catch (err) {
|
|
167
|
+
if (err.code === "ENOENT") return EMPTY();
|
|
168
|
+
throw err;
|
|
169
|
+
}
|
|
170
|
+
let data;
|
|
171
|
+
try {
|
|
172
|
+
data = JSON.parse(raw);
|
|
173
|
+
} catch {
|
|
174
|
+
throw coded("registry_malformed", `deck registry ${file} is not valid JSON; it was left in place for recovery`, { file });
|
|
175
|
+
}
|
|
176
|
+
if (!data || data.schemaVersion !== REGISTRY_SCHEMA_VERSION || !Array.isArray(data.decks) || !Array.isArray(data.roots)) {
|
|
177
|
+
throw coded("registry_malformed", `deck registry ${file} has an unsupported shape; it was left in place for recovery`, { file });
|
|
178
|
+
}
|
|
179
|
+
return data;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function writeRegistry(data) {
|
|
183
|
+
return writeJsonAtomic(registryPath(), data);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// The critical section itself, shared by both drivers. Every registry mutator is
|
|
187
|
+
// a synchronous function of the loaded registry, so only the WAIT differs.
|
|
188
|
+
const mutate = (fn) => () => {
|
|
189
|
+
const reg = readRegistry();
|
|
190
|
+
const out = fn(reg);
|
|
191
|
+
writeRegistry(reg);
|
|
192
|
+
return out;
|
|
193
|
+
};
|
|
194
|
+
const update = (fn) => withRegistryLock(mutate(fn));
|
|
195
|
+
const updateAsync = (fn) => withRegistryLockAsync(mutate(fn));
|
|
196
|
+
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
// decks
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
/** Canonical deck root of anything a user might point at: the root, its workspace, or a file inside. */
|
|
202
|
+
export function canonicalDeckPath(p) {
|
|
203
|
+
const abs = path.resolve(p);
|
|
204
|
+
const root = deckRoot(resolveWorkspace(abs));
|
|
205
|
+
return fs.realpathSync(root);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Register a deck directory (idempotent per realpath). Throws `not_a_deck` for a
|
|
210
|
+
* directory without deck markers and `missing_path` when it does not exist.
|
|
211
|
+
*/
|
|
212
|
+
// The path checks and the migration happen BEFORE the lock is taken (neither
|
|
213
|
+
// touches the registry), then the mutator runs under it. Split out so the async
|
|
214
|
+
// twin the daemon's routes use shares every rule rather than repeating it.
|
|
215
|
+
function deckEntryMutator(p, { hidden = false, name = null } = {}) {
|
|
216
|
+
let real;
|
|
217
|
+
try {
|
|
218
|
+
real = canonicalDeckPath(p);
|
|
219
|
+
} catch (err) {
|
|
220
|
+
if (err.code === "ENOENT") throw coded("missing_path", `${p} does not exist`);
|
|
221
|
+
throw err;
|
|
222
|
+
}
|
|
223
|
+
if (!fs.statSync(real).isDirectory() || !isDeckDir(real)) {
|
|
224
|
+
throw coded("not_a_deck", `${real} carries no deck markers (.slidemind/, slides/, plan.md or a plan record)`);
|
|
225
|
+
}
|
|
226
|
+
migrateBestEffort(real);
|
|
227
|
+
return (reg) => {
|
|
228
|
+
const now = new Date().toISOString();
|
|
229
|
+
let entry = reg.decks.find((d) => d.path === real);
|
|
230
|
+
const format = deckFormat(real);
|
|
231
|
+
if (entry) {
|
|
232
|
+
entry.lastSeenAt = now;
|
|
233
|
+
entry.missing = false;
|
|
234
|
+
entry.format = format;
|
|
235
|
+
if (name) entry.name = name;
|
|
236
|
+
return { ...entry, created: false };
|
|
237
|
+
}
|
|
238
|
+
entry = { id: randomUUID(), path: real, name: name ?? deckSlug(real), lastSeenAt: now, missing: false, hidden, format };
|
|
239
|
+
reg.decks.push(entry);
|
|
240
|
+
return { ...entry, created: true };
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export const addDeck = (p, opts = {}) => update(deckEntryMutator(p, opts));
|
|
245
|
+
/** `addDeck` for the daemon: the wait for the lock never blocks its event loop. */
|
|
246
|
+
export const addDeckAsync = (p, opts = {}) => updateAsync(deckEntryMutator(p, opts));
|
|
247
|
+
|
|
248
|
+
/** Move a flat deck into `.slidemind/`; a failure leaves it `legacy` and says why. */
|
|
249
|
+
export function migrateBestEffort(root) {
|
|
250
|
+
try {
|
|
251
|
+
// The resolver is injected rather than imported by deck-paths: bundle.mjs
|
|
252
|
+
// already imports deck-paths, so the arrow would be a cycle.
|
|
253
|
+
const moved = migrateLegacyDeck(root, { bundleDirFor: resolveBundleDirForDeck });
|
|
254
|
+
if (moved) process.stderr.write(`slidemind: migrated ${deckSlug(root)} into ${path.join(root, ".slidemind")} (${moved.length} entries)\n`);
|
|
255
|
+
return moved;
|
|
256
|
+
} catch (err) {
|
|
257
|
+
process.stderr.write(`slidemind: could not migrate ${root} into .slidemind/: ${err.message}\n`);
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Registration that never throws: warns on stderr and returns null. For command hooks. */
|
|
263
|
+
export function registerDeckBestEffort(p, opts = {}) {
|
|
264
|
+
try {
|
|
265
|
+
return addDeck(p, opts);
|
|
266
|
+
} catch (err) {
|
|
267
|
+
process.stderr.write(`slidemind: workspace registry skipped ${p}: ${err.message}\n`);
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function getDeck(id) {
|
|
273
|
+
return readRegistry().decks.find((d) => d.id === id) ?? null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Visible entries, newest-seen first, with `missing` refreshed from the filesystem. */
|
|
277
|
+
export function listDecks({ q = "", hidden = false, limit = 100, offset = 0 } = {}) {
|
|
278
|
+
const needle = String(q || "").toLowerCase();
|
|
279
|
+
const all = readRegistry()
|
|
280
|
+
.decks.filter((d) => hidden || !d.hidden)
|
|
281
|
+
.filter((d) => !needle || d.name.toLowerCase().includes(needle) || d.path.toLowerCase().includes(needle))
|
|
282
|
+
.map((d) => {
|
|
283
|
+
const missing = !fs.existsSync(d.path);
|
|
284
|
+
return { ...d, missing, format: missing ? (d.format ?? "unknown") : deckFormat(d.path) };
|
|
285
|
+
})
|
|
286
|
+
.sort((a, b) => (b.lastSeenAt ?? "").localeCompare(a.lastSeenAt ?? ""));
|
|
287
|
+
return { total: all.length, decks: all.slice(offset, offset + limit) };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function markMissing(id, missing = true) {
|
|
291
|
+
return update((reg) => {
|
|
292
|
+
const entry = reg.decks.find((d) => d.id === id);
|
|
293
|
+
if (!entry) throw coded("unknown_deck", `no registered deck ${id}`);
|
|
294
|
+
entry.missing = missing;
|
|
295
|
+
return { ...entry };
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Point a moved entry at its new location; refuses a path another entry already owns. */
|
|
300
|
+
export function locateDeck(id, newPath) {
|
|
301
|
+
const real = canonicalDeckPath(newPath);
|
|
302
|
+
if (!isDeckDir(real)) throw coded("not_a_deck", `${real} carries no deck markers`);
|
|
303
|
+
return update((reg) => {
|
|
304
|
+
const entry = reg.decks.find((d) => d.id === id);
|
|
305
|
+
if (!entry) throw coded("unknown_deck", `no registered deck ${id}`);
|
|
306
|
+
const other = reg.decks.find((d) => d.id !== id && d.path === real);
|
|
307
|
+
if (other) throw coded("path_registered", `${real} is already registered as ${other.name} (${other.id})`, { deckId: other.id });
|
|
308
|
+
entry.path = real;
|
|
309
|
+
entry.name = entry.name || deckSlug(real);
|
|
310
|
+
entry.missing = false;
|
|
311
|
+
entry.format = deckFormat(real);
|
|
312
|
+
entry.lastSeenAt = new Date().toISOString();
|
|
313
|
+
return { ...entry };
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** Drop the entry only. Never touches the deck's files. */
|
|
318
|
+
const removeDeckMutator = (id) => (reg) => {
|
|
319
|
+
const i = reg.decks.findIndex((d) => d.id === id);
|
|
320
|
+
if (i === -1) return false;
|
|
321
|
+
reg.decks.splice(i, 1);
|
|
322
|
+
return true;
|
|
323
|
+
};
|
|
324
|
+
export const removeDeck = (id) => update(removeDeckMutator(id));
|
|
325
|
+
/** `removeDeck` for the daemon: see `addDeckAsync`. */
|
|
326
|
+
export const removeDeckAsync = (id) => updateAsync(removeDeckMutator(id));
|
|
327
|
+
|
|
328
|
+
// ---------------------------------------------------------------------------
|
|
329
|
+
// roots
|
|
330
|
+
// ---------------------------------------------------------------------------
|
|
331
|
+
|
|
332
|
+
export function addRoot(p) {
|
|
333
|
+
const real = fs.realpathSync(path.resolve(p));
|
|
334
|
+
if (!fs.statSync(real).isDirectory()) throw coded("not_a_directory", `${real} is not a directory`);
|
|
335
|
+
return update((reg) => {
|
|
336
|
+
if (!reg.roots.includes(real)) reg.roots.push(real);
|
|
337
|
+
return real;
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export function listRoots() {
|
|
342
|
+
return readRegistry().roots.slice();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ---------------------------------------------------------------------------
|
|
346
|
+
// which CLI populated the library
|
|
347
|
+
// ---------------------------------------------------------------------------
|
|
348
|
+
|
|
349
|
+
/** The CLI version that last populated the library, or null (never / before this field). */
|
|
350
|
+
export function registryVersion() {
|
|
351
|
+
return readRegistry().cliVersion ?? null;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export function recordRegistryVersion(version) {
|
|
355
|
+
return update((reg) => {
|
|
356
|
+
reg.cliVersion = version;
|
|
357
|
+
return version;
|
|
358
|
+
});
|
|
359
|
+
}
|