slidemind 0.2.2 → 0.2.3
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 +8 -1
- package/AGENTS.md +57 -11
- package/README.md +78 -34
- package/assets/app-icon.png +0 -0
- package/bin/slidemind.mjs +328 -100
- package/package.json +2 -1
- package/skills/create-slides/SKILL.md +15 -15
- package/skills/search/SKILL.md +6 -6
- package/src/bridge/client.mjs +11 -10
- package/src/bridge/config.mjs +2 -2
- package/src/bridge/{pair.mjs → http-json.mjs} +9 -24
- package/src/bridge/setup-claim.mjs +127 -0
- package/src/installers/claude.mjs +24 -0
- package/src/installers/codex.mjs +60 -4
- package/src/installers/lib.mjs +49 -19
- package/src/installers/opencode.mjs +16 -0
- package/src/installers/tui.mjs +21 -10
package/src/installers/lib.mjs
CHANGED
|
@@ -113,7 +113,7 @@ const CURSOR_SHOW = "\x1b[?25h";
|
|
|
113
113
|
// uniform.
|
|
114
114
|
function startWordmarkAnimation() {
|
|
115
115
|
const lines = wordmarkFrame(0);
|
|
116
|
-
|
|
116
|
+
tui.out("\n" + lines.join("\n") + "\n\n");
|
|
117
117
|
return () => {};
|
|
118
118
|
}
|
|
119
119
|
|
|
@@ -125,6 +125,30 @@ export function isForce(flags) {
|
|
|
125
125
|
return flags.includes("--force") || flags.includes("-F");
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
// Is `name` an executable on the user's PATH? Used as the second half of
|
|
129
|
+
// harness detection: a coding agent's config dir (~/.claude, ~/.codex,
|
|
130
|
+
// ~/.config/opencode) is created by USE, not by install, so a freshly installed
|
|
131
|
+
// agent that has never been launched has no directory at all — its binary is
|
|
132
|
+
// the only evidence it exists. Resolved by walking PATH rather than shelling
|
|
133
|
+
// out to which/where, so it costs nothing and behaves the same on Windows.
|
|
134
|
+
export function binaryOnPath(name) {
|
|
135
|
+
const exts =
|
|
136
|
+
process.platform === "win32"
|
|
137
|
+
? (process.env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";")
|
|
138
|
+
: [""];
|
|
139
|
+
for (const dir of (process.env.PATH || "").split(path.delimiter)) {
|
|
140
|
+
if (!dir) continue;
|
|
141
|
+
for (const ext of exts) {
|
|
142
|
+
try {
|
|
143
|
+
if (fs.statSync(path.join(dir, name + ext)).isFile()) return true;
|
|
144
|
+
} catch {
|
|
145
|
+
/* not there / unreadable — keep looking */
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
|
|
128
152
|
export function expandHome(p) {
|
|
129
153
|
if (p.startsWith("~/")) return path.join(homeDir(), p.slice(2));
|
|
130
154
|
return p;
|
|
@@ -234,6 +258,12 @@ let _startedAt = 0;
|
|
|
234
258
|
let _stopWordmark = null;
|
|
235
259
|
|
|
236
260
|
export const ui = {
|
|
261
|
+
// Suppress ALL decorative output. `slidemind setup --json` runs the real
|
|
262
|
+
// installers but owns stdout for its machine-readable payload, so the rail
|
|
263
|
+
// would corrupt the very thing the caller is parsing.
|
|
264
|
+
silence(value = true) {
|
|
265
|
+
tui.setSilent(value);
|
|
266
|
+
},
|
|
237
267
|
// `wordmark: false` skips painting the wordmark — used when the caller
|
|
238
268
|
// (e.g. the install.sh bootstrap) already printed it, so we only emit the
|
|
239
269
|
// `v… · action` intro line and continue the rail.
|
|
@@ -276,12 +306,12 @@ export const ui = {
|
|
|
276
306
|
// Manual emission so we can animate the title in place. Symbol
|
|
277
307
|
// sits at col 1; " " padding lands the title at col 4.
|
|
278
308
|
tui.bumpRow(1);
|
|
279
|
-
|
|
309
|
+
tui.out(tui.rail() + "\n");
|
|
280
310
|
tui.bumpRow(1);
|
|
281
|
-
|
|
311
|
+
tui.out(SYM.step + " ");
|
|
282
312
|
await tui.shimmer(title, { startCol: 4, restColor: (s) => s });
|
|
283
|
-
if (sub)
|
|
284
|
-
|
|
313
|
+
if (sub) tui.out(" " + brand.slate(tildify(sub)));
|
|
314
|
+
tui.out("\n");
|
|
285
315
|
},
|
|
286
316
|
step({ status = "ok", label, value, note }) {
|
|
287
317
|
const symbol =
|
|
@@ -328,15 +358,15 @@ export const ui = {
|
|
|
328
358
|
|
|
329
359
|
const boxLine = (content) => {
|
|
330
360
|
const pad = Math.max(0, inner - visibleLength(content));
|
|
331
|
-
|
|
361
|
+
tui.out(slate("│") + content + " ".repeat(pad) + slate("│") + "\n");
|
|
332
362
|
};
|
|
333
363
|
|
|
334
|
-
|
|
364
|
+
tui.out("\n");
|
|
335
365
|
|
|
336
366
|
// Top: ╭─ installed ────╮
|
|
337
367
|
const labelText = " installed ";
|
|
338
368
|
const fillerLen = Math.max(2, inner - 1 - labelText.length);
|
|
339
|
-
|
|
369
|
+
tui.out(
|
|
340
370
|
slate("╭─") +
|
|
341
371
|
dim(labelText) +
|
|
342
372
|
slate("─".repeat(fillerLen)) +
|
|
@@ -359,10 +389,10 @@ export const ui = {
|
|
|
359
389
|
boxLine(" " + dim(stats));
|
|
360
390
|
|
|
361
391
|
// Bottom: ╰────────╯
|
|
362
|
-
|
|
392
|
+
tui.out(slate("╰" + "─".repeat(inner) + "╯") + "\n");
|
|
363
393
|
|
|
364
|
-
|
|
365
|
-
|
|
394
|
+
tui.out("\n");
|
|
395
|
+
tui.out(`${SYM.success} ${brand.bold(action)}\n`);
|
|
366
396
|
|
|
367
397
|
// Stop the wordmark animation last — that resets the scroll region
|
|
368
398
|
// and shows the cursor. The wordmark itself stays painted at the
|
|
@@ -401,16 +431,16 @@ export const ui = {
|
|
|
401
431
|
// Reserve two rows: rail spacer + the active spinner line. Both
|
|
402
432
|
// count toward the gradient row counter.
|
|
403
433
|
tui.bumpRow(1);
|
|
404
|
-
|
|
434
|
+
tui.out(tui.rail() + "\n");
|
|
405
435
|
tui.bumpRow(1);
|
|
406
|
-
|
|
436
|
+
tui.out(
|
|
407
437
|
`${brand.techBlue40(SPINNER_FRAMES[0])} ${fmtStep({ label, value, note })}`,
|
|
408
438
|
);
|
|
409
439
|
let frame = 0;
|
|
410
440
|
const timer = setInterval(() => {
|
|
411
441
|
frame = (frame + 1) % SPINNER_FRAMES.length;
|
|
412
|
-
|
|
413
|
-
|
|
442
|
+
tui.out("\r\x1b[2K");
|
|
443
|
+
tui.out(
|
|
414
444
|
`${brand.techBlue40(SPINNER_FRAMES[frame])} ${fmtStep({ label, value, note })}`,
|
|
415
445
|
);
|
|
416
446
|
}, 80);
|
|
@@ -418,15 +448,15 @@ export const ui = {
|
|
|
418
448
|
const result = await fn();
|
|
419
449
|
const time = Date.now() - start;
|
|
420
450
|
clearInterval(timer);
|
|
421
|
-
|
|
422
|
-
|
|
451
|
+
tui.out("\r\x1b[2K");
|
|
452
|
+
tui.out(
|
|
423
453
|
`${SYM.success} ${fmtStep({ label, value, note: finalNote ?? note, time })}\n`,
|
|
424
454
|
);
|
|
425
455
|
return result;
|
|
426
456
|
} catch (err) {
|
|
427
457
|
clearInterval(timer);
|
|
428
|
-
|
|
429
|
-
|
|
458
|
+
tui.out("\r\x1b[2K");
|
|
459
|
+
tui.out(
|
|
430
460
|
`${SYM.error} ${fmtStep({ label, value, note: "failed" })}\n`,
|
|
431
461
|
);
|
|
432
462
|
throw err;
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import fs from "node:fs";
|
|
18
18
|
import path from "node:path";
|
|
19
19
|
import {
|
|
20
|
+
binaryOnPath,
|
|
20
21
|
expandHome,
|
|
21
22
|
installDir,
|
|
22
23
|
isForce,
|
|
@@ -117,6 +118,21 @@ function removeLegacyArtifacts() {
|
|
|
117
118
|
}
|
|
118
119
|
}
|
|
119
120
|
|
|
121
|
+
// See claude.mjs `detect()`. opencode has no slidemind plugin channel, so the
|
|
122
|
+
// skills can only ever come from here.
|
|
123
|
+
export function detect() {
|
|
124
|
+
const root = opencodeRoot();
|
|
125
|
+
const configured = fs.existsSync(root);
|
|
126
|
+
const onPath = binaryOnPath("opencode");
|
|
127
|
+
return {
|
|
128
|
+
root,
|
|
129
|
+
configured,
|
|
130
|
+
onPath,
|
|
131
|
+
present: configured || onPath,
|
|
132
|
+
pluginInstalled: false,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
120
136
|
export async function install({ packageRoot, flags }) {
|
|
121
137
|
const link = isLinkMode(flags);
|
|
122
138
|
const force = isForce(flags);
|
package/src/installers/tui.mjs
CHANGED
|
@@ -23,6 +23,17 @@ const FADE_ROWS = 28; // rail finishes fading after this many emitted rows
|
|
|
23
23
|
|
|
24
24
|
let _row = 0;
|
|
25
25
|
|
|
26
|
+
// Every terminal write in the installer UI funnels through here, so a caller
|
|
27
|
+
// whose stdout carries machine-readable output (`slidemind setup --json`) can
|
|
28
|
+
// silence the decorative rail without each writer knowing about it.
|
|
29
|
+
let _silent = false;
|
|
30
|
+
export function setSilent(value) {
|
|
31
|
+
_silent = Boolean(value);
|
|
32
|
+
}
|
|
33
|
+
export function out(text) {
|
|
34
|
+
if (!_silent) process.stdout.write(text);
|
|
35
|
+
}
|
|
36
|
+
|
|
26
37
|
// Linear interpolate between #9999FF (techBlue40) and #6C7D8C (slate).
|
|
27
38
|
// Top of the rail is bright tech-blue, fading to brand slate as the
|
|
28
39
|
// install progresses — feels like the deck's tech-blue gradation card.
|
|
@@ -52,7 +63,7 @@ export function bumpRow(n = 1) {
|
|
|
52
63
|
|
|
53
64
|
export function intro(text) {
|
|
54
65
|
_row = 0;
|
|
55
|
-
|
|
66
|
+
out("\n" + paint(_row, "┌") + " " + text + "\n");
|
|
56
67
|
}
|
|
57
68
|
|
|
58
69
|
// Each message starts with its own gradient-coloured rail spacer, then
|
|
@@ -60,28 +71,28 @@ export function intro(text) {
|
|
|
60
71
|
// (◆ ● ▲ ■ etc.); when omitted we fall back to a continuation rail.
|
|
61
72
|
export function message(text, { symbol } = {}) {
|
|
62
73
|
_row += 1;
|
|
63
|
-
|
|
74
|
+
out(paint(_row, "│") + "\n");
|
|
64
75
|
_row += 1;
|
|
65
76
|
const sym = symbol ?? paint(_row, "│");
|
|
66
|
-
|
|
77
|
+
out(sym + " " + text + "\n");
|
|
67
78
|
}
|
|
68
79
|
|
|
69
80
|
export function outro(text) {
|
|
70
81
|
_row += 1;
|
|
71
|
-
|
|
82
|
+
out(paint(_row, "│") + "\n");
|
|
72
83
|
_row += 1;
|
|
73
|
-
|
|
84
|
+
out(paint(_row, "└") + " " + text + "\n\n");
|
|
74
85
|
_row = 0;
|
|
75
86
|
}
|
|
76
87
|
|
|
77
88
|
// Reserve the next two rows for a spin step: emits the rail spacer,
|
|
78
89
|
// returns the row index the spinner is written on. Callers should
|
|
79
|
-
// `
|
|
90
|
+
// `out(...)` the spinner content themselves (without a
|
|
80
91
|
// trailing `\n`) so they can overwrite it via `\r`. When the operation
|
|
81
92
|
// completes they call `commitSpinRow()` after writing the final line.
|
|
82
93
|
export function reserveSpinRows() {
|
|
83
94
|
_row += 1;
|
|
84
|
-
|
|
95
|
+
out(paint(_row, "│") + "\n");
|
|
85
96
|
_row += 1;
|
|
86
97
|
return _row;
|
|
87
98
|
}
|
|
@@ -96,7 +107,7 @@ const ANIMATE = !NO_COLOR && process.stdout.isTTY;
|
|
|
96
107
|
export async function shimmer(text, { startCol = 1, restColor }) {
|
|
97
108
|
const final = brand.bold(restColor(text));
|
|
98
109
|
if (!ANIMATE) {
|
|
99
|
-
|
|
110
|
+
out(final);
|
|
100
111
|
return;
|
|
101
112
|
}
|
|
102
113
|
const len = text.length;
|
|
@@ -117,9 +128,9 @@ export async function shimmer(text, { startCol = 1, restColor }) {
|
|
|
117
128
|
else painter = (s) => brand.dim(brand.slate(s));
|
|
118
129
|
line += brand.bold(painter(text[i]));
|
|
119
130
|
}
|
|
120
|
-
|
|
131
|
+
out(`\x1b[${startCol}G\x1b[K` + line);
|
|
121
132
|
if (f < frames) await new Promise((r) => setTimeout(r, frameMs));
|
|
122
133
|
}
|
|
123
134
|
// Final settle.
|
|
124
|
-
|
|
135
|
+
out(`\x1b[${startCol}G\x1b[K` + final);
|
|
125
136
|
}
|