@krtclcdy/tersio 2.14.0 → 2.16.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/cli/install.js CHANGED
@@ -3,7 +3,7 @@ import { promises as fs } from 'node:fs';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
- import { BUN_BIN_DIR, COMBO_PRESET_MODES, EXTENSIONS_KEY_RE, HOME, IS_WINDOWS, OMP_AGENT_DIR, OMP_PLUGINS_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_VERSION, RTK_BINARY_NAME, applyUpdate, cavemanDefaultFlag, comboDefaultFlag, command, dryRun, install, ponytailDefaultFlag, profileFlagsGiven, reinstall, rtkDefaultFlag, verbose, yes, dashboardExport, dashboardPort, debug, ensureExtensionAfterConfigEntry, ensureExtensionInConfig, ensurePonytailConfigValue, execP, parseJsonObject, readPluginsPackage, writeIfChanged, } from './common.js';
6
+ import { BUN_BIN_DIR, CAVEMAN_DEFAULTS, COMBO_PRESET_MODES, IS_WINDOWS, OMP_AGENT_DIR, OMP_PLUGINS_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_VERSION, PONYTAIL_DEFAULTS, RTK_BINARY_NAME, applyUpdate, cavemanDefaultFlag, comboDefaultFlag, command, dryRun, install, ponytailDefaultFlag, profileFlagsGiven, reinstall, rtkDefaultFlag, verbose, yes, dashboardExport, dashboardPort, debug, ensureExtensionAfterConfigEntry, ensureExtensionInConfig, ensurePonytailConfigValue, execP, parseJsonObject, readPluginsPackage, writeIfChanged, } from './common.js';
7
7
  import { askInteractiveChoice, askInteractiveConfirm, closeRL, execNetwork, tty, withInteractiveSpinner, } from './interactive.js';
8
8
  import { printWelcome } from './banner.js';
9
9
  import { checkForUpdate, runLatestUpdate } from './update.js';
@@ -32,17 +32,18 @@ const SHARED_CARBON = path.join(EXT_DIR, 'shared', 'carbon.js');
32
32
  const PONYTAIL_GITHUB_SPEC = 'github:DietrichGebert/ponytail';
33
33
  const PONYTAIL_NPM_SPEC = '@dietrichgebert/ponytail@latest';
