slidemind 0.3.20 → 0.4.1

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.
Files changed (187) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/AGENTS.md +592 -16
  4. package/LICENSE +4 -0
  5. package/README.md +203 -8
  6. package/THIRD-PARTY-NOTICES.md +201 -0
  7. package/bin/slidemind.mjs +50 -2
  8. package/package.json +5 -3
  9. package/skills/create-slides/SKILL.md +484 -886
  10. package/skills/search/SKILL.md +46 -23
  11. package/src/bridge/build-and-push.mjs +548 -73
  12. package/src/bridge/bundle.mjs +100 -31
  13. package/src/bridge/capture.mjs +14 -17
  14. package/src/bridge/client/brands.mjs +157 -18
  15. package/src/bridge/client/engine-routing.mjs +48 -13
  16. package/src/bridge/client/engine-search.mjs +24 -1
  17. package/src/bridge/client/render.mjs +27 -1
  18. package/src/bridge/client/transport.mjs +499 -23
  19. package/src/bridge/client.mjs +10 -0
  20. package/src/bridge/config.mjs +67 -6
  21. package/src/bridge/engine-download.mjs +0 -10
  22. package/src/bridge/graph-auth.mjs +55 -3
  23. package/src/bridge/image-intent-envelope.mjs +239 -0
  24. package/src/bridge/package-preview.mjs +67 -0
  25. package/src/bridge/sharepoint.mjs +914 -0
  26. package/src/builders/asset-fetch.mjs +203 -0
  27. package/src/builders/block-dividers.mjs +83 -0
  28. package/src/builders/build.mjs +171 -87
  29. package/src/builders/card-height.mjs +294 -0
  30. package/src/builders/chart-hole.mjs +183 -0
  31. package/src/builders/chart-jsx.mjs +410 -0
  32. package/src/builders/chart-labels.mjs +326 -0
  33. package/src/builders/concept-geometry.mjs +370 -0
  34. package/src/builders/concept-intent.mjs +200 -0
  35. package/src/builders/concept-layout.mjs +79 -0
  36. package/src/builders/concept-lines.mjs +509 -0
  37. package/src/builders/concept-lists.mjs +270 -0
  38. package/src/builders/concept-marks.mjs +84 -0
  39. package/src/builders/concept-relationships.mjs +12 -0
  40. package/src/builders/concept-spec.mjs +645 -0
  41. package/src/builders/concept-text.mjs +140 -0
  42. package/src/builders/concept-waves.mjs +57 -0
  43. package/src/builders/copy-fit.mjs +139 -0
  44. package/src/builders/design-tokens.mjs +347 -0
  45. package/src/builders/draft-check.mjs +552 -340
  46. package/src/builders/draft-lines.mjs +369 -0
  47. package/src/builders/edit-route.mjs +29 -0
  48. package/src/builders/figure-sketch.mjs +375 -0
  49. package/src/builders/fold-sets.mjs +639 -0
  50. package/src/builders/gate-server.mjs +9 -4
  51. package/src/builders/gate-session.js +273 -12
  52. package/src/builders/gate-shell.js +1494 -0
  53. package/src/builders/image-intent-check.mjs +308 -0
  54. package/src/builders/image-use-reconcile.mjs +107 -0
  55. package/src/builders/kit-helpers.mjs +121 -0
  56. package/src/builders/layout-names.mjs +32 -0
  57. package/src/builders/legacy-reference-uses.mjs +179 -0
  58. package/src/builders/line-fit.mjs +224 -0
  59. package/src/builders/line-native.mjs +895 -0
  60. package/src/builders/line-relations.mjs +230 -0
  61. package/src/builders/lint-fix.mjs +117 -9
  62. package/src/builders/lint.mjs +25 -20
  63. package/src/builders/list-jsx.mjs +88 -0
  64. package/src/builders/placement.mjs +133 -0
  65. package/src/builders/plan-checks.mjs +135 -0
  66. package/src/builders/plan-comments.mjs +19 -0
  67. package/src/builders/plan-handlers.mjs +95 -0
  68. package/src/builders/plan-markdown-edit.mjs +3 -3
  69. package/src/builders/plan-overlay.js +1530 -1608
  70. package/src/builders/plan.mjs +689 -186
  71. package/src/builders/preset-definitions.json +1 -0
  72. package/src/builders/preset-eval.mjs +244 -0
  73. package/src/builders/preview.html +51 -120
  74. package/src/builders/red-thread-backbone.mjs +135 -36
  75. package/src/builders/repair.mjs +205 -0
  76. package/src/builders/repeat-groups.mjs +540 -0
  77. package/src/builders/resolve-skill.mjs +5 -4
  78. package/src/builders/review-overlay.js +296 -293
  79. package/src/builders/review.mjs +305 -84
  80. package/src/builders/rule-shift.mjs +96 -0
  81. package/src/builders/seat.mjs +354 -0
  82. package/src/builders/settle-driver.mjs +236 -0
  83. package/src/builders/settle.mjs +1448 -0
  84. package/src/builders/slide-prep.mjs +67 -0
  85. package/src/builders/spec-assets.mjs +236 -0
  86. package/src/builders/spec-match.mjs +56 -0
  87. package/src/builders/stack.mjs +413 -0
  88. package/src/builders/storyboard-markdown.mjs +70 -27
  89. package/src/builders/storyboard-schema.mjs +54 -114
  90. package/src/builders/text-blocks.mjs +212 -0
  91. package/src/builders/text-card-fit.mjs +552 -0
  92. package/src/builders/text-stack.mjs +276 -0
  93. package/src/builders/transcribe.mjs +1069 -0
  94. package/src/cli/assets.mjs +164 -0
  95. package/src/cli/common.mjs +173 -12
  96. package/src/cli/concept-spec-support.mjs +76 -0
  97. package/src/cli/concepts.mjs +1002 -103
  98. package/src/cli/deck.mjs +401 -65
  99. package/src/cli/diagnose.mjs +4 -2
  100. package/src/cli/engine-helpers.mjs +17 -10
  101. package/src/cli/engine-results.mjs +76 -3
  102. package/src/cli/help.mjs +232 -28
  103. package/src/cli/install.mjs +9 -0
  104. package/src/cli/maintenance.mjs +104 -0
  105. package/src/cli/read.mjs +2 -1
  106. package/src/cli/ready.mjs +6 -3
  107. package/src/cli/record.mjs +522 -0
  108. package/src/cli/remote.mjs +818 -0
  109. package/src/cli/revision.mjs +686 -0
  110. package/src/cli/search.mjs +30 -11
  111. package/src/cli/session.mjs +56 -13
  112. package/src/cli/setup.mjs +46 -2
  113. package/src/cli/status.mjs +142 -33
  114. package/src/cli/tasks.mjs +246 -0
  115. package/src/cli/transcribe.mjs +854 -0
  116. package/src/cli/workspace-ui.mjs +260 -0
  117. package/src/cli/workspace.mjs +212 -34
  118. package/src/install-channel.mjs +64 -7
  119. package/src/lib/adopt.mjs +346 -25
  120. package/src/lib/concept-spec-binding.mjs +96 -0
  121. package/src/lib/conversions.mjs +70 -6
  122. package/src/lib/convert-folded.mjs +107 -0
  123. package/src/lib/deck-paths.mjs +418 -0
  124. package/src/lib/durable-ids.mjs +47 -0
  125. package/src/lib/edit-operations.mjs +316 -0
  126. package/src/lib/edit-state.mjs +1244 -0
  127. package/src/lib/edit-transitions.mjs +120 -0
  128. package/src/lib/extensions.mjs +501 -0
  129. package/src/lib/image-assets.mjs +465 -0
  130. package/src/lib/image-derivatives.mjs +234 -0
  131. package/src/lib/image-intent-fingerprint.mjs +70 -0
  132. package/src/lib/image-intent-tags.mjs +176 -0
  133. package/src/lib/image-intent.mjs +802 -0
  134. package/src/lib/image-use-overrides.mjs +279 -0
  135. package/src/lib/inline-components.mjs +613 -0
  136. package/src/lib/lint-converted.mjs +68 -4
  137. package/src/lib/merge-json.mjs +163 -0
  138. package/src/lib/overwrite-guard.mjs +3 -3
  139. package/src/lib/presentation-context.mjs +531 -0
  140. package/src/lib/preservation-tags.mjs +157 -0
  141. package/src/lib/preservation-verdict.mjs +1342 -0
  142. package/src/lib/publish.mjs +1557 -0
  143. package/src/lib/rebase.mjs +945 -0
  144. package/src/lib/reconcile-ack.mjs +4 -6
  145. package/src/lib/reconcile.mjs +62 -24
  146. package/src/lib/record-client.mjs +175 -0
  147. package/src/lib/record-state.mjs +151 -0
  148. package/src/lib/remote.mjs +661 -0
  149. package/src/lib/revision.mjs +1210 -0
  150. package/src/lib/slide-guids.mjs +4 -6
  151. package/src/lib/slide-preservation.mjs +423 -0
  152. package/src/lib/slide-selection.mjs +9 -0
  153. package/src/lib/workspace-transaction.mjs +117 -0
  154. package/src/pptx/content-digest.mjs +334 -0
  155. package/src/pptx/deck-assemble.mjs +1278 -0
  156. package/src/pptx/fill/engine.mjs +2 -3
  157. package/src/pptx/fill/freeform.mjs +6 -3
  158. package/src/pptx/ooxml.mjs +20 -0
  159. package/src/pptx/package-evidence.mjs +383 -0
  160. package/src/pptx/tags.mjs +163 -11
  161. package/src/update-check.mjs +22 -4
  162. package/src/workspace/adapters.mjs +222 -0
  163. package/src/workspace/auth.mjs +256 -0
  164. package/src/workspace/autostart.mjs +204 -0
  165. package/src/workspace/daemon.mjs +360 -0
  166. package/src/workspace/discovery.mjs +180 -0
  167. package/src/workspace/edit.mjs +167 -0
  168. package/src/workspace/events.mjs +52 -0
  169. package/src/workspace/gate-client.mjs +290 -0
  170. package/src/workspace/handback.mjs +73 -0
  171. package/src/workspace/lifecycle.mjs +635 -0
  172. package/src/workspace/operations.mjs +200 -0
  173. package/src/workspace/pick-folder.mjs +152 -0
  174. package/src/workspace/registry.mjs +359 -0
  175. package/src/workspace/remote-links.mjs +397 -0
  176. package/src/workspace/remote-status.mjs +359 -0
  177. package/src/workspace/routes.mjs +1331 -0
  178. package/src/workspace/server.mjs +159 -0
  179. package/src/workspace/sync.mjs +1119 -0
  180. package/src/workspace/task-actions.mjs +215 -0
  181. package/src/workspace/tasks.mjs +507 -0
  182. package/src/workspace/ui/app.js +1886 -0
  183. package/src/workspace/ui/index.html +141 -0
  184. package/src/workspace/ui/plan-bridge.js +120 -0
  185. package/src/workspace/ui/sharepoint.svg +59 -0
  186. package/src/workspace/ui/styles.css +423 -0
  187. package/src/builders/visual-families.mjs +0 -46
