@agentworkforce/cli 0.16.0 → 0.17.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.
- package/CHANGELOG.md +17 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +188 -138
- package/dist/cli.js.map +1 -1
- package/dist/cli.test.js +15 -4
- package/dist/cli.test.js.map +1 -1
- package/dist/local-personas.d.ts +6 -0
- package/dist/local-personas.d.ts.map +1 -1
- package/dist/local-personas.js +10 -0
- package/dist/local-personas.js.map +1 -1
- package/dist/local-personas.test.js +105 -0
- package/dist/local-personas.test.js.map +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.17.1] - 2026-05-08
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Address review feedback on CLI launch spinner PR
|
|
15
|
+
- Show spinners during persona session install and exit sync
|
|
16
|
+
|
|
17
|
+
## [0.17.0] - 2026-05-08
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- **Add optional defaultTier to PersonaSpec**
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- Agent: consult routingProfiles before defaultTier on no-@<tier>
|
|
26
|
+
|
|
10
27
|
## [0.16.0] - 2026-05-08
|
|
11
28
|
|
|
12
29
|
### Added
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAkBA,OAAO,EAUL,KAAK,OAAO,EACZ,KAAK,YAAY,EACjB,KAAK,gBAAgB,EAIrB,KAAK,aAAa,EACnB,MAAM,iCAAiC,CAAC;
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ import { dirname, isAbsolute, join, resolve as resolvePath } from 'node:path';
|
|
|
7
7
|
import { pathToFileURL } from 'node:url';
|
|
8
8
|
import { HARNESS_VALUES, materializeSkills, PERSONA_TAGS, PERSONA_TIERS, listBuiltInPersonas, personaCatalog, resolveSidecar, routingProfiles, useSelection } from '@agentworkforce/workload-router';
|
|
9
9
|
import { buildInteractiveSpec, detectHarnesses, formatDropWarnings, MissingPersonaInputError, renderPersonaInputs, resolvePersonaInputs, resolveMcpServersLenient, resolveStringMapLenient } from '@agentworkforce/harness-kit';
|
|
10
|
-
import {
|
|
10
|
+
import { createMount, readAgentDotfiles } from '@relayfile/local-mount';
|
|
11
11
|
import ora from 'ora';
|
|
12
12
|
import { startLaunchMetadataRecording } from './launch-metadata.js';
|
|
13
13
|
import { buildPersonaSourceDirectories, defaultCwdPersonaDir, loadLocalPersonas, loadPersonaSourceConfig, normalizePersonaDir, savePersonaSourceConfig } from './local-personas.js';
|
|
@@ -34,9 +34,11 @@ Commands:
|
|
|
34
34
|
--no-launch-metadata
|
|
35
35
|
Same behavior as agent.
|
|
36
36
|
agent [flags] <persona>[@<tier>]
|
|
37
|
-
Run a persona. Tier one of: ${PERSONA_TIERS.join(' | ')}
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
Run a persona. Tier one of: ${PERSONA_TIERS.join(' | ')}.
|
|
38
|
+
With no @<tier>, the resolution order is:
|
|
39
|
+
routingProfiles.default.intents (built-in personas only)
|
|
40
|
+
→ persona.defaultTier (when set) → best-value. Drops into
|
|
41
|
+
an interactive harness session.
|
|
40
42
|
|
|
41
43
|
Flags:
|
|
42
44
|
--install-in-repo Disengage the sandbox mount and
|
|
@@ -232,14 +234,22 @@ function parseSelector(sel) {
|
|
|
232
234
|
const tierRaw = at === -1 ? undefined : sel.slice(at + 1);
|
|
233
235
|
if (!key)
|
|
234
236
|
die('Missing persona name before "@"');
|
|
235
|
-
|
|
236
|
-
if (tierRaw !== undefined && !PERSONA_TIERS.includes(tier)) {
|
|
237
|
+
if (tierRaw !== undefined && !PERSONA_TIERS.includes(tierRaw)) {
|
|
237
238
|
die(`Invalid tier "${tierRaw}". Must be one of: ${PERSONA_TIERS.join(', ')}`);
|
|
238
239
|
}
|
|
239
240
|
const result = resolveSpec(key);
|
|
240
241
|
if ('error' in result)
|
|
241
242
|
die(result.error, false);
|
|
242
243
|
const kind = local.byId.has(key) ? 'local' : 'repo';
|
|
244
|
+
// Resolution order when no @<tier> is given: routingProfiles default for the
|
|
245
|
+
// persona's intent (built-ins only — local personas with custom intents miss
|
|
246
|
+
// the lookup and fall through), then the persona's own defaultTier, then
|
|
247
|
+
// 'best-value'. Mirrors `resolveShowTarget` and the `list` recommended-tier
|
|
248
|
+
// filter so all three commands agree on what "no tier" means.
|
|
249
|
+
const profileRule = kind === 'repo'
|
|
250
|
+
? routingProfiles.default.intents[result.intent]
|
|
251
|
+
: undefined;
|
|
252
|
+
const tier = (tierRaw ?? profileRule?.tier ?? result.defaultTier ?? 'best-value');
|
|
243
253
|
if (kind === 'local') {
|
|
244
254
|
return { kind, source: local.sources.get(result.id) ?? 'cwd', spec: result, tier };
|
|
245
255
|
}
|
|
@@ -316,17 +326,53 @@ function subprocessExitCode(res) {
|
|
|
316
326
|
return signalExitCode(res.signal);
|
|
317
327
|
return 1;
|
|
318
328
|
}
|
|
319
|
-
|
|
329
|
+
/**
|
|
330
|
+
* Run a skill-install subprocess behind an ora spinner. stdout and stderr are
|
|
331
|
+
* captured so a successful install collapses to a single ✓ line; on failure,
|
|
332
|
+
* the buffered output is dumped after spinner.fail so the user sees what
|
|
333
|
+
* actually broke. stdin is ignored — the install commands don't prompt.
|
|
334
|
+
*
|
|
335
|
+
* The spinner text stays "Installing skills…" while running; the longer
|
|
336
|
+
* `label` (which includes target paths and skill ids) is shown on
|
|
337
|
+
* success/failure so the verbose detail is still discoverable in logs.
|
|
338
|
+
*/
|
|
339
|
+
function runInstallWithSpinner(command, label, cwd) {
|
|
320
340
|
const [bin, ...args] = command;
|
|
321
341
|
if (!bin)
|
|
322
|
-
return;
|
|
323
|
-
process.stderr
|
|
324
|
-
const res = spawnSync(bin, args, {
|
|
342
|
+
return { code: 0, output: '' };
|
|
343
|
+
const spinner = ora({ text: 'Installing skills…', stream: process.stderr }).start();
|
|
344
|
+
const res = spawnSync(bin, args, {
|
|
345
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
346
|
+
shell: false,
|
|
347
|
+
encoding: 'utf8',
|
|
348
|
+
// Default is 1 MiB; verbose `npx prpm install` / `npx skills add` runs
|
|
349
|
+
// can blow past it, which would have spawnSync kill the child with
|
|
350
|
+
// ENOBUFS and report a spurious failure. 100 MiB is well past anything
|
|
351
|
+
// these installers print in practice.
|
|
352
|
+
maxBuffer: 100 * 1024 * 1024,
|
|
353
|
+
...(cwd ? { cwd } : {})
|
|
354
|
+
});
|
|
355
|
+
const output = `${res.stdout ?? ''}${res.stderr ?? ''}`;
|
|
325
356
|
const code = subprocessExitCode(res);
|
|
326
|
-
if (code
|
|
327
|
-
|
|
328
|
-
process.exit(code);
|
|
357
|
+
if (code === 0) {
|
|
358
|
+
spinner.succeed(label);
|
|
329
359
|
}
|
|
360
|
+
else {
|
|
361
|
+
spinner.fail(`${label} failed (exit ${code})`);
|
|
362
|
+
if (output.trim())
|
|
363
|
+
process.stderr.write(output.endsWith('\n') ? output : `${output}\n`);
|
|
364
|
+
}
|
|
365
|
+
return { code, output };
|
|
366
|
+
}
|
|
367
|
+
function runInstall(command, label, cwd) {
|
|
368
|
+
const [bin] = command;
|
|
369
|
+
if (!bin)
|
|
370
|
+
return;
|
|
371
|
+
// runInstallWithSpinner already prints the failure line via spinner.fail;
|
|
372
|
+
// the previous extra "${label} failed … Aborting." write would duplicate it.
|
|
373
|
+
const { code } = runInstallWithSpinner(command, label, cwd);
|
|
374
|
+
if (code !== 0)
|
|
375
|
+
process.exit(code);
|
|
330
376
|
}
|
|
331
377
|
/**
|
|
332
378
|
* Thrown by `runInstallOrThrow` when the install subprocess exits non-zero.
|
|
@@ -343,16 +389,14 @@ class InstallCommandError extends Error {
|
|
|
343
389
|
}
|
|
344
390
|
/**
|
|
345
391
|
* Install variant that throws instead of calling `process.exit` on failure.
|
|
346
|
-
* Used inside
|
|
392
|
+
* Used inside the mount branch's onBeforeLaunch step so mount teardown runs
|
|
347
393
|
* before the error surfaces.
|
|
348
394
|
*/
|
|
349
395
|
function runInstallOrThrow(command, label, cwd) {
|
|
350
|
-
const [bin
|
|
396
|
+
const [bin] = command;
|
|
351
397
|
if (!bin)
|
|
352
398
|
return;
|
|
353
|
-
|
|
354
|
-
const res = spawnSync(bin, args, { stdio: 'inherit', shell: false, cwd });
|
|
355
|
-
const code = subprocessExitCode(res);
|
|
399
|
+
const { code } = runInstallWithSpinner(command, label, cwd);
|
|
356
400
|
if (code !== 0) {
|
|
357
401
|
throw new InstallCommandError(label, code);
|
|
358
402
|
}
|
|
@@ -996,36 +1040,33 @@ async function runInteractive(selection, options) {
|
|
|
996
1040
|
configFilePaths: spec.configFiles.map((file) => file.path)
|
|
997
1041
|
});
|
|
998
1042
|
process.stderr.write(`• sandbox mount → ${mountDir}\n`);
|
|
999
|
-
//
|
|
1000
|
-
//
|
|
1001
|
-
//
|
|
1002
|
-
//
|
|
1003
|
-
//
|
|
1043
|
+
// Inline mount lifecycle (formerly delegated to launchOnMount) so we can
|
|
1044
|
+
// surface a spinner the moment the child exits — not just when the user
|
|
1045
|
+
// presses Ctrl-C. The sync-back walks both trees and can take several
|
|
1046
|
+
// seconds on a large repo; without an indicator, exiting the persona via
|
|
1047
|
+
// /exit looked like a hang.
|
|
1004
1048
|
//
|
|
1005
|
-
//
|
|
1006
|
-
//
|
|
1007
|
-
//
|
|
1008
|
-
//
|
|
1009
|
-
//
|
|
1010
|
-
//
|
|
1011
|
-
//
|
|
1012
|
-
//
|
|
1013
|
-
//
|
|
1014
|
-
//
|
|
1015
|
-
//
|
|
1049
|
+
// SIGINT semantics:
|
|
1050
|
+
// • While the child is running: Ctrl-C reaches the harness directly via
|
|
1051
|
+
// the controlling TTY's foreground process group (the child is
|
|
1052
|
+
// spawned with `stdio: 'inherit'` and inherits the parent's pgid). We
|
|
1053
|
+
// register a no-op handler here purely to suppress Node's default
|
|
1054
|
+
// exit-on-SIGINT — forwarding via child.kill('SIGINT') would deliver
|
|
1055
|
+
// a *second* SIGINT and break harnesses that escalate on repeated
|
|
1056
|
+
// interrupts (e.g. claude treats 1st = cancel, 2nd = quit).
|
|
1057
|
+
// • While syncing: 1st press aborts the shutdownSignal (relayfile then
|
|
1058
|
+
// skips autosync's draining reconcile and returns the partial count
|
|
1059
|
+
// from the final syncBack). 2nd press hard-exits and rms the session
|
|
1060
|
+
// dir so no mount is left behind.
|
|
1016
1061
|
const shutdownController = new AbortController();
|
|
1017
|
-
let sigintCount = 0;
|
|
1018
1062
|
let syncSpinner;
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
text: 'Syncing session changes back to the repo… (Ctrl-C again to skip)',
|
|
1024
|
-
stream: process.stderr
|
|
1025
|
-
}).start();
|
|
1063
|
+
let isSyncing = false;
|
|
1064
|
+
let abortPresses = 0;
|
|
1065
|
+
const sigintHandler = () => {
|
|
1066
|
+
if (!isSyncing)
|
|
1026
1067
|
return;
|
|
1027
|
-
|
|
1028
|
-
if (
|
|
1068
|
+
abortPresses += 1;
|
|
1069
|
+
if (abortPresses === 1) {
|
|
1029
1070
|
if (syncSpinner) {
|
|
1030
1071
|
syncSpinner.text =
|
|
1031
1072
|
'Aborting sync — partial changes will be propagated. (Ctrl-C again to force quit)';
|
|
@@ -1040,8 +1081,6 @@ async function runInteractive(selection, options) {
|
|
|
1040
1081
|
else {
|
|
1041
1082
|
process.stderr.write('\n✗ Force-quit: mount teardown skipped. Session dir may be left behind.\n');
|
|
1042
1083
|
}
|
|
1043
|
-
// Node-native removal rather than `rm -rf` so the emergency path
|
|
1044
|
-
// works on Windows too.
|
|
1045
1084
|
try {
|
|
1046
1085
|
rmSync(sessionRoot, { recursive: true, force: true });
|
|
1047
1086
|
}
|
|
@@ -1050,97 +1089,96 @@ async function runInteractive(selection, options) {
|
|
|
1050
1089
|
}
|
|
1051
1090
|
process.exit(130);
|
|
1052
1091
|
};
|
|
1053
|
-
process.on('SIGINT',
|
|
1092
|
+
process.on('SIGINT', sigintHandler);
|
|
1093
|
+
const handle = createMount(process.cwd(), mountDir, {
|
|
1094
|
+
ignoredPatterns: [...ignoredPatterns],
|
|
1095
|
+
readonlyPatterns: [...readonlyPatterns],
|
|
1096
|
+
excludeDirs: [],
|
|
1097
|
+
agentName: personaId,
|
|
1098
|
+
// Pull `.git` into the mount so git commands work inside the sandbox.
|
|
1099
|
+
// relayfile treats this as one-way project→mount: host-side `.git`
|
|
1100
|
+
// changes flow in, mount-side commits/refs stay sandboxed and are
|
|
1101
|
+
// discarded on cleanup. The agent must `git push` to persist work.
|
|
1102
|
+
includeGit: true
|
|
1103
|
+
});
|
|
1104
|
+
let autoSync;
|
|
1105
|
+
let exitCode = 0;
|
|
1054
1106
|
try {
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
const aborted = shutdownController.signal.aborted;
|
|
1092
|
-
const qualifier = aborted ? ' (partial)' : '';
|
|
1093
|
-
const message = count > 0
|
|
1094
|
-
? `Session complete — ${count} file event${count === 1 ? '' : 's'} during session${qualifier}.`
|
|
1095
|
-
: 'Session complete — no file events.';
|
|
1096
|
-
if (syncSpinner) {
|
|
1097
|
-
syncSpinner.succeed(message);
|
|
1098
|
-
syncSpinner = undefined;
|
|
1099
|
-
}
|
|
1100
|
-
else {
|
|
1101
|
-
process.stderr.write(`✓ ${message}\n`);
|
|
1102
|
-
}
|
|
1103
|
-
},
|
|
1104
|
-
onBeforeLaunch: async (dir) => {
|
|
1105
|
-
// Run before install / configFile writes so the freshly written
|
|
1106
|
-
// files (e.g. `.opencode/`, `opencode.json`) aren't yet present
|
|
1107
|
-
// when we run `git ls-files` to pick skip-worktree candidates —
|
|
1108
|
-
// we don't need them flagged in the index, just hidden via the
|
|
1109
|
-
// `.git/info/exclude` block.
|
|
1110
|
-
configureGitForMount(dir, ignoredPatterns);
|
|
1111
|
-
if (deferInstallToMount) {
|
|
1112
|
-
runInstallOrThrow(install.command, installLabel, dir);
|
|
1113
|
-
}
|
|
1114
|
-
for (const file of spec.configFiles) {
|
|
1115
|
-
assertSafeRelativePath(file.path);
|
|
1116
|
-
const target = join(dir, file.path);
|
|
1117
|
-
// mkdir -p for any subdirs in file.path — the
|
|
1118
|
-
// InteractiveConfigFile contract allows nested relative
|
|
1119
|
-
// paths, and writeFileSync would otherwise throw ENOENT.
|
|
1120
|
-
mkdirSync(dirname(target), { recursive: true });
|
|
1121
|
-
writeFileSync(target, file.contents, 'utf8');
|
|
1122
|
-
}
|
|
1123
|
-
if (resolvedSidecar) {
|
|
1124
|
-
const body = buildSidecarBody(resolvedSidecar, process.cwd());
|
|
1125
|
-
writeFileSync(join(dir, resolvedSidecar.mountFile), body, 'utf8');
|
|
1126
|
-
}
|
|
1127
|
-
launchMetadata = await startLaunchMetadataForLaunch(dir);
|
|
1128
|
-
}
|
|
1107
|
+
// Run before install / configFile writes so the freshly written files
|
|
1108
|
+
// (e.g. `.opencode/`, `opencode.json`) aren't yet present when we run
|
|
1109
|
+
// `git ls-files` to pick skip-worktree candidates — we don't need them
|
|
1110
|
+
// flagged in the index, just hidden via the `.git/info/exclude` block.
|
|
1111
|
+
configureGitForMount(handle.mountDir, ignoredPatterns);
|
|
1112
|
+
if (deferInstallToMount) {
|
|
1113
|
+
runInstallOrThrow(install.command, installLabel, handle.mountDir);
|
|
1114
|
+
}
|
|
1115
|
+
for (const file of spec.configFiles) {
|
|
1116
|
+
assertSafeRelativePath(file.path);
|
|
1117
|
+
const target = join(handle.mountDir, file.path);
|
|
1118
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
1119
|
+
writeFileSync(target, file.contents, 'utf8');
|
|
1120
|
+
}
|
|
1121
|
+
if (resolvedSidecar) {
|
|
1122
|
+
const body = buildSidecarBody(resolvedSidecar, process.cwd());
|
|
1123
|
+
writeFileSync(join(handle.mountDir, resolvedSidecar.mountFile), body, 'utf8');
|
|
1124
|
+
}
|
|
1125
|
+
launchMetadata = await startLaunchMetadataForLaunch(handle.mountDir);
|
|
1126
|
+
autoSync = handle.startAutoSync();
|
|
1127
|
+
const childEnv = resolvedEnv ? { ...process.env, ...resolvedEnv } : process.env;
|
|
1128
|
+
exitCode = await new Promise((resolve, reject) => {
|
|
1129
|
+
const child = spawn(spec.bin, finalArgs, {
|
|
1130
|
+
cwd: handle.mountDir,
|
|
1131
|
+
stdio: 'inherit',
|
|
1132
|
+
env: childEnv
|
|
1133
|
+
});
|
|
1134
|
+
child.on('error', reject);
|
|
1135
|
+
child.on('close', (code, signal) => {
|
|
1136
|
+
if (typeof code === 'number')
|
|
1137
|
+
resolve(code);
|
|
1138
|
+
else if (signal)
|
|
1139
|
+
resolve(signalExitCode(signal));
|
|
1140
|
+
else
|
|
1141
|
+
resolve(1);
|
|
1142
|
+
});
|
|
1129
1143
|
});
|
|
1130
|
-
|
|
1144
|
+
// Child exited — start the spinner immediately so the sync-back is
|
|
1145
|
+
// visibly live rather than a silent pause.
|
|
1146
|
+
isSyncing = true;
|
|
1147
|
+
syncSpinner = ora({
|
|
1148
|
+
text: 'Syncing session changes back to the repo… (Ctrl-C to skip)',
|
|
1149
|
+
stream: process.stderr
|
|
1150
|
+
}).start();
|
|
1151
|
+
let count = 0;
|
|
1152
|
+
if (autoSync) {
|
|
1153
|
+
await autoSync.stop({ signal: shutdownController.signal });
|
|
1154
|
+
count += autoSync.totalChanges();
|
|
1155
|
+
autoSync = undefined;
|
|
1156
|
+
}
|
|
1157
|
+
// NOTE: `count` is bidirectional — it sums autosync activity in both
|
|
1158
|
+
// directions (inbound project→mount and outbound mount→project,
|
|
1159
|
+
// including deletes) plus the final mount→project syncBack. Phrase as
|
|
1160
|
+
// "file events during session" so we don't overclaim direction.
|
|
1161
|
+
count += await handle.syncBack({ signal: shutdownController.signal });
|
|
1162
|
+
const aborted = shutdownController.signal.aborted;
|
|
1163
|
+
const qualifier = aborted ? ' (partial)' : '';
|
|
1164
|
+
const message = count > 0
|
|
1165
|
+
? `Session complete — ${count} file event${count === 1 ? '' : 's'} during session${qualifier}.`
|
|
1166
|
+
: 'Session complete — no file events.';
|
|
1167
|
+
syncSpinner.succeed(message);
|
|
1168
|
+
syncSpinner = undefined;
|
|
1169
|
+
return exitCode;
|
|
1131
1170
|
}
|
|
1132
1171
|
catch (err) {
|
|
1133
|
-
// If the spinner is still live when we error out, mark it failed so
|
|
1134
|
-
// the pending animation doesn't hang around under the error message.
|
|
1135
1172
|
if (syncSpinner) {
|
|
1136
1173
|
syncSpinner.fail('Sync did not complete');
|
|
1137
1174
|
syncSpinner = undefined;
|
|
1138
1175
|
}
|
|
1139
1176
|
// InstallCommandError carries the real install exit code — surfacing
|
|
1140
1177
|
// it (rather than collapsing onto 127) lets callers distinguish a
|
|
1141
|
-
// failed `npx prpm install` from a missing harness binary.
|
|
1178
|
+
// failed `npx prpm install` from a missing harness binary. The message
|
|
1179
|
+
// itself was already shown via spinner.fail inside runInstallWithSpinner,
|
|
1180
|
+
// so we just return the code here.
|
|
1142
1181
|
if (err instanceof InstallCommandError) {
|
|
1143
|
-
process.stderr.write(`${err.message}. Aborting.\n`);
|
|
1144
1182
|
return err.exitCode;
|
|
1145
1183
|
}
|
|
1146
1184
|
const e = err;
|
|
@@ -1152,21 +1190,29 @@ async function runInteractive(selection, options) {
|
|
|
1152
1190
|
return 1;
|
|
1153
1191
|
}
|
|
1154
1192
|
finally {
|
|
1155
|
-
// Defensive: if neither onAfterSync nor the catch branch stopped the
|
|
1156
|
-
// spinner (e.g. unexpected exit path), stop it cleanly here so the
|
|
1157
|
-
// terminal is not left in spinner state.
|
|
1158
1193
|
if (syncSpinner) {
|
|
1159
1194
|
syncSpinner.stop();
|
|
1160
1195
|
syncSpinner = undefined;
|
|
1161
1196
|
}
|
|
1197
|
+
// Best-effort: stop autosync if we errored out before the success path
|
|
1198
|
+
// already cleared it.
|
|
1199
|
+
if (autoSync) {
|
|
1200
|
+
try {
|
|
1201
|
+
await autoSync.stop({ signal: shutdownController.signal });
|
|
1202
|
+
}
|
|
1203
|
+
catch {
|
|
1204
|
+
/* ignore — we're tearing down anyway */
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
handle.cleanup();
|
|
1162
1208
|
await launchMetadata?.stop();
|
|
1163
|
-
process.removeListener('SIGINT',
|
|
1209
|
+
process.removeListener('SIGINT', sigintHandler);
|
|
1164
1210
|
// When the install ran inside the mount, its cleanup paths are
|
|
1165
|
-
// mount-relative (e.g. `.skills/<name>`, `skills/<name>`) and
|
|
1166
|
-
//
|
|
1167
|
-
//
|
|
1168
|
-
//
|
|
1169
|
-
//
|
|
1211
|
+
// mount-relative (e.g. `.skills/<name>`, `skills/<name>`) and running
|
|
1212
|
+
// cleanup here would resolve them against the real repo cwd —
|
|
1213
|
+
// potentially `rm -rf`ing pre-existing user content. The mount dir is
|
|
1214
|
+
// removed wholesale by `removeSessionRoot` below, so the install's
|
|
1215
|
+
// cleanup is redundant anyway in that case.
|
|
1170
1216
|
if (!deferInstallToMount) {
|
|
1171
1217
|
runCleanup(install.cleanupCommand, install.cleanupCommandString);
|
|
1172
1218
|
}
|
|
@@ -1510,7 +1556,8 @@ function collectPersonaRows() {
|
|
|
1510
1556
|
intent: spec.intent,
|
|
1511
1557
|
tags: spec.tags,
|
|
1512
1558
|
description: spec.description,
|
|
1513
|
-
rating: tier
|
|
1559
|
+
rating: tier,
|
|
1560
|
+
defaultTier: spec.defaultTier
|
|
1514
1561
|
});
|
|
1515
1562
|
}
|
|
1516
1563
|
};
|
|
@@ -1659,7 +1706,7 @@ function runList(args) {
|
|
|
1659
1706
|
return false;
|
|
1660
1707
|
if (applyRecommended) {
|
|
1661
1708
|
const rule = recommendedByIntent[r.intent];
|
|
1662
|
-
if (r.rating !== (rule?.tier ?? 'best-value'))
|
|
1709
|
+
if (r.rating !== (rule?.tier ?? r.defaultTier ?? 'best-value'))
|
|
1663
1710
|
return false;
|
|
1664
1711
|
}
|
|
1665
1712
|
return true;
|
|
@@ -1753,7 +1800,7 @@ function resolveShowTarget(selector, all) {
|
|
|
1753
1800
|
}
|
|
1754
1801
|
else {
|
|
1755
1802
|
const rule = routingProfiles.default.intents[spec.intent];
|
|
1756
|
-
tiers = [rule?.tier ?? 'best-value'];
|
|
1803
|
+
tiers = [rule?.tier ?? spec.defaultTier ?? 'best-value'];
|
|
1757
1804
|
}
|
|
1758
1805
|
return { spec, source, tiers, explicitTier };
|
|
1759
1806
|
}
|
|
@@ -1770,6 +1817,9 @@ function formatPersonaShow(spec, source, tiers, tierNote) {
|
|
|
1770
1817
|
lines.push(`INTENT ${spec.intent}`);
|
|
1771
1818
|
lines.push(`TAGS ${spec.tags.length ? spec.tags.join(', ') : '(none)'}`);
|
|
1772
1819
|
lines.push(`DESCRIPTION ${spec.description}`);
|
|
1820
|
+
if (spec.defaultTier) {
|
|
1821
|
+
lines.push(`DEFAULT TIER ${spec.defaultTier}`);
|
|
1822
|
+
}
|
|
1773
1823
|
lines.push(`TIERS SHOWN ${tiers.join(', ')}${tierNote ? ` (${tierNote})` : ''}`);
|
|
1774
1824
|
lines.push('');
|
|
1775
1825
|
lines.push('SKILLS');
|