34
34
  async function stepPonytail(pluginsDir, userDir, options) {
35
- console.log('\n[1/8] Installing Ponytail plugin...');
35
+ if (!options.quiet)
36
+ console.log(' Ponytail — refresh plugin');
36
37
  await fs.mkdir(pluginsDir, { recursive: true });
37
38
  const pkgPath = path.join(pluginsDir, 'package.json');
38
39
  const pkg = await readPluginsPackage(pkgPath);
39
40
  pkg.dependencies['@dietrichgebert/ponytail'] = PONYTAIL_GITHUB_SPEC;
40
41
  if (options.dryRun) {
41
- console.log(` [dry-run] would write ${pkgPath}`);
42
+ if (verbose && !options.quiet)
43
+ console.log(` [dry-run] would write ${pkgPath}`);
42
44
  }
43
45
  else {
44
46
  await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
45
- console.log(' [write] package.json');
46
47
  }
47
48
  const ponytailExtPath = path.join(pluginsDir, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js');
48
49
  const probeExt = async () => (await readTextIfExists(ponytailExtPath)) !== null;
@@ -53,8 +54,9 @@ async function stepPonytail(pluginsDir, userDir, options) {
53
54
  }
54
55
  else if (options.dryRun) {
55
56
  // Dry runs preview the wiring below without touching the network.
56
- console.log(` [dry-run] would run: omp plugin install ${PONYTAIL_GITHUB_SPEC}`);
57
- if (options.reinstall) {
57
+ if (verbose && !options.quiet)
58
+ console.log(` [dry-run] would run: omp plugin install ${PONYTAIL_GITHUB_SPEC}`);
59
+ if (options.reinstall && verbose && !options.quiet) {
58
60
  console.log(` [dry-run] would run: npm install ${PONYTAIL_NPM_SPEC} --save --no-audit --no-fund`);
59
61
  }
60
62
  ponytailExtExists = true;
@@ -63,7 +65,6 @@ async function stepPonytail(pluginsDir, userDir, options) {
63
65
  // Try omp plugin install first
64
66
  try {
65
67
  await execNetwork('Installing Ponytail plugin', OMP_BIN, ['plugin', 'install', PONYTAIL_GITHUB_SPEC], { cwd: pluginsDir });
66
- console.log(' [ok] omp plugin install ran');
67
68
  }
68
69
  catch (e) {
69
70
  console.log(` [warn] omp plugin install failed: ${e.message}`);
@@ -71,7 +72,6 @@ async function stepPonytail(pluginsDir, userDir, options) {
71
72
  if (options.reinstall) {
72
73
  try {
73
74
  await execNetwork('Refreshing Ponytail package', 'npm', ['install', PONYTAIL_NPM_SPEC, '--save', '--no-audit', '--no-fund'], { cwd: pluginsDir, timeout: 120000 });
74
- console.log(' [ok] Ponytail refreshed');
75
75
  }
76
76
  catch (e) {
77
77
  console.log(` [fail] Could not refresh ponytail: ${e.message}`);
@@ -82,15 +82,14 @@ async function stepPonytail(pluginsDir, userDir, options) {
82
82
  ponytailExtExists = await probeExt();
83
83
  // Fallback: try bun install or npm install
84
84
  if (!ponytailExtExists) {
85
- console.log(' [info] pi-extension/index.js not found after omp plugin install — trying npm/bun install...');
85
+ if (!options.quiet)
86
+ console.log(' [info] pi-extension/index.js not found after omp plugin install — trying npm/bun install...');
86
87
  try {
87
88
  await execNetwork('Installing Ponytail dependencies', 'npm', ['install'], { cwd: pluginsDir, timeout: 120000 });
88
- console.log(' [ok] npm install completed');
89
89
  }
90
90
  catch {
91
91
  try {
92
92
  await execNetwork('Installing Ponytail dependencies', 'bun', ['install'], { cwd: pluginsDir, timeout: 120000 });
93
- console.log(' [ok] bun install completed');
94
93
  }
95
94
  catch (e2) {
96
95
  console.log(` [fail] Could not install ponytail: ${e2.message}`);
@@ -101,12 +100,12 @@ async function stepPonytail(pluginsDir, userDir, options) {
101
100
  }
102
101
  // Last-resort fallback: git clone the repo into node_modules
103
102
  if (!ponytailExtExists) {
104
- console.log(' [info] npm/bun did not produce pi-extension — trying git clone...');
103
+ if (!options.quiet)
104
+ console.log(' [info] npm/bun did not produce pi-extension — trying git clone...');
105
105
  try {
106
106
  const dest = path.join(pluginsDir, 'node_modules', '@dietrichgebert', 'ponytail');
107
107
  await fs.mkdir(path.dirname(dest), { recursive: true });
108
108
  await execNetwork('Cloning Ponytail repository', 'git', ['clone', '--depth', '1', 'https://github.com/DietrichGebert/ponytail.git', dest], { timeout: 180000 });
109
- console.log(' [ok] git clone completed');
110
109
  ponytailExtExists = await probeExt();
111
110
  }
112
111
  catch (e3) {
@@ -119,9 +118,9 @@ async function stepPonytail(pluginsDir, userDir, options) {
119
118
  console.log(' [skip] Ponytail pi-extension/index.js still not found — skill-only mode');
120
119
  console.log(' [hint] The /ponytail command won\'t work, but ponytail skills will still load');
121
120
  }
122
- else if (!options.dryRun) {
121
+ else if (!options.dryRun && !options.quiet) {
123
122
  // Wire extension into config.yml so /ponytail command loads
124
- console.log(' [ok] Ponytail pi-extension found');
123
+ debug('Ponytail pi-extension found');
125
124
  }
126
125
  // Still set Ponytail config defaults even without the extension command
127
126
  const configPath = path.join(userDir, 'config.yml');
@@ -134,20 +133,23 @@ async function stepPonytail(pluginsDir, userDir, options) {
134
133
  // Settings → Plugins page (OMP enumerates plugins/package.json dependencies).
135
134
  // Returns true when the package is verified in plugins/node_modules.
136
135
  async function stepSelfPlugin(pluginsDir, options) {
137
- console.log('\n[2/8] Registering tersio as OMP plugin...');
136
+ if (!options.quiet)
137
+ console.log(' Tersio — register plugin');
138
138
  const pkgPath = path.join(pluginsDir, 'package.json');
139
139
  const pkg = await readPluginsPackage(pkgPath);
140
140
  pkg.dependencies[PACKAGE_NAME] = `^${PACKAGE_VERSION}`;
141
141
  for (const legacy of ['oh-my-pi-token-saver', 'tersio-omp']) {
142
142
  if (legacy in pkg.dependencies) {
143
143
  delete pkg.dependencies[legacy];
144
- if (!options.dryRun)
144
+ if (!options.dryRun && !options.quiet)
145
145
  console.log(` [migrate] dropped legacy ${legacy} dependency`);
146
146
  }
147
147
  }
148
148
  if (options.dryRun) {
149
- console.log(` [dry-run] would add ${PACKAGE_NAME}@^${PACKAGE_VERSION} to ${pkgPath}`);
150
- console.log(` [dry-run] would run: npm install --no-audit --no-fund (in ${pluginsDir})`);
149
+ if (verbose && !options.quiet)
150
+ console.log(` [dry-run] would add ${PACKAGE_NAME}@^${PACKAGE_VERSION} to ${pkgPath}`);
151
+ if (verbose && !options.quiet)
152
+ console.log(` [dry-run] would run: npm install --no-audit --no-fund (in ${pluginsDir})`);
151
153
  return false;
152
154
  }
153
155
  // pkg.dependencies[PACKAGE_NAME] was assigned above, so only reinstall skips this fast path.
@@ -155,7 +157,6 @@ async function stepSelfPlugin(pluginsDir, options) {
155
157
  const installedPkgRaw = await readTextIfExists(path.join(pluginsDir, 'node_modules', PACKAGE_NAME, 'package.json'));
156
158
  if (parseJsonObject(installedPkgRaw)?.version === PACKAGE_VERSION) {
157
159
  debug(`${PACKAGE_NAME} already installed at v${PACKAGE_VERSION}; skipping npm install`);
158
- console.log(' [ok] Listed in OMP Settings → Plugins as tersio');
159
160
  return true;
160
161
  }
161
162
  }
@@ -179,7 +180,7 @@ async function stepSelfPlugin(pluginsDir, options) {
179
180
  console.log(` [warn] ${PACKAGE_NAME} not found in plugins/node_modules after install`);
180
181
  return false;
181
182
  }
182
- console.log(' [ok] Listed in OMP Settings → Plugins as tersio');
183
+ debug('tersio listed in OMP plugins');
183
184
  return true;
184
185
  }
185
186
  function resolveRtkTriple() {
@@ -211,19 +212,21 @@ async function downloadRtkChecksums(release) {
211
212
  return null;
212
213
  }
213
214
  }
214
- async function verifyRtkArchive(archivePath, assetName, checksumsText) {
215
+ async function verifyRtkArchive(archivePath, assetName, checksumsText, options = {}) {
215
216
  if (!checksumsText) {
216
- console.log(' [warn] No checksums.txt available — skipping verification');
217
+ if (!options.quiet)
218
+ console.log(' [warn] No checksums.txt available — skipping verification');
217
219
  return true;
218
220
  }
219
221
  const expected = parseChecksum(checksumsText, assetName);
220
222
  const actual = await sha256File(archivePath);
221
223
  if (!expected) {
222
- console.log(` [warn] checksums.txt missing entry for ${assetName} — skipping verification`);
224
+ if (!options.quiet)
225
+ console.log(` [warn] checksums.txt missing entry for ${assetName} — skipping verification`);
223
226
  return true;
224
227
  }
225
228
  if (actual === expected) {
226
- console.log(` [ok] Checksum verified for ${assetName}`);
229
+ debug(`Checksum verified for ${assetName}`);
227
230
  return true;
228
231
  }
229
232
  console.log(` [fail] Checksum mismatch for ${assetName}`);
@@ -272,10 +275,18 @@ async function extractRtkArchive(archivePath, extractDir) {
272
275
  return false;
273
276
  }
274
277
  async function stepRtk(binDir, options) {
275
- console.log('\n[3/8] Installing RTK binary...');
278
+ if (!options.quiet)
279
+ console.log(' RTK — download binary and wire into OMP');
276
280
  const binDest = path.join(binDir, RTK_BINARY_NAME);
277
281
  // Download failure must not skip wiring: a pre-existing rtk binary is
278
282
  // exactly as good for the OMP hook, so wire whatever ends up at binDest.
283
+ // Dry runs stay offline: no registry probe, just the plan line above.
284
+ if (options.dryRun) {
285
+ if (verbose && !options.quiet)
286
+ console.log(` [dry-run] would download rtk binary and install to ${binDest}`);
287
+ await wireRtkOmp(binDest, options);
288
+ return;
289
+ }
279
290
  try {
280
291
  const release = await withInteractiveSpinner('Finding latest RTK release', () => fetchJson(RTK_RELEASE_API));
281
292
  const triple = resolveRtkTriple();
@@ -284,12 +295,6 @@ async function stepRtk(binDir, options) {
284
295
  const asset = findRtkAsset(release, triple);
285
296
  if (!asset)
286
297
  return;
287
- if (options.dryRun) {
288
- console.log(` [dry-run] would download ${asset.name} from release ${release.tag_name}`);
289
- console.log(' [dry-run] would verify checksum against checksums.txt');
290
- console.log(` [dry-run] would extract and install to ${binDest}`);
291
- return;
292
- }
293
298
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'omp-rtk-'));
294
299
  try {
295
300
  const archivePath = path.join(tmpDir, asset.name);
@@ -301,7 +306,7 @@ async function stepRtk(binDir, options) {
301
306
  update('Verifying RTK checksum');
302
307
  return downloadedChecksums;
303
308
  });
304
- if (!await verifyRtkArchive(archivePath, asset.name, checksumsText))
309
+ if (!await verifyRtkArchive(archivePath, asset.name, checksumsText, options))
305
310
  return;
306
311
  const extractDir = path.join(tmpDir, 'extracted');
307
312
  if (!await extractRtkArchive(archivePath, extractDir))
@@ -314,14 +319,12 @@ async function stepRtk(binDir, options) {
314
319
  await fs.mkdir(path.dirname(binDest), { recursive: true });
315
320
  await fs.copyFile(binDest, `${binDest}.bak`).catch(() => { });
316
321
  await fs.copyFile(found, binDest);
317
- console.log(` [write] ${binDest}`);
318
322
  if (!IS_WINDOWS) {
319
323
  await fs.chmod(binDest, 0o755);
320
324
  debug(`chmod 755 ${binDest}`);
321
325
  }
322
326
  try {
323
- const v = (await execP(binDest, ['--version'], { timeout: 30000, shell: false })).stdout.trim();
324
- console.log(` [ok] ${binDest} → ${v}`);
327
+ await execP(binDest, ['--version'], { timeout: 30000, shell: false });
325
328
  }
326
329
  catch {
327
330
  console.log(` [hint] Verify manually: ${binDest} --version`);
@@ -352,7 +355,8 @@ async function stepRtk(binDir, options) {
352
355
  async function copySources(extDir, files, skipLabel, options) {
353
356
  const src = await readTextIfExists(files[0][0]);
354
357
  if (!src) {
355
- console.log(` [skip] ${skipLabel} not found in repo`);
358
+ if (!options.quiet && options.dryRun)
359
+ console.log(` [skip] ${skipLabel} not found in repo`);
356
360
  return false;
357
361
  }
358
362
  await writeIfChanged(path.join(extDir, files[0][1]), src, options);
@@ -366,6 +370,8 @@ async function copySources(extDir, files, skipLabel, options) {
366
370
  return true;
367
371
  }
368
372
  async function stepSharedSessionState(extDir, options) {
373
+ if (!options.quiet)
374
+ console.log(' Shared files — sync session bridge');
369
375
  await copySources(extDir, [
370
376
  [SHARED_SESSION_STATE, path.join('shared', 'session-state.js')],
371
377
  [SHARED_TYPES, path.join('shared', 'types.js')],
@@ -377,14 +383,16 @@ async function stepSharedSessionState(extDir, options) {
377
383
  ], 'shared/session-state.js', options);
378
384
  }
379
385
  async function stepModeReinforcement(extDir, ponytailExtPath, options) {
380
- console.log('\n[8/8] Installing mode reinforcement extension...');
386
+ if (!options.quiet)
387
+ console.log(' Session helpers — sync shared files');
381
388
  const dest = path.join(extDir, 'shared', 'mode-reinforcement.js');
382
389
  if (!await copySources(extDir, [[MODE_REINFORCEMENT_INDEX, path.join('shared', 'mode-reinforcement.js')]], 'shared/mode-reinforcement.js', options))
383
390
  return;
384
391
  await ensureExtensionAfterConfigEntry(path.join(path.dirname(extDir), 'config.yml'), dest, ponytailExtPath, 'mode reinforcement', options);
385
392
  }
386
393
  async function stepRtkSession(extDir, options) {
387
- console.log('\n[4/8] Installing RTK session extension...');
394
+ if (!options.quiet)
395
+ console.log(' RTK session — install session mode');
388
396
  await copySources(extDir, [[RTK_SESSION_INDEX, path.join('rtk-session', 'index.js')]], 'rtk-session/index.js', options);
389
397
  }
390
398
  // One rule fetch serves the install; dry runs stay offline
@@ -401,13 +409,14 @@ async function fetchCavemanRule(options) {
401
409
  }
402
410
  }
403
411
  async function stepCaveman(extDir, rule, options) {
404
- console.log('\n[5/8] Installing Caveman session extension...');
412
+ if (!options.quiet)
413
+ console.log(' Caveman — fetch rule and install session mode');
405
414
  const cavemanDir = path.join(extDir, 'caveman-session');
406
415
  if (!options.dryRun)
407
416
  await fs.mkdir(cavemanDir, { recursive: true });
408
417
  const ruleDest = path.join(cavemanDir, 'rule.md');
409
418
  if (rule === null && (await readTextIfExists(ruleDest)) !== null) {
410
- console.log(' [info] Keeping existing rule.md');
419
+ debug('Keeping existing rule.md');
411
420
  }
412
421
  else if (rule === null) {
413
422
  console.log(' [skip] Caveman rule.md unavailable');
@@ -419,14 +428,16 @@ async function stepCaveman(extDir, rule, options) {
419
428
  await copySources(extDir, [[CAVEMAN_INDEX, path.join('caveman-session', 'index.js')]], 'caveman-session/index.js', options);
420
429
  }
421
430
  async function stepTersioCommands(extDir, options) {
422
- console.log('\n[7/8] Installing Tersio root-command extension...');
431
+ if (!options.quiet)
432
+ console.log(' Tersio commands — install /tersio root command');
423
433
  await copySources(extDir, [[TERSIO_COMMANDS_INDEX, path.join('tersio-commands', 'index.js')]], 'tersio-commands/index.js', options);
424
434
  }
425
435
  async function stepUpdater(extDir, options) {
426
436
  await copySources(extDir, [[UPDATER_INDEX, path.join('ai-addons-updater', 'index.js')]], 'ai-addons-updater/index.js', options);
427
437
  }
428
438
  async function stepCombo(extDir, options) {
429
- console.log('\n[6/8] Installing Combo toggle extension...');
439
+ if (!options.quiet)
440
+ console.log(' Combo — install preset switch');
430
441
  const dest = path.join(extDir, 'combo-toggle', 'index.js');
431
442
  if (!await copySources(extDir, [[COMBO_TOGGLE_INDEX, path.join('combo-toggle', 'index.js')]], 'combo-toggle/index.js', options))
432
443
  return;
@@ -442,10 +453,41 @@ function defaultProfile() {
442
453
  ponytailDefault: 'off',
443
454
  };
444
455
  }
445
- // Install is always user-level (all OMP sessions). Project scope was removed:
446
- // session extensions must live in ~/.omp/agent/extensions to load.
447
- async function resolveProfile(forceReinstall = false) {
448
- const profile = defaultProfile();
456
+ async function storedProfile() {
457
+ const base = defaultProfile();
458
+ const raw = await readTextIfExists(path.join(OMP_PLUGINS_DIR, 'omp-plugins.lock.json'));
459
+ if (!raw)
460
+ return base;
461
+ let stored;
462
+ try {
463
+ const parsed = JSON.parse(raw);
464
+ if (!parsed || typeof parsed !== 'object' || !('settings' in parsed))
465
+ return base;
466
+ const settings = parsed.settings;
467
+ if (!settings || typeof settings !== 'object' || !(PACKAGE_NAME in settings))
468
+ return base;
469
+ const entry = settings[PACKAGE_NAME];
470
+ if (!entry || typeof entry !== 'object')
471
+ return base;
472
+ stored = entry;
473
+ }
474
+ catch {
475
+ return base;
476
+ }
477
+ if (typeof stored.comboDefault === 'string' && stored.comboDefault in COMBO_PRESET_MODES)
478
+ base.comboDefault = stored.comboDefault;
479
+ if (typeof stored.cavemanDefault === 'string' && CAVEMAN_DEFAULTS.has(stored.cavemanDefault))
480
+ base.cavemanDefault = stored.cavemanDefault;
481
+ if (typeof stored.rtkDefault === 'boolean')
482
+ base.rtkDefault = stored.rtkDefault;
483
+ if (typeof stored.ponytailDefault === 'string' && PONYTAIL_DEFAULTS.has(stored.ponytailDefault))
484
+ base.ponytailDefault = stored.ponytailDefault;
485
+ return base;
486
+ }
487
+ async function resolveProfile(forceReinstall = false, opts = {}) {
488
+ // Seed from the lock file so flag-less update/reinstall runs keep the
489
+ // user's configured defaults instead of resetting them to off.
490
+ const profile = await storedProfile();
449
491
  // Single interactive prompt: the Combo preset implies all three modes.
450
492
  // Numbered menu — no typing preset names.
451
493
  // Only for a real user at a terminal, only when no default flags were
@@ -456,7 +498,7 @@ async function resolveProfile(forceReinstall = false) {
456
498
  { value: 'medium', label: 'medium', hint: 'caveman=lite, rtk=on, ponytail=lite' },
457
499
  { value: 'balanced', label: 'balanced', hint: 'caveman=full, rtk=on, ponytail=full' },
458
500
  { value: 'max', label: 'max', hint: 'caveman=ultra, rtk=on, ponytail=ultra' },
459
- ], 'off');
501
+ ], profile.comboDefault);
460
502
  if (choice.status === 'selected')
461
503
  profile.comboDefault = choice.value;
462
504
  else {
@@ -477,32 +519,10 @@ async function resolveProfile(forceReinstall = false) {
477
519
  profile.rtkDefault = rtkDefaultFlag === 'on';
478
520
  if (ponytailDefaultFlag !== undefined)
479
521
  profile.ponytailDefault = ponytailDefaultFlag;
480
- console.log(` Profile: combo default=${profile.comboDefault} (caveman=${profile.cavemanDefault} · rtk=${profile.rtkDefault ? 'on' : 'off'} · ponytail=${profile.ponytailDefault})`);
522
+ if ((!opts.quiet && verbose) || dryRun)
523
+ console.log(` Defaults: combo=${profile.comboDefault} (caveman=${profile.cavemanDefault} · rtk=${profile.rtkDefault ? 'on' : 'off'} · ponytail=${profile.ponytailDefault})`);
481
524
  return profile;
482
525
  }
483
- // ponytail: post-install guard. List entries under `extensions: null` break
484
- // OMP launch, so fail loudly here instead of at the next OMP start.
485
- async function validateConfigExtensions(agentDir, options) {
486
- if (options.dryRun)
487
- return;
488
- const raw = await readTextIfExists(path.join(agentDir, 'config.yml'));
489
- if (!raw)
490
- return;
491
- const lines = raw.split('\n');
492
- const keyIdx = lines.findIndex((l) => EXTENSIONS_KEY_RE.test(l));
493
- if (keyIdx === -1)
494
- return;
495
- const scalarNull = /^\s*extensions\s*:\s*(null|~|\[\s*\])\s*$/i.test(lines[keyIdx]);
496
- const hasEntries = lines.slice(keyIdx + 1).some((l) => /^\s*-\s+\S/.test(l));
497
- if (scalarNull && hasEntries) {
498
- console.log(' [fail] config.yml lists extensions under `extensions: null` — OMP will fail to launch.');
499
- console.log(' [hint] Replace the `extensions: null` line with `extensions:`, then reinstall.');
500
- process.exitCode = 1;
501
- }
502
- else {
503
- console.log(' [ok] config.yml extensions key valid');
504
- }
505
- }
506
526
  // Persist the profile as omp plugin settings so `omp plugin config get`
507
527
  // reflects the choice and the extensions pick it up on session start.
508
528
  async function writePluginSettings(profile, options) {
@@ -528,7 +548,8 @@ async function writePluginSettings(profile, options) {
528
548
  ponytailDefault: profile.ponytailDefault,
529
549
  };
530
550
  if (options.dryRun) {
531
- console.log(` [dry-run] would write plugin settings (${PACKAGE_NAME}) to ${lockPath}`);
551
+ if (verbose && !options.quiet)
552
+ console.log(` [dry-run] would write plugin settings (${PACKAGE_NAME}) to ${lockPath}`);
532
553
  return;
533
554
  }
534
555
  await fs.mkdir(pluginsDir, { recursive: true });
@@ -575,16 +596,13 @@ async function runCommandMenu() {
575
596
  await runInstall();
576
597
  break;
577
598
  case 'update': {
578
- // One bound flow: report the CLI version, then offer the add-on
579
- // refresh in the same pass — no redundant second "update" option.
580
- const latest = await checkForUpdate(true);
581
- if (typeof latest === 'string')
582
- console.log(` [update] tersio ${latest} available (installed ${PACKAGE_VERSION})`);
583
- else if (latest === 'unknown')
584
- console.log(` [warn] could not reach the npm registry — run \`tersio update\` to retry`);
599
+ // One bound flow: the version check already ran above, so report it
600
+ // and offer the refresh in the same breath — no second "update" quiz.
601
+ if (typeof newer === 'string')
602
+ console.log(` tersio ${newer} available (installed ${PACKAGE_VERSION})`);
585
603
  else
586
- console.log(` [ok] tersio ${PACKAGE_VERSION} is the latest`);
587
- const go = await askInteractiveConfirm('Update add-ons now (RTK, Caveman rule, Ponytail)?');
604
+ console.log(` tersio ${PACKAGE_VERSION} is the latest`);
605
+ const go = await askInteractiveConfirm('Update now (CLI + RTK, Caveman rule, Ponytail)?');
588
606
  if (go.status === 'confirmed' && go.value) {
589
607
  await runLatestUpdate();
590
608
  }
@@ -633,19 +651,23 @@ async function runInstall(overrides = {}) {
633
651
  await runCommandMenu();
634
652
  return;
635
653
  }
636
- printWelcome();
654
+ // apply-update is the delegated payload of `tersio update`: stay silent —
655
+ // the parent already printed the plan and owns the closing summary. Banner
656
+ // repeats here otherwise (the reported double-print), burying real output.
657
+ const quiet = applyUpdate;
658
+ if (!quiet)
659
+ printWelcome();
637
660
  if (isReinstall) {
638
661
  // removeRtk stays false: reinstall is about to replace the binary, and
639
662
  // deleting it first would leave nothing to wire if the fresh download
640
663
  // fails (rate limit, offline). rtk.ts is removed here and re-wired below.
641
664
  await runUninstall({ yes: true, removePonytail: false, removeRtk: false });
642
665
  }
643
- if (dryRun)
644
- console.log('[dry-run] No changes will be written.\n');
645
- console.log(`=== Tersio v${PACKAGE_VERSION} ===`);
646
- console.log(` Platform: ${process.platform}`);
647
- console.log(` Arch: ${process.arch}`);
648
- console.log(` Home: ${HOME}`);
666
+ if (dryRun && !quiet)
667
+ console.log('Preview — no changes will be written.\n');
668
+ if (!quiet) {
669
+ console.log(`Installing Tersio v${PACKAGE_VERSION}`);
670
+ }
649
671
  // Remind humans a newer release exists; silent for scripts (no TTY) and for
650
672
  // the apply-update payload, which is itself an update run.
651
673
  if (tty() && !applyUpdate && command !== 'uninstall') {
@@ -672,47 +694,54 @@ async function runInstall(overrides = {}) {
672
694
  }
673
695
  }
674
696
  }
675
- console.log(' Scope: user (all OMP sessions)');
676
697
  // Resolve session defaults: flags > interactive prompt > defaults.
677
- const profile = await resolveProfile(isReinstall);
698
+ const profile = await resolveProfile(isReinstall, { quiet });
678
699
  const userDir = OMP_AGENT_DIR;
679
700
  const userExtDir = path.join(userDir, 'extensions');
680
701
  // apply-update is `tersio update`'s payload run: treat it like reinstall so
681
702
  // the add-ons refresh too — Ponytail package via npm, self plugin, RTK
682
703
  // binary (always re-downloaded), and the Caveman rule (always re-fetched).
683
- const options = { dryRun, verbose, yes, reinstall: isReinstall || applyUpdate };
684
- // Check prerequisites
685
- console.log('\nPrerequisites:');
704
+ const options = { dryRun, verbose, yes, reinstall: isReinstall || applyUpdate, quiet };
686
705
  try {
687
706
  const v = (await execP(OMP_BIN, ['--version'])).stdout.trim();
688
- console.log(` [ok] omp ${v}`);
707
+ if (verbose && !quiet)
708
+ console.log(` omp ${v}`);
689
709
  }
690
710
  catch {
691
711
  console.log(' [fail] omp not found — ensure it\'s installed');
692
712
  }
693
713
  const cavemanRule = await fetchCavemanRule(options);
694
- console.log('\n--- User-level install ---');
695
- await stepSharedSessionState(userExtDir, options);
696
- await stepPonytail(OMP_PLUGINS_DIR, userDir, options);
697
- const selfPlugin = await stepSelfPlugin(OMP_PLUGINS_DIR, options);
714
+ const failures = [];
715
+ const capture = async (label, work) => {
716
+ try {
717
+ await work();
718
+ }
719
+ catch (e) {
720
+ failures.push(label);
721
+ console.log(` [fail] ${label}: ${shortError(e)}`);
722
+ }
723
+ };
724
+ await capture('shared', () => stepSharedSessionState(userExtDir, options));
725
+ await capture('ponytail', () => stepPonytail(OMP_PLUGINS_DIR, userDir, options));
726
+ let selfPlugin = false;
727
+ await capture('self-plugin', async () => { selfPlugin = await stepSelfPlugin(OMP_PLUGINS_DIR, options); });
698
728
  const ponytailExtPath = path.join(OMP_PLUGINS_DIR, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js');
699
- await stepRtk(BUN_BIN_DIR, options);
700
- await stepRtkSession(userExtDir, options);
701
- await stepCaveman(userExtDir, cavemanRule, options);
702
- await stepCombo(userExtDir, options);
703
- await stepTersioCommands(userExtDir, options);
704
- await stepModeReinforcement(userExtDir, ponytailExtPath, options);
705
- await stepUpdater(userExtDir, options);
729
+ await capture('rtk', () => stepRtk(BUN_BIN_DIR, options));
730
+ await capture('rtk session', () => stepRtkSession(userExtDir, options));
731
+ await capture('caveman', () => stepCaveman(userExtDir, cavemanRule, options));
732
+ await capture('combo', () => stepCombo(userExtDir, options));
733
+ await capture('commands', () => stepTersioCommands(userExtDir, options));
734
+ await capture('reinforcement', () => stepModeReinforcement(userExtDir, ponytailExtPath, options));
735
+ await capture('updater', () => stepUpdater(userExtDir, options));
706
736
  if (selfPlugin)
707
- await writePluginSettings(profile, options);
708
- console.log('\n=== Installation complete ===');
709
- console.log('\nNext steps:');
710
- console.log(' 1. Restart OMP');
711
- console.log(' 2. /caveman full');
712
- console.log(' 3. /rtk on');
713
- console.log(' 4. /ponytail full');
714
- console.log(' 5. /ai-addons check');
715
- console.log(' 6. /combo medium (toggle all 3 at once — off by default)');
737
+ await capture('settings', () => writePluginSettings(profile, options));
738
+ if (quiet) {
739
+ if (failures.length > 0)
740
+ console.log(` add-ons: ${failures.length} failed (${failures.join(', ')}) — see [fail] lines above`);
741
+ }
742
+ else {
743
+ console.log(failures.length === 0 ? '\nDone — restart OMP, then /combo medium.' : `\nDone with ${failures.length} failure(s) — see [fail] lines above.`);
744
+ }
716
745
  closeRL();
717
746
  }
718
747
  export { runInstall };
package/cli/rtk-wiring.js CHANGED
@@ -1,12 +1,20 @@
1
1
  // cli/rtk-wiring.ts — wire the installed rtk binary into OMP.
2
2
  // `rtk init -g --agent omp` writes rtk's tool_call extension to
3
- // ~/.omp/agent/extensions/rtk.ts; OMP auto-loads it and rewrites bash
4
- // commands to rtk before execution, so every rewritten call lands in
5
- // rtk's history.db and shows metered savings in the gain dashboard.
3
+ // ~/.omp/agent/extensions/rtk.ts, and we register that path in config.yml
4
+ // (OMP only loads listed extensions — rtk init does not register it).
5
+ // Once loaded, OMP rewrites bash commands to rtk before execution, so every
6
+ // rewritten call lands in rtk's history.db and shows metered savings.
6
7
  // Fail-open: a failed wire only downgrades to manual `rtk` prefixing.
7
8
  // Deliberately free of cli/common.ts imports (argv side effects) so tests
8
9
  // can load it directly.
10
+ import { promises as fs } from 'node:fs';
11
+ import os from 'node:os';
12
+ import path from 'node:path';
9
13
  import { execFile } from 'node:child_process';
14
+ function debugWire(options, msg) {
15
+ if (!options.quiet)
16
+ console.log(` ${msg}`);
17
+ }
10
18
  function shortWiringError(e) {
11
19
  const err = e;
12
20
  const why = [err.code !== undefined ? `code ${err.code}` : '', err.signal ?? '', err.stderr?.trim(), err.message]
@@ -29,14 +37,64 @@ function execFileP(cmd, args, timeout) {
29
37
  // Idempotent: rtk rewrites its extension file on every init run, so
30
38
  // reinstalling tersio refreshes the wiring for free.
31
39
  export async function wireRtkOmp(rtkBin, options = {}) {
32
- console.log(' Wiring rtk → OMP (bash tool_call rewrite)...');
33
- if (options.dryRun) {
34
- console.log(' [dry-run] would run: rtk init -g --agent omp');
40
+ if (!options.dryRun)
41
+ debugWire(options, 'Wiring rtk → OMP (bash tool_call rewrite)…');
42
+ if (options.dryRun)
35
43
  return true;
44
+ const wired = await runRtkInit(rtkBin, options);
45
+ if (!wired)
46
+ return false;
47
+ await ensureRtkInConfig(options);
48
+ return true;
49
+ }
50
+ // The rtk.ts path rtk init writes (mirrors OMP_AGENT_DIR in cli/common.ts
51
+ // without importing it — see header note on argv side effects).
52
+ function rtkExtensionPath() {
53
+ const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
54
+ return path.join(home, '.omp', 'agent', 'extensions', 'rtk.ts');
55
+ }
56
+ // OMP loads only listed extensions: append rtk.ts after the existing entries
57
+ // (or create the key) so a fresh wire takes effect on next OMP start.
58
+ async function ensureRtkInConfig(options) {
59
+ const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
60
+ const configPath = path.join(home, '.omp', 'agent', 'config.yml');
61
+ const extPath = rtkExtensionPath().replace(/\\/g, '/');
62
+ let raw = null;
63
+ try {
64
+ raw = await fs.readFile(configPath, 'utf8');
65
+ }
66
+ catch {
67
+ return;
68
+ }
69
+ if (raw.includes(extPath)) {
70
+ debugWire(options, 'rtk already in config.yml');
71
+ return;
36
72
  }
73
+ const lines = raw.split('\n');
74
+ const keyIdx = lines.findIndex((l) => /^\s*extensions\s*:/i.test(l));
75
+ const line = ` - ${extPath}`;
76
+ if (keyIdx === -1) {
77
+ lines.push('extensions:', line, '');
78
+ }
79
+ else {
80
+ if (/^\s*extensions\s*:\s*(?:\[\s*\]|null|~)?\s*$/i.test(lines[keyIdx]))
81
+ lines[keyIdx] = 'extensions:';
82
+ lines.splice(keyIdx + 1, 0, line);
83
+ }
84
+ try {
85
+ await fs.mkdir(path.dirname(configPath), { recursive: true });
86
+ await fs.copyFile(configPath, `${configPath}.bak`).catch(() => { });
87
+ await fs.writeFile(configPath, lines.join('\n'), 'utf8');
88
+ debugWire(options, 'rtk registered in config.yml');
89
+ }
90
+ catch (e) {
91
+ console.log(` [warn] could not register rtk in config.yml: ${e.message}`);
92
+ }
93
+ }
94
+ async function runRtkInit(rtkBin, options) {
37
95
  try {
38
96
  await execFileP(rtkBin, ['init', '-g', '--agent', 'omp'], 30000);
39
- console.log(' [ok] rtk OMP extension wired (~/.omp/agent/extensions/rtk.ts)');
97
+ debugWire(options, 'rtk OMP extension wired');
40
98
  return true;
41
99
  }
42
100
  catch (first) {
@@ -45,7 +103,7 @@ export async function wireRtkOmp(rtkBin, options = {}) {
45
103
  await new Promise((resolve) => setTimeout(resolve, 2000));
46
104
  try {
47
105
  await execFileP(rtkBin, ['init', '-g', '--agent', 'omp'], 30000);
48
- console.log(' [ok] rtk OMP extension wired (~/.omp/agent/extensions/rtk.ts)');
106
+ debugWire(options, 'rtk OMP extension wired');
49
107
  return true;
50
108
  }
51
109
  catch (e) {