@@ -0,0 +1,200 @@
1
+ // Per-deck mutation wrapper for the persistent workspace.
2
+ //
3
+ // Every supported writer — browser save, task response, adapter action, sync
4
+ // apply — runs through `runDeckOperation`. It reuses the ONE deck lock
5
+ // (`acquireOwner`, revision.mjs) and adds what the lock alone does not give:
6
+ //
7
+ // - a per-deck in-process queue, so two requests inside the daemon never race
8
+ // for the same deck and re-entry from inside an operation fails loudly
9
+ // instead of deadlocking on its own queue;
10
+ // - a durable operation intent (`state/workspace-operation.json`, atomic
11
+ // temp+rename) written BEFORE the mutation and cleared after it, so a crash
12
+ // mid-commit is found by the next writer as `recovery_required` rather than
13
+ // silently half-applied;
14
+ // - a busy answer (`deck_busy`) for a live agent session lock. A session is
15
+ // never stolen; the browser keeps its buffer and retries.
16
+ //
17
+ // Network work is never done under the lock. A remote save is two operations:
18
+ // one to claim and snapshot, the network call outside, one to apply. The queue
19
+ // keeps the pair ordered against other local writers; the sync record
20
+ // (`workspace-sync.json`, sync lane) keeps the network claim.
21
+
22
+ import { AsyncLocalStorage } from "node:async_hooks";
23
+ import { randomUUID } from "node:crypto";
24
+ import fs from "node:fs";
25
+ import path from "node:path";
26
+
27
+ import { acquireOwner } from "../lib/revision.mjs";
28
+
29
+ export const OPERATION_FILE = "workspace-operation.json";
30
+ export const SCHEMA_VERSION = 1;
31
+
32
+ const queues = new Map(); // deck key → tail promise
33
+ const active = new AsyncLocalStorage(); // deck key of the operation this code runs inside
34
+
35
+ const nowIso = () => new Date().toISOString();
36
+
37
+ export function operationPath(workspaceDir) {
38
+ return path.join(workspaceDir, "state", OPERATION_FILE);
39
+ }
40
+
41
+ function deckKey(workspaceDir) {
42
+ try {
43
+ return fs.realpathSync(workspaceDir);
44
+ } catch {
45
+ return path.resolve(workspaceDir);
46
+ }
47
+ }
48
+
49
+ function writeAtomic(file, data) {
50
+ fs.mkdirSync(path.dirname(file), { recursive: true });
51
+ const tmp = `${file}.${process.pid}.${randomUUID().slice(0, 8)}.tmp`;
52
+ fs.writeFileSync(tmp, JSON.stringify(data, null, 2) + "\n");
53
+ fs.renameSync(tmp, file);
54
+ }
55
+
56
+ function typed(code, message, extra = {}) {
57
+ return Object.assign(new Error(message), { code, ...extra });
58
+ }
59
+
60
+ /**
61
+ * The unfinished operation on a deck, if any.
62
+ *
63
+ * `{ intent }` for a readable record, `{ malformed: true, raw }` for a file that
64
+ * is there but not a v1 record (kept on disk — it is recovery evidence, not
65
+ * garbage), `null` when the deck is clean.
66
+ */
67
+ export function readOperationIntent(workspaceDir) {
68
+ const file = operationPath(workspaceDir);
69
+ let raw;
70
+ try {
71
+ raw = fs.readFileSync(file, "utf8");
72
+ } catch (err) {
73
+ if (err?.code === "ENOENT") return null;
74
+ throw err;
75
+ }
76
+ try {
77
+ const intent = JSON.parse(raw);
78
+ if (intent?.schemaVersion === SCHEMA_VERSION && typeof intent.id === "string") return { intent };
79
+ return { malformed: true, raw };
80
+ } catch {
81
+ return { malformed: true, raw };
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Finish or refuse an unfinished operation. Runs before every mutation.
87
+ *
88
+ * `recover(intent)` is the caller's chance to complete or roll back what the
89
+ * intent describes (it has the before/after hashes the operation noted). It
90
+ * returns truthy when the deck is consistent again; the intent is then cleared.
91
+ * Without a recoverer, or when it declines, the deck stays as it is and
92
+ * `recovery_required` carries the intent — nobody manufactures a result.
93
+ */
94
+ export function recoverDeckOperation(workspaceDir, { recover = null } = {}) {
95
+ const pending = readOperationIntent(workspaceDir);
96
+ if (!pending) return { recovered: false, intent: null };
97
+ if (pending.malformed) {
98
+ throw typed(
99
+ "recovery_required",
100
+ `${OPERATION_FILE} is unreadable; inspect it before mutating this deck.`,
101
+ { intent: null, malformed: true },
102
+ );
103
+ }
104
+ const { intent } = pending;
105
+ if (recover && recover(intent)) {
106
+ fs.rmSync(operationPath(workspaceDir), { force: true });
107
+ return { recovered: true, intent };
108
+ }
109
+ throw typed(
110
+ "recovery_required",
111
+ `operation ${intent.id} (${intent.kind}) did not finish; recover it before mutating this deck.`,
112
+ { intent },
113
+ );
114
+ }
115
+
116
+ /**
117
+ * Run one mutation on a deck under its lock, serialized with every other
118
+ * operation on that deck in this process.
119
+ *
120
+ * `fn(op)` receives `{ token, generation, intent, note(patch) }`. `token` is
121
+ * the owner token to hand to a CHILD adapter process for this operation only;
122
+ * it never goes to a browser. `note(patch)` merges evidence (before/after
123
+ * hashes, partial results) into the durable intent so recovery can finish or
124
+ * refuse precisely.
125
+ *
126
+ * Options: `operationId` (default a fresh UUID), `kind`, `expected` (hashes the
127
+ * caller verified before asking), `command` (lock label), `owner` — `undefined`
128
+ * inherits `SLIDEMIND_OWNER` (a headless command re-entering its own session),
129
+ * a string re-enters that token, `false` refuses to inherit any token (the
130
+ * daemon: it is nobody's session), `recover` (see recoverDeckOperation).
131
+ *
132
+ * Throws `deck_busy` (live session or another process), `recovery_required`,
133
+ * `operation_reentrant` (fn called runDeckOperation on the same deck — the
134
+ * queue would wait on itself), or whatever `fn` throws — with the intent kept
135
+ * on disk for the next writer to recover.
136
+ */
137
+ export async function runDeckOperation(workspaceDir, options, fn) {
138
+ if (typeof options === "function") {
139
+ fn = options;
140
+ options = {};
141
+ }
142
+ const { operationId = randomUUID(), kind = "mutation", expected = null, command = "workspace", owner, recover = null } =
143
+ options ?? {};
144
+ const key = deckKey(workspaceDir);
145
+ if (active.getStore() === key) {
146
+ throw typed("operation_reentrant", "an operation may not start another on the same deck; split it or compose the work.");
147
+ }
148
+ const prev = queues.get(key) ?? Promise.resolve();
149
+ const run = prev.catch(() => {}).then(() => active.run(key, () => locked()));
150
+ queues.set(key, run);
151
+ try {
152
+ return await run;
153
+ } finally {
154
+ if (queues.get(key) === run) queues.delete(key);
155
+ }
156
+
157
+ async function locked() {
158
+ const held = takeOwner();
159
+ try {
160
+ recoverDeckOperation(workspaceDir, { recover });
161
+ const file = operationPath(workspaceDir);
162
+ let intent = {
163
+ schemaVersion: SCHEMA_VERSION,
164
+ id: operationId,
165
+ kind,
166
+ expected,
167
+ pid: process.pid,
168
+ startedAt: nowIso(),
169
+ notes: {},
170
+ };
171
+ writeAtomic(file, intent);
172
+ const result = await fn({
173
+ token: held.token,
174
+ generation: held.generation,
175
+ get intent() {
176
+ return intent;
177
+ },
178
+ note(patch) {
179
+ intent = { ...intent, notes: { ...intent.notes, ...patch }, updatedAt: nowIso() };
180
+ writeAtomic(file, intent);
181
+ return intent;
182
+ },
183
+ });
184
+ fs.rmSync(file, { force: true });
185
+ return result;
186
+ } finally {
187
+ held.release();
188
+ }
189
+ }
190
+
191
+ function takeOwner() {
192
+ const env = process.env.SLIDEMIND_OWNER;
193
+ if (owner === false) delete process.env.SLIDEMIND_OWNER;
194
+ try {
195
+ return acquireOwner(workspaceDir, { command, owner: typeof owner === "string" ? owner : null });
196
+ } finally {
197
+ if (owner === false && env !== undefined) process.env.SLIDEMIND_OWNER = env;
198
+ }
199
+ }
200
+ }
@@ -0,0 +1,152 @@
1
+ // The OS folder dialog, opened by the daemon on the user's behalf. A browser
2
+ // page can never learn a picked folder's absolute path, so the workspace page
3
+ // asks the daemon, which owns the machine, to show Finder / Explorer / zenity
4
+ // and hand the choice back. Resolves to the absolute path, or null when the
5
+ // user cancelled. Throws `picker_unavailable` when no dialog can be shown
6
+ // (headless Linux without zenity/kdialog, a remote session).
7
+ import { execFile } from "node:child_process";
8
+ import fs from "node:fs";
9
+ import os from "node:os";
10
+ import path from "node:path";
11
+
12
+ const DIALOG_TIMEOUT_MS = 10 * 60_000; // the dialog waits for a human
13
+
14
+ const commands = {
15
+ darwin: (title) => ["osascript", ["-e", 'tell application "System Events" to activate', "-e", `POSIX path of (choose folder with prompt "${title}")`]],
16
+ win32: (title) => [
17
+ "powershell",
18
+ [
19
+ "-NoProfile",
20
+ "-STA",
21
+ "-Command",
22
+ `Add-Type -AssemblyName System.Windows.Forms; $d = New-Object System.Windows.Forms.FolderBrowserDialog; $d.Description = '${title}'; $d.ShowNewFolderButton = $false; if ($d.ShowDialog() -eq 'OK') { $d.SelectedPath }`,
23
+ ],
24
+ ],
25
+ linux: (title) => ["zenity", ["--file-selection", "--directory", `--title=${title}`]],
26
+ };
27
+
28
+ let inFlight = null; // one dialog at a time; a second request joins the first
29
+
30
+ export function pickFolder({ title = "Choose a deck folder" } = {}) {
31
+ if (inFlight) return inFlight;
32
+ const safeTitle = title.replace(/["'\\]/g, "");
33
+ const [cmd, args] = (commands[process.platform] ?? commands.linux)(safeTitle);
34
+ inFlight = new Promise((resolve, reject) => {
35
+ execFile(cmd, args, { timeout: DIALOG_TIMEOUT_MS, windowsHide: false }, (err, stdout) => {
36
+ const out = String(stdout ?? "").trim();
37
+ if (out) return resolve(out.length > 1 ? out.replace(/[\\/]+$/, "") : out);
38
+ // cancel: osascript exits 1 ("User canceled"), zenity exits 1, PowerShell prints nothing
39
+ if (!err || err.code === 1 || /cancel/i.test(err.message ?? "")) return resolve(null);
40
+ const e = new Error(`no folder dialog could be shown here (${err.code === "ENOENT" ? `${cmd} is not installed` : err.message}); type the path instead`);
41
+ e.code = "picker_unavailable";
42
+ reject(e);
43
+ });
44
+ }).finally(() => (inFlight = null));
45
+ return inFlight;
46
+ }
47
+
48
+ function launch(cmd, args, { tolerateExit1 = process.platform === "win32", timeout = 0 } = {}) {
49
+ return new Promise((resolve, reject) => {
50
+ execFile(cmd, args, { windowsHide: true, timeout }, (err, _stdout, stderr) => {
51
+ // explorer.exe answers 1 even when it opened the window; a PowerShell that
52
+ // threw answers 1 too, so a caller that must know opts out
53
+ if (err && !(tolerateExit1 && err.code === 1)) {
54
+ return reject(new Error(err.code === "ENOENT" ? `${cmd} is not installed` : String(stderr || "").trim() || err.message));
55
+ }
56
+ resolve();
57
+ });
58
+ });
59
+ }
60
+
61
+ /** Show `dir` in the OS file manager (Finder / Explorer / xdg-open). */
62
+ export function revealFolder(dir) {
63
+ const [cmd, args] = process.platform === "darwin" ? ["open", [dir]] : process.platform === "win32" ? ["explorer", [dir]] : ["xdg-open", [dir]];
64
+ return launch(cmd, args).catch((err) => {
65
+ const e = new Error(`could not open the folder here (${err.message})`);
66
+ e.code = "reveal_unavailable";
67
+ throw e;
68
+ });
69
+ }
70
+
71
+ /**
72
+ * Open `file` in its desktop app. PowerPoint is named explicitly on macOS —
73
+ * `open` alone hands a .pptx to whatever claims the extension (Keynote on a
74
+ * fresh Mac) — and falls back to the default handler when it is not installed.
75
+ */
76
+ export async function openFile(file) {
77
+ const tries =
78
+ process.platform === "darwin"
79
+ ? [["open", ["-a", "Microsoft PowerPoint", file]], ["open", [file]]]
80
+ : process.platform === "win32"
81
+ ? [["explorer", [file]]]
82
+ : [["xdg-open", [file]]];
83
+ let last = null;
84
+ for (const [cmd, args] of tries) {
85
+ try {
86
+ return await launch(cmd, args);
87
+ } catch (err) {
88
+ last = err;
89
+ }
90
+ }
91
+ const e = new Error(`could not open this file here (${last ? last.message : "no handler"})`);
92
+ e.code = "open_unavailable";
93
+ throw e;
94
+ }
95
+
96
+ const powerShellString = (s) => `'${s.replace(/'/g, "''")}'`;
97
+
98
+ /**
99
+ * Move `dir` to the OS trash. Recoverable on purpose: the workspace never calls
100
+ * fs.rm on a user's deck, so a wrong click is undone from the Trash. Node has no
101
+ * API for this and we add no dependency.
102
+ *
103
+ * macOS moves the folder into ~/.Trash directly. Asking Finder over AppleScript
104
+ * is the documented way and it is NOT usable here: the first call raises the
105
+ * automation-permission prompt and blocks the daemon until a human answers it
106
+ * (observed: `AppleEvent timed out (-1712)` after two minutes). A rename needs
107
+ * no permission and no prompt. Elsewhere the platform's own trash command runs,
108
+ * under a timeout.
109
+ *
110
+ * The result is VERIFIED — a command that exits 0 without removing the folder is
111
+ * a failure, not a success. Throws `trash_unavailable` when this machine has no
112
+ * trash to move it to.
113
+ */
114
+ export async function trashFolder(dir) {
115
+ let failure = null;
116
+ try {
117
+ if (process.platform === "darwin") moveToHomeTrash(dir);
118
+ else if (process.platform === "win32") {
119
+ await launch("powershell", ["-NoProfile", "-Command", `$ErrorActionPreference='Stop'; Add-Type -AssemblyName Microsoft.VisualBasic; [Microsoft.VisualBasic.FileIO.FileSystem]::DeleteDirectory(${powerShellString(dir)}, 'OnlyErrorDialogs', 'SendToRecycleBin')`], { tolerateExit1: false, timeout: TRASH_TIMEOUT_MS });
120
+ } else await launch("gio", ["trash", dir], { tolerateExit1: false, timeout: TRASH_TIMEOUT_MS });
121
+ } catch (err) {
122
+ failure = err;
123
+ }
124
+ if (!fs.existsSync(dir)) return dir;
125
+ const e = new Error(`could not move the folder to the Trash here (${failure ? failure.message : "it is still on disk"}); delete it in your file manager instead`);
126
+ e.code = "trash_unavailable";
127
+ throw e;
128
+ }
129
+
130
+ const TRASH_TIMEOUT_MS = 60_000; // a trash move is local work; a prompt or a hang must not own the daemon
131
+
132
+ /**
133
+ * ~/.Trash/<name>, with " 2", " 3"… when that name is taken, exactly as Finder
134
+ * numbers a collision.
135
+ * ponytail: same-volume only. A deck on an external disk belongs in that
136
+ * volume's /Volumes/X/.Trashes/<uid>; EXDEV is reported instead of copying
137
+ * gigabytes behind the user's back.
138
+ */
139
+ function moveToHomeTrash(dir) {
140
+ const trash = path.join(os.homedir(), ".Trash");
141
+ fs.mkdirSync(trash, { recursive: true });
142
+ const base = path.basename(dir);
143
+ let target = path.join(trash, base);
144
+ for (let n = 2; fs.existsSync(target); n++) target = path.join(trash, `${base} ${n}`);
145
+ try {
146
+ fs.renameSync(dir, target);
147
+ } catch (err) {
148
+ if (err?.code !== "EXDEV") throw err;
149
+ throw new Error(`${dir} is on another volume than your home folder; move it to the Trash in Finder`);
150
+ }
151
+ return target;
152
+ }