@lovinka/vitrinka 1.7.1 → 1.9.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/CHANGELOG.md +45 -0
- package/README.md +22 -5
- package/dist/bin-mcp.js +6 -2
- package/dist/bin-mcp.js.map +1 -1
- package/dist/cli.js +302 -145
- package/dist/cli.js.map +1 -1
- package/dist/mcp.js +18 -6
- package/dist/mcp.js.map +1 -1
- package/package.json +4 -6
- package/skills/artifact/SKILL.md +0 -82
- package/skills/brainstorming/SKILL.md +0 -107
- package/skills/screenshot/SKILL.md +0 -108
- package/skills/screenshot/gallery.mjs +0 -6
- package/skills/vitrinka/cli.ts +0 -39
- package/skills/vitrinka/commands/listen.md +0 -10
- package/skills/vitrinka/listen/SKILL.md +0 -197
- package/skills/vitrinka/tests/_helpers.ts +0 -108
- package/skills/vitrinka/tests/derive.test.ts +0 -118
- package/skills/vitrinka/tests/dx.test.ts +0 -264
- package/skills/vitrinka/tests/embed.test.ts +0 -96
- package/skills/vitrinka/tests/index.test.ts +0 -250
- package/skills/vitrinka/tests/install-name.test.ts +0 -117
- package/skills/vitrinka/tests/manifest.test.ts +0 -98
- package/skills/vitrinka/tests/operator.test.ts +0 -119
- package/skills/vitrinka/tests/push.test.ts +0 -115
- package/skills/vitrinka/tests/scaffold.test.ts +0 -173
- package/skills/vitrinka/tests/shim.test.ts +0 -28
- package/skills/vitrinka/tests/snap.test.ts +0 -175
- package/skills/vitrinka/tests/watch.test.ts +0 -204
package/dist/cli.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// skills/screenshot/gallery.mjs (offline marker, COPYFILE_DISABLE, control-file excludes,
|
|
11
11
|
// `--key`, repeatable `--chip`). gallery.mjs is now a passthrough shim to this file.
|
|
12
12
|
// Design: vitrinka/docs/plans/2026-07-03-hand-in-hand-tooling-design.md
|
|
13
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync, readdirSync, copyFileSync, mkdtempSync, chmodSync, appendFileSync, statSync,
|
|
13
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync, readdirSync, copyFileSync, mkdtempSync, chmodSync, appendFileSync, statSync, openSync, readSync, closeSync, realpathSync, } from 'node:fs';
|
|
14
14
|
import { join, resolve, basename, dirname, extname, relative, sep } from 'node:path';
|
|
15
15
|
import { tmpdir, hostname } from 'node:os';
|
|
16
16
|
import { spawnSync, spawn } from 'node:child_process';
|
|
@@ -1107,7 +1107,9 @@ async function operatorCmd(rest, args) {
|
|
|
1107
1107
|
const pos = positionals(rest).slice(1); // drop the command word itself
|
|
1108
1108
|
const base = (strArg(args.base) || process.env.VITRINKA_URL || process.env.VITRINKA_BASE_URL
|
|
1109
1109
|
|| 'https://vitrinka.lovinka.com').replace(/\/+$/, '');
|
|
1110
|
-
|
|
1110
|
+
// stripControl mirrors the server's 422 on control characters — an
|
|
1111
|
+
// arrow-edited or paste-polluted name must never reach the settings key.
|
|
1112
|
+
const name = stripControl(pos.join(' '));
|
|
1111
1113
|
if (!name) {
|
|
1112
1114
|
const local = readOperator(true);
|
|
1113
1115
|
console.log(`operator (local): ${local || '(unset)'}`);
|
|
@@ -1218,20 +1220,20 @@ async function doctorCmd(args) {
|
|
|
1218
1220
|
console.log(`✓ operator ${op}`);
|
|
1219
1221
|
else
|
|
1220
1222
|
console.log('✗ operator unset — board writes stay anonymous (fix: vitrinka operator <name>)');
|
|
1221
|
-
// Setup surfaces (
|
|
1223
|
+
// Setup surfaces (plugin / shim / MCP / Claude UI) — informational rows, and
|
|
1222
1224
|
// `--fix` repairs everything repairable in place. Token/operator stay manual
|
|
1223
1225
|
// (they're credentials/identity, not installation).
|
|
1224
1226
|
const fix = args.fix === true;
|
|
1225
1227
|
const home = process.env.HOME || '';
|
|
1226
|
-
const
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
if (
|
|
1230
|
-
console.log(
|
|
1231
|
-
else if (fix)
|
|
1232
|
-
|
|
1228
|
+
for (const rt of pluginRuntimes()) {
|
|
1229
|
+
if (!rt.present)
|
|
1230
|
+
continue; // no runtime, nothing to wire
|
|
1231
|
+
if (rt.installed)
|
|
1232
|
+
console.log(`✓ plugin ${rt.name}: vitrinka installed`);
|
|
1233
|
+
else if (fix && installPluginInto(rt))
|
|
1234
|
+
console.log(`✓ plugin ${rt.name}: vitrinka installed`);
|
|
1233
1235
|
else
|
|
1234
|
-
console.log(`✗
|
|
1236
|
+
console.log(`✗ plugin ${rt.name}: not installed — repair: vitrinka doctor --fix`);
|
|
1235
1237
|
}
|
|
1236
1238
|
if (home) {
|
|
1237
1239
|
const shimP = shimPathOf(home);
|
|
@@ -1262,110 +1264,88 @@ async function doctorCmd(args) {
|
|
|
1262
1264
|
if (home && existsSync(join(home, '.claude', 'vitrinka-statusline.sh'))) {
|
|
1263
1265
|
if (fix)
|
|
1264
1266
|
installClaudeCmd(args);
|
|
1265
|
-
else
|
|
1267
|
+
else if (existsSync(join(home, '.claude', 'hooks', 'vitrinka-board-link.sh')))
|
|
1266
1268
|
console.log('✓ claude ui wired (refresh: vitrinka doctor --fix)');
|
|
1269
|
+
else
|
|
1270
|
+
console.log('✗ claude ui session-start hook missing — repair: vitrinka doctor --fix');
|
|
1267
1271
|
}
|
|
1272
|
+
const appPath = refreshDesktopAppFlag();
|
|
1273
|
+
console.log(appPath ? `✓ app ${appPath} (flag: ${desktopAppFlagPath()})` : `- app Vitrinka.app not installed (no flag)`);
|
|
1268
1274
|
if (failed)
|
|
1269
1275
|
process.exit(1);
|
|
1270
1276
|
}
|
|
1271
1277
|
// ---------- install: PATH shim ----------
|
|
1272
|
-
//
|
|
1273
|
-
//
|
|
1274
|
-
//
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
fail('install-skills: $HOME is not set (or pass --local)');
|
|
1295
|
-
const target = local ? resolve('.claude', 'skills') : join(home, '.claude', 'skills');
|
|
1296
|
-
mkdirSync(target, { recursive: true });
|
|
1297
|
-
// Whole directories: a skill may nest its SKILL.md (the vitrinka skill
|
|
1298
|
-
// keeps it under listen/ next to its CLI + tests).
|
|
1299
|
-
const names = readdirSync(bundle).filter((n) => !n.startsWith('.') && statSync(join(bundle, n)).isDirectory());
|
|
1300
|
-
if (!names.length)
|
|
1301
|
-
fail(`install-skills: no skills found in ${bundle}`);
|
|
1302
|
-
for (const n of names) {
|
|
1303
|
-
rmSync(join(target, n), { recursive: true, force: true });
|
|
1304
|
-
cpSync(join(bundle, n), join(target, n), { recursive: true });
|
|
1305
|
-
// Local installs live at ./.claude/skills — keep referenced CLI paths copy-pasteable.
|
|
1306
|
-
if (local) {
|
|
1307
|
-
for (const doc of [join(target, n, 'SKILL.md'), join(target, n, 'listen', 'SKILL.md')]) {
|
|
1308
|
-
if (!existsSync(doc))
|
|
1309
|
-
continue;
|
|
1310
|
-
writeFileSync(doc, readFileSync(doc, 'utf8')
|
|
1311
|
-
.replaceAll('~/.claude/skills/vitrinka/cli.ts', '.claude/skills/vitrinka/cli.ts'));
|
|
1312
|
-
}
|
|
1278
|
+
// ---------- plugin distribution (skills ship via the runtimes' plugin systems) ----------
|
|
1279
|
+
// The skills bundle used to be copied into ~/.claude/skills (install-skills,
|
|
1280
|
+
// REMOVED 2026-07-11). Skills now distribute exclusively as the `vitrinka`
|
|
1281
|
+
// plugin from the `lovinka` marketplace (this repo), installable into BOTH
|
|
1282
|
+
// Claude Code and Codex — one distribution per machine per runtime.
|
|
1283
|
+
const MARKETPLACE_REPO = 'LEFTEQ/vitrinka';
|
|
1284
|
+
// pluginRuntimes probes which agent CLIs exist and whether the vitrinka
|
|
1285
|
+
// plugin is installed in each. Claude records installs in
|
|
1286
|
+
// ~/.claude/plugins/installed_plugins.json; Codex is asked directly
|
|
1287
|
+
// (`codex plugin list` is fast and authoritative across its versions).
|
|
1288
|
+
function pluginRuntimes() {
|
|
1289
|
+
const home = process.env.HOME || '';
|
|
1290
|
+
const out = [];
|
|
1291
|
+
const claudePresent = claudeCliPresent();
|
|
1292
|
+
let claudeInstalled = false;
|
|
1293
|
+
if (claudePresent && home) {
|
|
1294
|
+
try {
|
|
1295
|
+
claudeInstalled = readFileSync(join(home, '.claude', 'plugins', 'installed_plugins.json'), 'utf8')
|
|
1296
|
+
.includes('vitrinka@lovinka');
|
|
1297
|
+
}
|
|
1298
|
+
catch {
|
|
1299
|
+
claudeInstalled = false; // no plugins file yet — nothing installed
|
|
1313
1300
|
}
|
|
1314
|
-
console.log(`✓ ${n} → ${join(target, n)}`);
|
|
1315
1301
|
}
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
mkdirSync(commandsDir, { recursive: true });
|
|
1323
|
-
let content = readFileSync(stub, 'utf8');
|
|
1324
|
-
if (local)
|
|
1325
|
-
content = content.replaceAll('~/.claude/skills/vitrinka/listen/SKILL.md', '.claude/skills/vitrinka/listen/SKILL.md');
|
|
1326
|
-
writeFileSync(join(commandsDir, 'listen.md'), content);
|
|
1327
|
-
console.log(`✓ /vitrinka:listen command → ${join(commandsDir, 'listen.md')}`);
|
|
1302
|
+
out.push({ name: 'claude', present: claudePresent, installed: claudeInstalled });
|
|
1303
|
+
const codexPresent = !spawnSync('codex', ['--version'], { stdio: 'ignore' }).error;
|
|
1304
|
+
let codexInstalled = false;
|
|
1305
|
+
if (codexPresent) {
|
|
1306
|
+
const r = spawnSync('codex', ['plugin', 'list'], { encoding: 'utf8' });
|
|
1307
|
+
codexInstalled = r.status === 0 && /\bvitrinka\b/.test(r.stdout || '');
|
|
1328
1308
|
}
|
|
1329
|
-
|
|
1309
|
+
out.push({ name: 'codex', present: codexPresent, installed: codexInstalled });
|
|
1310
|
+
return out;
|
|
1330
1311
|
}
|
|
1331
|
-
//
|
|
1332
|
-
//
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1312
|
+
// installPluginInto registers the lovinka marketplace and installs the
|
|
1313
|
+
// vitrinka plugin for one runtime. Both runtimes' commands are idempotent
|
|
1314
|
+
// enough for our purposes (marketplace add on an existing source errors —
|
|
1315
|
+
// tolerated; install/add on an installed plugin re-snapshots).
|
|
1316
|
+
function installPluginInto(rt) {
|
|
1317
|
+
const cmds = rt.name === 'claude'
|
|
1318
|
+
? [['claude', 'plugin', 'marketplace', 'add', MARKETPLACE_REPO], ['claude', 'plugin', 'install', 'vitrinka@lovinka']]
|
|
1319
|
+
: [['codex', 'plugin', 'marketplace', 'add', MARKETPLACE_REPO], ['codex', 'plugin', 'add', 'vitrinka@lovinka']];
|
|
1320
|
+
for (const [bin, ...rest] of cmds) {
|
|
1321
|
+
const r = spawnSync(bin, rest, { encoding: 'utf8' });
|
|
1322
|
+
if (r.status !== 0 && !/already|exists/i.test((r.stderr || '') + (r.stdout || ''))) {
|
|
1323
|
+
console.error(`⚠ ${rt.name}: \`${[bin, ...rest].join(' ')}\` failed${r.stderr ? ` — ${r.stderr.trim().split('\n').pop()}` : ''}`);
|
|
1341
1324
|
return false;
|
|
1342
|
-
if (da) {
|
|
1343
|
-
if (!dirsEqual(pa, pb))
|
|
1344
|
-
return false;
|
|
1345
1325
|
}
|
|
1346
|
-
else if (!readFileSync(pa).equals(readFileSync(pb)))
|
|
1347
|
-
return false;
|
|
1348
1326
|
}
|
|
1349
1327
|
return true;
|
|
1350
1328
|
}
|
|
1351
|
-
//
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
return 'missing';
|
|
1358
|
-
return names.every((n) => dirsEqual(join(bundle, n), join(target, n))) ? 'current' : 'outdated';
|
|
1329
|
+
// updatePluginIn refreshes an installed plugin to the marketplace's latest.
|
|
1330
|
+
function updatePluginIn(rt) {
|
|
1331
|
+
const cmd = rt.name === 'claude'
|
|
1332
|
+
? ['claude', 'plugin', 'update', 'vitrinka@lovinka']
|
|
1333
|
+
: ['codex', 'plugin', 'marketplace', 'upgrade'];
|
|
1334
|
+
return spawnSync(cmd[0], cmd.slice(1), { stdio: 'inherit' }).status === 0;
|
|
1359
1335
|
}
|
|
1336
|
+
// ---------- setup state probing (shared by install / doctor / uninstall) ----------
|
|
1360
1337
|
const SHIM_CONTENT = `#!/bin/sh\nexec node "${CLI_PATH}" "$@"\n`;
|
|
1361
1338
|
function shimPathOf(home) { return join(home, '.local', 'bin', 'vitrinka'); }
|
|
1362
1339
|
function rcPathOf(home) {
|
|
1363
1340
|
return join(home, (process.env.SHELL || '').includes('bash') ? '.bashrc' : '.zshrc');
|
|
1364
1341
|
}
|
|
1365
1342
|
// claudeCliPresent / mcpRegistered / registerMcp — the `claude mcp` touchpoints.
|
|
1366
|
-
// The
|
|
1367
|
-
//
|
|
1368
|
-
// the
|
|
1343
|
+
// The registration is the REMOTE streamable-HTTP endpoint the Go server hosts
|
|
1344
|
+
// at {base}/mcp (remote-mcp decisions 2026-07-12) — no local process per
|
|
1345
|
+
// session; the old stdio bin is deprecated. The bearer token is required on
|
|
1346
|
+
// every /mcp request, so registration without a token is refused with the
|
|
1347
|
+
// token help. X-Board-Actor rides along when an operator persona exists
|
|
1348
|
+
// (UTF-8 bytes latin-1-encoded — header values are ByteStrings).
|
|
1369
1349
|
function claudeCliPresent() {
|
|
1370
1350
|
return !spawnSync('claude', ['--version'], { stdio: 'ignore' }).error;
|
|
1371
1351
|
}
|
|
@@ -1374,15 +1354,22 @@ function mcpRegistered() {
|
|
|
1374
1354
|
return r.status === 0 && /\bvitrinka\b/.test(r.stdout);
|
|
1375
1355
|
}
|
|
1376
1356
|
function mcpTarget() {
|
|
1377
|
-
const
|
|
1378
|
-
const
|
|
1379
|
-
if (
|
|
1380
|
-
|
|
1381
|
-
|
|
1357
|
+
const args = ['--transport', 'http', 'vitrinka', `${resolveBaseUrl()}/mcp`];
|
|
1358
|
+
const token = readToken();
|
|
1359
|
+
if (token)
|
|
1360
|
+
args.push('--header', `Authorization: Bearer ${token}`);
|
|
1361
|
+
const actor = actorHeaderValue(readOperator(true));
|
|
1362
|
+
if (actor)
|
|
1363
|
+
args.push('--header', `X-Board-Actor: ${actor}`);
|
|
1364
|
+
return args;
|
|
1382
1365
|
}
|
|
1383
1366
|
function registerMcp(scope) {
|
|
1367
|
+
if (!readToken()) {
|
|
1368
|
+
console.log(`✗ mcp not registered — /mcp requires a bearer token on every request.\n${tokenHelp()}`);
|
|
1369
|
+
return false;
|
|
1370
|
+
}
|
|
1384
1371
|
spawnSync('claude', ['mcp', 'remove', '--scope', scope, 'vitrinka'], { stdio: 'ignore' });
|
|
1385
|
-
return spawnSync('claude', ['mcp', 'add', '--scope', scope,
|
|
1372
|
+
return spawnSync('claude', ['mcp', 'add', '--scope', scope, ...mcpTarget()], { stdio: 'inherit' }).status === 0;
|
|
1386
1373
|
}
|
|
1387
1374
|
// promptLine — visible one-line read from the controlling TTY ('' when
|
|
1388
1375
|
// non-interactive, like promptYesNo).
|
|
@@ -1395,12 +1382,21 @@ function promptLine(question) {
|
|
|
1395
1382
|
const buf = Buffer.alloc(512);
|
|
1396
1383
|
const n = readSync(fd, buf, 0, 512, null);
|
|
1397
1384
|
closeSync(fd);
|
|
1398
|
-
|
|
1385
|
+
// A cooked-mode tty read line-edits backspace but NOT arrow keys — those
|
|
1386
|
+
// arrive as raw ESC[D/ESC[C sequences that LOOK like cursor movement while
|
|
1387
|
+
// typing. Strip ANSI sequences and control chars so an arrow-edited answer
|
|
1388
|
+
// never persists escape bytes (the ␛[D␛[D␛[C operator-name incident).
|
|
1389
|
+
return stripControl(buf.toString('utf8', 0, n));
|
|
1399
1390
|
}
|
|
1400
1391
|
catch {
|
|
1401
1392
|
return ''; // no controlling TTY — treat as non-interactive, never block
|
|
1402
1393
|
}
|
|
1403
1394
|
}
|
|
1395
|
+
// stripControl — remove ANSI escape sequences and control characters from a
|
|
1396
|
+
// line of user input, then trim.
|
|
1397
|
+
export function stripControl(s) {
|
|
1398
|
+
return s.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, '').replace(/[\x00-\x1f\x7f]/g, '').trim();
|
|
1399
|
+
}
|
|
1404
1400
|
// promptSecret — promptLine with terminal echo off (token input, the old
|
|
1405
1401
|
// install.sh's `read -rs`). Degrades to a visible read if stty is unavailable.
|
|
1406
1402
|
function promptSecret(question) {
|
|
@@ -1431,9 +1427,7 @@ async function installCmd(args) {
|
|
|
1431
1427
|
const base = resolveBaseUrl(strArg(args.base));
|
|
1432
1428
|
const mcpScope = local ? 'project' : 'user';
|
|
1433
1429
|
// ---- probe (before touching anything) ----
|
|
1434
|
-
const
|
|
1435
|
-
const skillsTarget = local ? resolve('.claude', 'skills') : join(home, '.claude', 'skills');
|
|
1436
|
-
const skills = bundle ? skillsStateOf(bundle, skillsTarget) : 'no-bundle';
|
|
1430
|
+
const runtimes = pluginRuntimes();
|
|
1437
1431
|
const shimP = shimPathOf(home);
|
|
1438
1432
|
const shimOk = existsSync(shimP) && readFileSync(shimP, 'utf8') === SHIM_CONTENT;
|
|
1439
1433
|
const rc = rcPathOf(home);
|
|
@@ -1448,8 +1442,9 @@ async function installCmd(args) {
|
|
|
1448
1442
|
console.log(` ${ok ? '✓' : '✗'} ${label.padEnd(11)}${detail}`);
|
|
1449
1443
|
};
|
|
1450
1444
|
console.log(`vitrinka setup — ${base}${local ? ' (local scope)' : ''}`);
|
|
1451
|
-
|
|
1452
|
-
|
|
1445
|
+
for (const rt of runtimes) {
|
|
1446
|
+
row(rt.installed, `plugin·${rt.name}`, rt.installed ? 'vitrinka installed' : rt.present ? 'will install (skills ship as the plugin)' : `${rt.name} CLI not found`);
|
|
1447
|
+
}
|
|
1453
1448
|
if (!local)
|
|
1454
1449
|
row(shimOk, 'shim', shimOk ? shimP : 'will install');
|
|
1455
1450
|
if (!local)
|
|
@@ -1457,11 +1452,13 @@ async function installCmd(args) {
|
|
|
1457
1452
|
row(mcpOk, 'mcp', mcpOk ? 'registered' : haveClaude ? `not registered (scope ${mcpScope})` : 'claude CLI not found');
|
|
1458
1453
|
row(tokenSrc !== 'missing', 'token', tokenSrc === 'missing' ? 'missing — writes will 401' : `present (${tokenSrc === 'env' ? '$VITRINKA_TOKEN' : tokenPath()})`);
|
|
1459
1454
|
row(!!op, 'operator', op || 'unset — board writes stay anonymous');
|
|
1460
|
-
row(uiOk, 'claude ui', uiOk ? 'statusline + footer wired' : 'not wired');
|
|
1455
|
+
row(uiOk, 'claude ui', uiOk ? 'statusline + footer + session hook wired' : 'not wired');
|
|
1461
1456
|
console.log('');
|
|
1462
|
-
// ---- silent steps:
|
|
1463
|
-
|
|
1464
|
-
|
|
1457
|
+
// ---- silent steps: plugins, shim, PATH, completion (reversible, own dirs) ----
|
|
1458
|
+
for (const rt of runtimes) {
|
|
1459
|
+
if (rt.present && !rt.installed && installPluginInto(rt))
|
|
1460
|
+
console.log(`✓ plugin (${rt.name}) → vitrinka@lovinka`);
|
|
1461
|
+
}
|
|
1465
1462
|
if (!local) {
|
|
1466
1463
|
if (!shimOk) {
|
|
1467
1464
|
mkdirSync(dirname(shimP), { recursive: true });
|
|
@@ -1485,7 +1482,9 @@ async function installCmd(args) {
|
|
|
1485
1482
|
}
|
|
1486
1483
|
// ---- asked steps: the config-touchers ----
|
|
1487
1484
|
// 1. MCP registration (claude's registry).
|
|
1488
|
-
|
|
1485
|
+
// Hint never inlines the token — the shell substitution reads it at run time.
|
|
1486
|
+
const mcpCmdHint = `claude mcp add --scope ${mcpScope} --transport http vitrinka ${resolveBaseUrl()}/mcp `
|
|
1487
|
+
+ `--header "Authorization: Bearer $(cat ${tokenPath()})"`;
|
|
1489
1488
|
if (!haveClaude) {
|
|
1490
1489
|
console.error(`⚠ claude CLI not found — register the MCP later with:\n ${mcpCmdHint}`);
|
|
1491
1490
|
}
|
|
@@ -1542,7 +1541,7 @@ async function installCmd(args) {
|
|
|
1542
1541
|
console.log('claude ui: skipped (--no-claude)');
|
|
1543
1542
|
}
|
|
1544
1543
|
else if (uiOk || args.claude === true
|
|
1545
|
-
|| promptYesNo('Wire Claude Code UI (🧷 board statusline segment + clickable footer badge)? [Y/n] ')) {
|
|
1544
|
+
|| promptYesNo('Wire Claude Code UI (🧷 board statusline segment + clickable footer badge + session-start board link)? [Y/n] ')) {
|
|
1546
1545
|
installClaudeCmd(args);
|
|
1547
1546
|
}
|
|
1548
1547
|
else if (!process.stdin.isTTY) {
|
|
@@ -1587,9 +1586,9 @@ export function stripMarkerBlock(content, marker) {
|
|
|
1587
1586
|
}
|
|
1588
1587
|
return out.join('\n');
|
|
1589
1588
|
}
|
|
1590
|
-
//
|
|
1591
|
-
//
|
|
1592
|
-
const
|
|
1589
|
+
// Legacy ~/.claude/skills copy names — removed by uninstall on machines set
|
|
1590
|
+
// up before the plugin distribution (install-skills, removed 2026-07-11).
|
|
1591
|
+
const LEGACY_SKILLS = ['artifact', 'brainstorming', 'screenshot', 'vitrinka', 'listen'];
|
|
1593
1592
|
// uninstallCmd removes everything install put on the machine — the exact
|
|
1594
1593
|
// inverse: bundled skills + the /vitrinka:listen stub, shim, rc marker blocks,
|
|
1595
1594
|
// statusline wrapper (restoring the original statusLine command), the /boards/
|
|
@@ -1600,21 +1599,31 @@ function uninstallCmd(args) {
|
|
|
1600
1599
|
if (!home)
|
|
1601
1600
|
fail('uninstall: $HOME is not set');
|
|
1602
1601
|
const local = args.local === true;
|
|
1603
|
-
if (args.yes !== true && !promptYesNo(`Remove vitrinka from this machine (
|
|
1602
|
+
if (args.yes !== true && !promptYesNo(`Remove vitrinka from this machine (plugins, legacy skill copies, shim, completion, Claude wiring, MCP${args.purge === true ? ', token+operator' : ''})? [Y/n] `)) {
|
|
1604
1603
|
fail('uninstall: aborted (non-interactive runs need --yes)', 2);
|
|
1605
1604
|
}
|
|
1606
|
-
//
|
|
1607
|
-
|
|
1608
|
-
const
|
|
1609
|
-
|
|
1610
|
-
|
|
1605
|
+
// Plugins (the current distribution) + legacy ~/.claude/skills copies +
|
|
1606
|
+
// the old /vitrinka:listen command stub, from machines set up pre-plugin.
|
|
1607
|
+
for (const rt of pluginRuntimes()) {
|
|
1608
|
+
if (!rt.installed)
|
|
1609
|
+
continue;
|
|
1610
|
+
const cmd = rt.name === 'claude'
|
|
1611
|
+
? ['claude', 'plugin', 'uninstall', 'vitrinka@lovinka']
|
|
1612
|
+
: ['codex', 'plugin', 'remove', 'vitrinka@lovinka'];
|
|
1613
|
+
if (spawnSync(cmd[0], cmd.slice(1), { stdio: 'inherit' }).status === 0) {
|
|
1614
|
+
console.log(`✓ removed ${rt.name} plugin`);
|
|
1615
|
+
}
|
|
1616
|
+
else {
|
|
1617
|
+
console.error(`⚠ ${rt.name} plugin removal failed — run: ${cmd.join(' ')}`);
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1611
1620
|
const skillsDir = local ? resolve('.claude', 'skills') : join(home, '.claude', 'skills');
|
|
1612
1621
|
const commandsDir = local ? resolve('.claude', 'commands', 'vitrinka') : join(home, '.claude', 'commands', 'vitrinka');
|
|
1613
|
-
for (const n of
|
|
1622
|
+
for (const n of LEGACY_SKILLS) {
|
|
1614
1623
|
if (!existsSync(join(skillsDir, n)))
|
|
1615
1624
|
continue;
|
|
1616
1625
|
rmSync(join(skillsDir, n), { recursive: true, force: true });
|
|
1617
|
-
console.log(`✓ removed skill ${n}`);
|
|
1626
|
+
console.log(`✓ removed legacy skill copy ${n}`);
|
|
1618
1627
|
}
|
|
1619
1628
|
if (existsSync(join(commandsDir, 'listen.md'))) {
|
|
1620
1629
|
rmSync(join(commandsDir, 'listen.md'));
|
|
@@ -1679,6 +1688,20 @@ function uninstallCmd(args) {
|
|
|
1679
1688
|
console.log('✓ footer badge entry removed');
|
|
1680
1689
|
}
|
|
1681
1690
|
}
|
|
1691
|
+
if (hasBoardLinkHook(settings)) {
|
|
1692
|
+
const hooks = settings.hooks;
|
|
1693
|
+
const ss = hooks.SessionStart
|
|
1694
|
+
.map((m) => (Array.isArray(m?.hooks)
|
|
1695
|
+
? { ...m, hooks: m.hooks.filter((h) => !(typeof h?.command === 'string' && h.command.includes('vitrinka-board-link'))) }
|
|
1696
|
+
: m))
|
|
1697
|
+
.filter((m) => !Array.isArray(m?.hooks) || m.hooks.length > 0);
|
|
1698
|
+
if (ss.length)
|
|
1699
|
+
hooks.SessionStart = ss;
|
|
1700
|
+
else
|
|
1701
|
+
delete hooks.SessionStart;
|
|
1702
|
+
changed = true;
|
|
1703
|
+
console.log('✓ session-start hook entry removed');
|
|
1704
|
+
}
|
|
1682
1705
|
if (changed)
|
|
1683
1706
|
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
1684
1707
|
}
|
|
@@ -1686,6 +1709,15 @@ function uninstallCmd(args) {
|
|
|
1686
1709
|
rmSync(wrapperP);
|
|
1687
1710
|
console.log(`✓ removed ${wrapperP}`);
|
|
1688
1711
|
}
|
|
1712
|
+
const hookP = join(home, '.claude', 'hooks', 'vitrinka-board-link.sh');
|
|
1713
|
+
if (existsSync(hookP)) {
|
|
1714
|
+
rmSync(hookP);
|
|
1715
|
+
console.log(`✓ removed ${hookP}`);
|
|
1716
|
+
}
|
|
1717
|
+
if (existsSync(desktopAppFlagPath())) {
|
|
1718
|
+
rmSync(desktopAppFlagPath());
|
|
1719
|
+
console.log(`✓ removed ${desktopAppFlagPath()}`);
|
|
1720
|
+
}
|
|
1689
1721
|
// MCP registration (both scopes — cheap, and remove is idempotent).
|
|
1690
1722
|
if (claudeCliPresent()) {
|
|
1691
1723
|
for (const scope of local ? ['project', 'user'] : ['user', 'project']) {
|
|
@@ -1708,7 +1740,7 @@ function uninstallCmd(args) {
|
|
|
1708
1740
|
}
|
|
1709
1741
|
// ---------- install-claude: Claude Code UI wiring ----------
|
|
1710
1742
|
//
|
|
1711
|
-
// Wires the
|
|
1743
|
+
// Wires the three Claude Code surfaces that link to boards (decisions:
|
|
1712
1744
|
// docs/specs/2026-07-07-claude-install-decisions.md):
|
|
1713
1745
|
// 1. footerLinksRegexes in ~/.claude/settings.json — Claude renders its own
|
|
1714
1746
|
// clickable "🧷 board" footer badge whenever a /boards/ URL appears in the
|
|
@@ -1721,16 +1753,30 @@ function uninstallCmd(args) {
|
|
|
1721
1753
|
// "not planned"), but terminals like Warp/iTerm2/Ghostty cmd+click-linkify
|
|
1722
1754
|
// visible URLs on their own.
|
|
1723
1755
|
// With no original statusline, the wrapper renders a minimal line itself.
|
|
1724
|
-
//
|
|
1725
|
-
//
|
|
1726
|
-
//
|
|
1727
|
-
//
|
|
1756
|
+
// 3. SessionStart hook — ~/.claude/hooks/vitrinka-board-link.sh prints the
|
|
1757
|
+
// capture root's board URL into session context at start, so the footer
|
|
1758
|
+
// badge is clickable from message one (the statusline URL isn't).
|
|
1759
|
+
// Idempotent, never an uninstall: re-runs detect the /boards/ footer pattern,
|
|
1760
|
+
// the wrapper registration, and the SessionStart entry and no-op. Removal =
|
|
1761
|
+
// delete the wrapper + hook + restore statusLine.command (recorded in the
|
|
1762
|
+
// wrapper's VITRINKA_ORIG line) + drop the footerLinksRegexes and SessionStart
|
|
1763
|
+
// entries (documented in pkg/README.md).
|
|
1728
1764
|
export const BOARD_FOOTER_LINK = {
|
|
1729
1765
|
type: 'regex',
|
|
1730
1766
|
pattern: '(?<url>https?:\\/\\/\\S+\\/boards\\/(?<slug>[\\w.-]+))',
|
|
1731
1767
|
url: '{url}',
|
|
1732
1768
|
label: 'vitrinka/b/{slug}',
|
|
1733
1769
|
};
|
|
1770
|
+
// Second badge on the same board URLs: a vitrinka:// deep link that opens the
|
|
1771
|
+
// Tauri desktop app (which registers ONLY the custom scheme — https stays in
|
|
1772
|
+
// the browser by design, see apps/desktop). Captures the path after the host
|
|
1773
|
+
// so `vitrinka://w/<ws>/boards/<slug>` lands on the same board in-app.
|
|
1774
|
+
export const BOARD_APP_FOOTER_LINK = {
|
|
1775
|
+
type: 'regex',
|
|
1776
|
+
pattern: 'https?:\\/\\/[^\\/\\s]+\\/(?<path>(?:w\\/[\\w.-]+\\/)?boards\\/[\\w.-]+)',
|
|
1777
|
+
url: 'vitrinka://{path}',
|
|
1778
|
+
label: '⧉ app',
|
|
1779
|
+
};
|
|
1734
1780
|
// hasBoardFooterLink — an entry whose pattern targets /boards/ URLs already
|
|
1735
1781
|
// exists (ours or hand-written); the marker that makes the settings edit idempotent.
|
|
1736
1782
|
export function hasBoardFooterLink(settings) {
|
|
@@ -1738,6 +1784,41 @@ export function hasBoardFooterLink(settings) {
|
|
|
1738
1784
|
// Strip regex escaping first — patterns write the path as \/boards\/ as often as /boards/.
|
|
1739
1785
|
return Array.isArray(arr) && arr.some((e) => typeof e?.pattern === 'string' && e.pattern.replaceAll('\\', '').includes('/boards/'));
|
|
1740
1786
|
}
|
|
1787
|
+
// hasBoardAppFooterLink — the vitrinka:// deep-link badge already exists.
|
|
1788
|
+
export function hasBoardAppFooterLink(settings) {
|
|
1789
|
+
const arr = settings.footerLinksRegexes;
|
|
1790
|
+
return Array.isArray(arr) && arr.some((e) => typeof e?.url === 'string' && e.url.startsWith('vitrinka://'));
|
|
1791
|
+
}
|
|
1792
|
+
// ---------- desktop-app flag ----------
|
|
1793
|
+
// ONE canonical "is Vitrinka.app installed here" answer, so skills (and the
|
|
1794
|
+
// Claude wiring) never probe /Applications themselves: install-claude and
|
|
1795
|
+
// doctor refresh ~/.config/vitrinka/desktop-app to hold the app path, or
|
|
1796
|
+
// delete it when the app is gone. Skills just test the file.
|
|
1797
|
+
export function desktopAppFlagPath() {
|
|
1798
|
+
return join(process.env.HOME || '', '.config', 'vitrinka', 'desktop-app');
|
|
1799
|
+
}
|
|
1800
|
+
// desktopAppPath — where Vitrinka.app actually is right now ('' when absent).
|
|
1801
|
+
export function desktopAppPath() {
|
|
1802
|
+
for (const p of [join(process.env.HOME || '', 'Applications', 'Vitrinka.app'), '/Applications/Vitrinka.app']) {
|
|
1803
|
+
if (existsSync(p))
|
|
1804
|
+
return p;
|
|
1805
|
+
}
|
|
1806
|
+
return '';
|
|
1807
|
+
}
|
|
1808
|
+
// refreshDesktopAppFlag — sync the flag file to reality; returns the app path
|
|
1809
|
+
// ('' when absent). Called from install-claude and doctor.
|
|
1810
|
+
export function refreshDesktopAppFlag() {
|
|
1811
|
+
const app = desktopAppPath();
|
|
1812
|
+
const flag = desktopAppFlagPath();
|
|
1813
|
+
if (app) {
|
|
1814
|
+
mkdirSync(dirname(flag), { recursive: true });
|
|
1815
|
+
writeFileSync(flag, app + '\n');
|
|
1816
|
+
}
|
|
1817
|
+
else if (existsSync(flag)) {
|
|
1818
|
+
rmSync(flag);
|
|
1819
|
+
}
|
|
1820
|
+
return app;
|
|
1821
|
+
}
|
|
1741
1822
|
// buildStatuslineWrapper — the ~/.claude/vitrinka-statusline.sh content. The
|
|
1742
1823
|
// original command is recorded on the VITRINKA_ORIG line (single-quoted), both
|
|
1743
1824
|
// to run it and so a re-run can recover it instead of wrapping the wrapper.
|
|
@@ -1803,6 +1884,41 @@ else
|
|
|
1803
1884
|
fi
|
|
1804
1885
|
`;
|
|
1805
1886
|
}
|
|
1887
|
+
// SessionStart hook — the third Claude UI surface. Statusline URLs aren't
|
|
1888
|
+
// clickable in every terminal (and never OSC 8), so this puts the board URL
|
|
1889
|
+
// into the session context at start: the footer badge renders from message
|
|
1890
|
+
// one and Claude knows the live board without asking. Silent no-op when the
|
|
1891
|
+
// repo has no .screenshots/.vitrinka descriptor.
|
|
1892
|
+
export const BOARD_LINK_HOOK_CMD = '~/.claude/hooks/vitrinka-board-link.sh';
|
|
1893
|
+
export const BOARD_LINK_HOOK_SCRIPT = `#!/bin/bash
|
|
1894
|
+
# vitrinka-board-link — SessionStart hook, managed by \`vitrinka install-claude\`
|
|
1895
|
+
# (re-runs rewrite it). Emits the repo's live board URL into session context so
|
|
1896
|
+
# the clickable footer badge (footerLinksRegexes /boards/ entry) exists from
|
|
1897
|
+
# message one. Silent no-op without a .screenshots/.vitrinka descriptor.
|
|
1898
|
+
command -v jq >/dev/null 2>&1 || exit 0
|
|
1899
|
+
input=$(cat)
|
|
1900
|
+
dir=$(printf '%s' "$input" | jq -r '.cwd // ""')
|
|
1901
|
+
dir="\${dir:-$PWD}"
|
|
1902
|
+
root=$(git -C "$dir" rev-parse --show-toplevel 2>/dev/null)
|
|
1903
|
+
vjson=""
|
|
1904
|
+
for c in "$dir/.screenshots/.vitrinka" \${root:+"$root/.screenshots/.vitrinka"}; do
|
|
1905
|
+
[ -f "$c" ] && { vjson=$c; break; }
|
|
1906
|
+
done
|
|
1907
|
+
[ -n "$vjson" ] || exit 0
|
|
1908
|
+
base=$(jq -r '.base // empty' "$vjson" 2>/dev/null)
|
|
1909
|
+
key=$(jq -r '.key // empty' "$vjson" 2>/dev/null)
|
|
1910
|
+
ws=$(jq -r '.workspace // empty' "$vjson" 2>/dev/null)
|
|
1911
|
+
[ -n "$base" ] && [ -n "$key" ] || exit 0
|
|
1912
|
+
[ -n "$ws" ] && base="$base/w/$ws"
|
|
1913
|
+
printf 'Live vitrinka board for this repo: %s/boards/%s\\n' "$base" "$key"
|
|
1914
|
+
`;
|
|
1915
|
+
// hasBoardLinkHook — a SessionStart entry running vitrinka-board-link already
|
|
1916
|
+
// exists; the marker that makes the settings edit idempotent.
|
|
1917
|
+
export function hasBoardLinkHook(settings) {
|
|
1918
|
+
const arr = settings.hooks?.SessionStart;
|
|
1919
|
+
return Array.isArray(arr) && arr.some((m) => Array.isArray(m?.hooks)
|
|
1920
|
+
&& m.hooks.some((h) => typeof h?.command === 'string' && h.command.includes('vitrinka-board-link')));
|
|
1921
|
+
}
|
|
1806
1922
|
// wrapperOrigCommand — recover the original statusline command a wrapper file
|
|
1807
1923
|
// recorded, so re-runs refresh the wrapper without wrapping it around itself.
|
|
1808
1924
|
export function wrapperOrigCommand(content) {
|
|
@@ -1858,6 +1974,24 @@ export function installClaudeCmd(_args) {
|
|
|
1858
1974
|
changed = true;
|
|
1859
1975
|
console.log('✓ footer badge → footerLinksRegexes (clickable "🧷 board" whenever a board URL appears in conversation)');
|
|
1860
1976
|
}
|
|
1977
|
+
// 1b. App badge — only on machines with the desktop app: a second badge per
|
|
1978
|
+
// board URL whose vitrinka:// deep link opens the same board in Vitrinka.app.
|
|
1979
|
+
// The flag is refreshed here (see desktopAppPath); skills read the flag file,
|
|
1980
|
+
// never probe /Applications themselves.
|
|
1981
|
+
const appPath = refreshDesktopAppFlag();
|
|
1982
|
+
if (appPath && !hasBoardAppFooterLink(settings)) {
|
|
1983
|
+
settings.footerLinksRegexes = [...settings.footerLinksRegexes, BOARD_APP_FOOTER_LINK];
|
|
1984
|
+
changed = true;
|
|
1985
|
+
console.log(`✓ app badge → footerLinksRegexes ("⧉ app" opens boards in ${appPath})`);
|
|
1986
|
+
}
|
|
1987
|
+
else if (appPath) {
|
|
1988
|
+
console.log('✓ app badge already configured (vitrinka:// footer entry)');
|
|
1989
|
+
}
|
|
1990
|
+
else if (hasBoardAppFooterLink(settings)) {
|
|
1991
|
+
settings.footerLinksRegexes = settings.footerLinksRegexes.filter((e) => !(typeof e?.url === 'string' && e.url.startsWith('vitrinka://')));
|
|
1992
|
+
changed = true;
|
|
1993
|
+
console.log('✓ app badge removed (Vitrinka.app not installed on this machine)');
|
|
1994
|
+
}
|
|
1861
1995
|
// 2. Statusline wrapper.
|
|
1862
1996
|
const wrapperPath = join(claudeDir, 'vitrinka-statusline.sh');
|
|
1863
1997
|
const wrapperCmd = '~/.claude/vitrinka-statusline.sh';
|
|
@@ -1883,6 +2017,24 @@ export function installClaudeCmd(_args) {
|
|
|
1883
2017
|
else
|
|
1884
2018
|
console.log(`✓ statusline installed → ${wrapperPath} (minimal: dir │ branch │ 🧷 board │ model)`);
|
|
1885
2019
|
}
|
|
2020
|
+
// 3. SessionStart hook — board URL into context at session start, so the
|
|
2021
|
+
// footer badge is clickable from message one.
|
|
2022
|
+
const hooksDir = join(claudeDir, 'hooks');
|
|
2023
|
+
const hookPath = join(hooksDir, 'vitrinka-board-link.sh');
|
|
2024
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
2025
|
+
writeFileSync(hookPath, BOARD_LINK_HOOK_SCRIPT);
|
|
2026
|
+
chmodSync(hookPath, 0o755);
|
|
2027
|
+
if (hasBoardLinkHook(settings)) {
|
|
2028
|
+
console.log(`✓ session-start hook refreshed (${hookPath})`);
|
|
2029
|
+
}
|
|
2030
|
+
else {
|
|
2031
|
+
const hooks = (settings.hooks && typeof settings.hooks === 'object' ? settings.hooks : {});
|
|
2032
|
+
const arr = Array.isArray(hooks.SessionStart) ? hooks.SessionStart : [];
|
|
2033
|
+
hooks.SessionStart = [...arr, { hooks: [{ type: 'command', command: BOARD_LINK_HOOK_CMD, timeout: 10 }] }];
|
|
2034
|
+
settings.hooks = hooks;
|
|
2035
|
+
changed = true;
|
|
2036
|
+
console.log(`✓ session-start hook → ${hookPath} (board URL in context from message one)`);
|
|
2037
|
+
}
|
|
1886
2038
|
if (changed)
|
|
1887
2039
|
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
1888
2040
|
// The wrapper reads Claude's stdin JSON with jq; without it the board segment
|
|
@@ -1919,7 +2071,16 @@ function openCmd(root, args) {
|
|
|
1919
2071
|
if (args.print === true)
|
|
1920
2072
|
return;
|
|
1921
2073
|
const opener = process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
1922
|
-
|
|
2074
|
+
// Desktop app installed (the ~/.config/vitrinka/desktop-app flag) → open the
|
|
2075
|
+
// board in Vitrinka.app via its vitrinka:// scheme instead of the browser.
|
|
2076
|
+
// --browser forces the https URL.
|
|
2077
|
+
let target = url;
|
|
2078
|
+
if (args.browser !== true && process.platform === 'darwin' && existsSync(desktopAppFlagPath())) {
|
|
2079
|
+
target = url.replace(/^https?:\/\/[^/]+\//, 'vitrinka://');
|
|
2080
|
+
if (target === url)
|
|
2081
|
+
target = 'vitrinka://'; // bare base URL — just focus the app
|
|
2082
|
+
}
|
|
2083
|
+
const r = spawnSync(opener, [target], { stdio: 'ignore' });
|
|
1923
2084
|
if (r.error || r.status !== 0)
|
|
1924
2085
|
console.error(`open: could not launch ${opener} — URL printed above`);
|
|
1925
2086
|
}
|
|
@@ -1977,7 +2138,7 @@ export function cmpSemver(a, b) {
|
|
|
1977
2138
|
return 0;
|
|
1978
2139
|
}
|
|
1979
2140
|
// Commands that must never carry the notifier/hint noise (setup and meta paths).
|
|
1980
|
-
const HINT_SKIP = new Set(['install', 'install-claude', '
|
|
2141
|
+
const HINT_SKIP = new Set(['install', 'install-claude', 'update', 'uninstall',
|
|
1981
2142
|
'doctor', 'help', 'completion', '__complete', 'ai']);
|
|
1982
2143
|
function updateCheckPath() {
|
|
1983
2144
|
return join(process.env.HOME || '', '.config', 'vitrinka', 'update-check');
|
|
@@ -2070,7 +2231,7 @@ function pkgVersion() {
|
|
|
2070
2231
|
// before, the Claude Code UI wiring. It never asks new consent questions —
|
|
2071
2232
|
// that stays `vitrinka install`'s job. The refresh steps re-exec the (possibly
|
|
2072
2233
|
// just replaced) CLI file so they run the NEW code, not this stale process.
|
|
2073
|
-
async function updateCmd(
|
|
2234
|
+
async function updateCmd(_args) {
|
|
2074
2235
|
const repoTop = git(['rev-parse', '--show-toplevel'], dirname(CLI_PATH));
|
|
2075
2236
|
if (repoTop) {
|
|
2076
2237
|
// Repo checkout — the shim/MCP run the source directly, so a pull IS the update.
|
|
@@ -2124,11 +2285,13 @@ async function updateCmd(args) {
|
|
|
2124
2285
|
}
|
|
2125
2286
|
}
|
|
2126
2287
|
}
|
|
2127
|
-
// Refresh installed surfaces from the NEW code —
|
|
2128
|
-
//
|
|
2288
|
+
// Refresh installed surfaces from the NEW code — plugins where installed,
|
|
2289
|
+
// Claude wiring only if the wrapper shows prior consent.
|
|
2129
2290
|
const rerun = (sub) => spawnSync(process.execPath, [CLI_PATH, ...sub], { stdio: 'inherit' }).status === 0;
|
|
2130
|
-
|
|
2131
|
-
|
|
2291
|
+
for (const rt of pluginRuntimes()) {
|
|
2292
|
+
if (rt.installed && !updatePluginIn(rt)) {
|
|
2293
|
+
console.error(`⚠ update: ${rt.name} plugin refresh failed — re-run its plugin update manually`);
|
|
2294
|
+
}
|
|
2132
2295
|
}
|
|
2133
2296
|
const wrapper = join(process.env.HOME || '', '.claude', 'vitrinka-statusline.sh');
|
|
2134
2297
|
if (existsSync(wrapper)) {
|
|
@@ -2854,21 +3017,17 @@ export const COMMANDS = [
|
|
|
2854
3017
|
examples: ['vitrinka install-claude'],
|
|
2855
3018
|
},
|
|
2856
3019
|
{
|
|
2857
|
-
name: '
|
|
2858
|
-
usage: '[--local]', flags: [{ f: '--local', d: 'install into ./.claude/skills of the current project instead of ~/.claude/skills' }],
|
|
2859
|
-
examples: ['vitrinka install-skills'],
|
|
2860
|
-
},
|
|
2861
|
-
{
|
|
2862
|
-
name: 'update', summary: 'Update the CLI (git pull / npm -g), show what\'s new, refresh the installed skills + Claude wiring',
|
|
3020
|
+
name: 'update', summary: 'Update the CLI (git pull / npm -g), show what\'s new, refresh the installed plugins + Claude wiring',
|
|
2863
3021
|
usage: '[--local]',
|
|
2864
3022
|
flags: [{ f: '--local', d: 'refresh skills into ./.claude/skills of the current project instead of ~/.claude/skills' }],
|
|
2865
3023
|
examples: ['vitrinka update'],
|
|
2866
3024
|
},
|
|
2867
3025
|
{
|
|
2868
|
-
name: 'open', summary: 'Open the capture root\'s live board in the browser (--qr renders a terminal QR for the iPad)',
|
|
2869
|
-
usage: '[--root <dir>] [--qr] [--print]',
|
|
3026
|
+
name: 'open', summary: 'Open the capture root\'s live board — in Vitrinka.app when installed, else the browser (--qr renders a terminal QR for the iPad)',
|
|
3027
|
+
usage: '[--root <dir>] [--qr] [--print] [--browser]',
|
|
2870
3028
|
flags: [ROOT_FLAG, { f: '--qr', d: 'print an ANSI QR code of the URL instead of opening the browser (needs qrencode)' },
|
|
2871
|
-
{ f: '--print', d: 'only print the URL' }
|
|
3029
|
+
{ f: '--print', d: 'only print the URL' },
|
|
3030
|
+
{ f: '--browser', d: 'force the browser even when Vitrinka.app is installed' }],
|
|
2872
3031
|
examples: ['vitrinka open', 'vitrinka open --qr'],
|
|
2873
3032
|
},
|
|
2874
3033
|
{
|
|
@@ -3741,8 +3900,6 @@ async function runSub(argv) {
|
|
|
3741
3900
|
await installCmd(args);
|
|
3742
3901
|
else if (cmd === 'install-claude')
|
|
3743
3902
|
installClaudeCmd(args);
|
|
3744
|
-
else if (cmd === 'install-skills')
|
|
3745
|
-
installSkillsCmd(args);
|
|
3746
3903
|
else if (cmd === 'uninstall')
|
|
3747
3904
|
uninstallCmd(args);
|
|
3748
3905
|
else if (cmd === 'update')
|