fv-skills-baif 2.3.2 → 2.3.5
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 +52 -0
- package/README.md +64 -14
- package/agents/fvs-crypto-executor.md +8 -13
- package/agents/fvs-crypto-thinker.md +29 -21
- package/agents/fvs-doc-syncer.md +37 -27
- package/agents/fvs-external-modeler.md +124 -0
- package/bin/install.js +482 -65
- package/commands/fvs/aeneas-extract.md +21 -23
- package/commands/fvs/aeneas.md +2 -1
- package/commands/fvs/configure.md +174 -0
- package/commands/fvs/crypto-eval.md +57 -29
- package/commands/fvs/crypto-execute.md +19 -30
- package/commands/fvs/crypto-followup.md +27 -16
- package/commands/fvs/crypto-plan.md +34 -21
- package/commands/fvs/crypto-review.md +44 -23
- package/commands/fvs/fc-plan.md +10 -5
- package/commands/fvs/fc.md +2 -1
- package/commands/fvs/help.md +57 -25
- package/commands/fvs/lean-formalise.md +10 -15
- package/commands/fvs/lean-refactor.md +10 -15
- package/commands/fvs/lean-spec-review.md +26 -5
- package/commands/fvs/lean-specify.md +10 -15
- package/commands/fvs/lean-verify.md +10 -15
- package/commands/fvs/manage.md +3 -2
- package/commands/fvs/map-code.md +10 -19
- package/commands/fvs/model-external.md +48 -0
- package/commands/fvs/natural-language.md +12 -2
- package/commands/fvs/reapply-patches.md +28 -21
- package/commands/fvs/sync-aeneas-verif.md +71 -47
- package/commands/fvs/trust-audit.md +11 -6
- package/fv-skills/VERSION +1 -1
- package/fv-skills/references/aeneas-patterns.md +27 -20
- package/fv-skills/references/blocker-catalog.md +23 -23
- package/fv-skills/references/crypto-plan-review.md +10 -3
- package/fv-skills/references/external-modeling.md +127 -0
- package/fv-skills/references/fc-spec-review.md +4 -2
- package/fv-skills/references/model-profiles.md +293 -188
- package/fv-skills/references/proof-strategies.md +26 -0
- package/fv-skills/references/review-diagnostics.md +10 -8
- package/fv-skills/references/tactic-usage.md +38 -14
- package/fv-skills/templates/config.json +29 -4
- package/fv-skills/upstream/aeneas/_sync-meta.json +246 -10
- package/fv-skills/upstream/aeneas/aeneas-lean-core.instructions.md +97 -1114
- package/fv-skills/upstream/aeneas/aeneas-tactics-quickref.instructions.md +31 -164
- package/fv-skills/upstream/aeneas/extraction/documentation/emit-json.md +68 -0
- package/fv-skills/upstream/aeneas/extraction/documentation/skills/verification-campaigns.instructions.md +86 -0
- package/fv-skills/upstream/aeneas/launching-proof-agents.instructions.md +45 -6
- package/fv-skills/workflows/aeneas-extract.md +13 -5
- package/fv-skills/workflows/crypto-eval.md +39 -16
- package/fv-skills/workflows/crypto-execute.md +11 -5
- package/fv-skills/workflows/crypto-followup.md +10 -4
- package/fv-skills/workflows/crypto-plan.md +9 -4
- package/fv-skills/workflows/crypto-review.md +34 -14
- package/fv-skills/workflows/fc-plan.md +4 -5
- package/fv-skills/workflows/lean-formalise.md +4 -13
- package/fv-skills/workflows/lean-refactor.md +7 -13
- package/fv-skills/workflows/lean-spec-review.md +66 -42
- package/fv-skills/workflows/lean-specify.md +4 -13
- package/fv-skills/workflows/lean-verify.md +4 -13
- package/fv-skills/workflows/map-code.md +4 -14
- package/fv-skills/workflows/model-external.md +147 -0
- package/fv-skills/workflows/sync-aeneas-verif.md +63 -38
- package/fv-skills/workflows/trust-audit.md +13 -4
- package/package.json +11 -3
- package/pi/skills/fvs-aeneas/SKILL.md +24 -0
- package/pi/skills/fvs-aeneas-extract/SKILL.md +224 -0
- package/pi/skills/fvs-checkpoint/SKILL.md +154 -0
- package/pi/skills/fvs-configure/SKILL.md +175 -0
- package/pi/skills/fvs-context/SKILL.md +22 -0
- package/pi/skills/fvs-crypto-eval/SKILL.md +208 -0
- package/pi/skills/fvs-crypto-execute/SKILL.md +221 -0
- package/pi/skills/fvs-crypto-followup/SKILL.md +275 -0
- package/pi/skills/fvs-crypto-plan/SKILL.md +325 -0
- package/pi/skills/fvs-crypto-review/SKILL.md +162 -0
- package/pi/skills/fvs-fc/SKILL.md +32 -0
- package/pi/skills/fvs-fc-plan/SKILL.md +289 -0
- package/pi/skills/fvs-formalise/SKILL.md +37 -0
- package/pi/skills/fvs-help/SKILL.md +478 -0
- package/pi/skills/fvs-kb-setup/SKILL.md +322 -0
- package/pi/skills/fvs-lean-formalise/SKILL.md +397 -0
- package/pi/skills/fvs-lean-refactor/SKILL.md +293 -0
- package/pi/skills/fvs-lean-spec-review/SKILL.md +61 -0
- package/pi/skills/fvs-lean-specify/SKILL.md +424 -0
- package/pi/skills/fvs-lean-verify/SKILL.md +461 -0
- package/pi/skills/fvs-manage/SKILL.md +29 -0
- package/pi/skills/fvs-map-code/SKILL.md +349 -0
- package/pi/skills/fvs-model-external/SKILL.md +47 -0
- package/pi/skills/fvs-natural-language/SKILL.md +220 -0
- package/pi/skills/fvs-pause-work/SKILL.md +154 -0
- package/pi/skills/fvs-reapply-patches/SKILL.md +27 -0
- package/pi/skills/fvs-resume-work/SKILL.md +96 -0
- package/pi/skills/fvs-sync-aeneas-verif/SKILL.md +260 -0
- package/pi/skills/fvs-trust-audit/SKILL.md +234 -0
- package/pi/skills/fvs-update/SKILL.md +24 -0
- package/scripts/build-plugin.cjs +130 -6
- package/scripts/fvs-codex-think.mjs +128 -54
- package/scripts/fvs-model-external.mjs +540 -0
- package/scripts/fvs-model-review.mjs +231 -0
- package/scripts/fvs-spec-review.mjs +139 -41
package/bin/install.js
CHANGED
|
@@ -5,6 +5,7 @@ const path = require('path');
|
|
|
5
5
|
const os = require('os');
|
|
6
6
|
const crypto = require('crypto');
|
|
7
7
|
const readline = require('readline');
|
|
8
|
+
const { spawnSync } = require('child_process');
|
|
8
9
|
|
|
9
10
|
// Colors
|
|
10
11
|
const orange = '\x1b[38;5;208m';
|
|
@@ -67,46 +68,152 @@ const CODEX_AGENT_SANDBOX = {
|
|
|
67
68
|
// completes proofs, so it needs workspace-write; the unmapped default is
|
|
68
69
|
// read-only, which would silently fail every write on Codex.
|
|
69
70
|
'fvs-crypto-executor': 'workspace-write',
|
|
71
|
+
// External modeling owns exact hand-written model/specification targets inside
|
|
72
|
+
// a parent-managed reversible transaction.
|
|
73
|
+
'fvs-external-modeler': 'workspace-write',
|
|
70
74
|
'fvs-axiom-auditor': 'read-only',
|
|
71
75
|
};
|
|
72
76
|
|
|
73
|
-
// Codex
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
77
|
+
// Codex agent TOMLs cannot express FVS's stage-aware quality matrix: shared
|
|
78
|
+
// agents serve authority, work, and scout stages. These are safe installation
|
|
79
|
+
// fallbacks only. The command-level selection manifest is authoritative and
|
|
80
|
+
// passes a stage effort dynamically when spawn_agent supports it; otherwise it
|
|
81
|
+
// must surface the limitation instead of claiming the requested stage setting.
|
|
82
|
+
// Codex accepts minimal|low|medium|high|xhigh in agent TOML; any unmapped agent
|
|
83
|
+
// defaults to high at the lookup site.
|
|
80
84
|
const FVS_CODEX_AGENT_EFFORT = {
|
|
81
85
|
'fvs-executor': 'xhigh',
|
|
82
86
|
'fvs-lean-refactorer': 'xhigh',
|
|
83
|
-
'fvs-explainer': '
|
|
87
|
+
'fvs-explainer': 'high',
|
|
84
88
|
'fvs-researcher': 'high',
|
|
85
|
-
// Extraction
|
|
86
|
-
|
|
87
|
-
// meaning-bearing change preserves behaviour) and the bisector (minimizing a
|
|
88
|
-
// novel blocker to a faithful failing example) — run at xhigh; the remaining
|
|
89
|
-
// workers run at high.
|
|
90
|
-
'fvs-equivalence-assessor': 'xhigh',
|
|
89
|
+
// Extraction fallbacks follow the confirmed work/scout split.
|
|
90
|
+
'fvs-equivalence-assessor': 'high',
|
|
91
91
|
'fvs-extract-bisector': 'xhigh',
|
|
92
92
|
'fvs-extract-classifier': 'high',
|
|
93
|
-
'fvs-extract-applier': '
|
|
93
|
+
'fvs-extract-applier': 'xhigh',
|
|
94
94
|
'fvs-draft-investigator': 'high',
|
|
95
95
|
'fvs-doc-syncer': 'high',
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
// hard); the read-only auditor introspects axioms at high.
|
|
96
|
+
// The thinker spans authority plan/follow-up and scout eval stages, so xhigh
|
|
97
|
+
// is the strongest safe static fallback; the manifest supplies max/high when
|
|
98
|
+
// dynamic effort is available. The executor is a work stage.
|
|
100
99
|
'fvs-crypto-thinker': 'xhigh',
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
'fvs-crypto-executor': 'high',
|
|
100
|
+
'fvs-crypto-executor': 'xhigh',
|
|
101
|
+
'fvs-external-modeler': 'xhigh',
|
|
104
102
|
'fvs-axiom-auditor': 'high',
|
|
105
103
|
};
|
|
106
104
|
|
|
107
105
|
// Get version from package.json
|
|
108
106
|
const pkg = require('../package.json');
|
|
109
107
|
|
|
108
|
+
const PI_PACKAGE_NAME = 'fv-skills-baif';
|
|
109
|
+
const ALL_RUNTIMES = ['pi', 'claude', 'opencode', 'gemini', 'codex'];
|
|
110
|
+
|
|
111
|
+
function buildPiPackageSource(version = 'latest') {
|
|
112
|
+
if (version === 'latest') return `npm:${PI_PACKAGE_NAME}`;
|
|
113
|
+
if (!/^(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)$/.test(version ?? '')) {
|
|
114
|
+
throw new Error('--pi-version must be latest or X.Y.Z');
|
|
115
|
+
}
|
|
116
|
+
return `npm:${PI_PACKAGE_NAME}@${version}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function buildPiCommand(action, version = 'latest', isGlobal = true) {
|
|
120
|
+
if (!['install', 'remove'].includes(action)) throw new Error('Pi action must be install or remove');
|
|
121
|
+
const source = action === 'install' ? buildPiPackageSource(version) : `npm:${PI_PACKAGE_NAME}`;
|
|
122
|
+
return [action, source, ...(isGlobal ? [] : ['--local'])];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function piPackageSource(entry) {
|
|
126
|
+
if (typeof entry === 'string') return entry;
|
|
127
|
+
if (!entry || typeof entry !== 'object') return null;
|
|
128
|
+
return entry.source ?? entry.package ?? entry.spec ?? null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function isFvsPiSource(source) {
|
|
132
|
+
return typeof source === 'string' &&
|
|
133
|
+
new RegExp(`^npm:${PI_PACKAGE_NAME.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?:@[^\\s]+)?$`).test(source);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function readPiPackageSources(file) {
|
|
137
|
+
if (!fs.existsSync(file)) return [];
|
|
138
|
+
let settings;
|
|
139
|
+
try {
|
|
140
|
+
settings = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
141
|
+
} catch (error) {
|
|
142
|
+
throw new Error(`Cannot inspect Pi settings ${file}: ${error.message}`);
|
|
143
|
+
}
|
|
144
|
+
if (settings.packages !== undefined && !Array.isArray(settings.packages)) {
|
|
145
|
+
throw new Error(`Cannot inspect Pi settings ${file}: packages must be an array`);
|
|
146
|
+
}
|
|
147
|
+
return (settings.packages ?? []).map(piPackageSource).filter(isFvsPiSource);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function detectPiPackageScopes({ home = os.homedir(), cwd = process.cwd() } = {}) {
|
|
151
|
+
return {
|
|
152
|
+
user: readPiPackageSources(path.join(home, '.pi', 'agent', 'settings.json')),
|
|
153
|
+
project: readPiPackageSources(path.join(cwd, '.pi', 'settings.json')),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function createPiAdapter({ runner = spawnSync, home = os.homedir(), cwd = process.cwd(),
|
|
158
|
+
env = process.env } = {}) {
|
|
159
|
+
const invoke = (argv, options = {}) => {
|
|
160
|
+
const result = runner('pi', argv, { cwd, env, encoding: 'utf8', ...options });
|
|
161
|
+
if (result.error?.code === 'ENOENT') {
|
|
162
|
+
throw new Error('Pi executable is unavailable. Install or upgrade Pi, then retry.');
|
|
163
|
+
}
|
|
164
|
+
if (result.error || result.status !== 0) {
|
|
165
|
+
const detail = result.error?.message || result.stderr?.trim() ||
|
|
166
|
+
`exit ${result.status ?? 'unknown'}${result.signal ? ` (${result.signal})` : ''}`;
|
|
167
|
+
throw new Error(`Pi ${argv.join(' ')} failed: ${detail}`);
|
|
168
|
+
}
|
|
169
|
+
return result;
|
|
170
|
+
};
|
|
171
|
+
return {
|
|
172
|
+
preflight({ isGlobal = true } = {}) {
|
|
173
|
+
invoke(['--version']);
|
|
174
|
+
for (const action of ['install', 'remove']) {
|
|
175
|
+
const result = invoke([action, '--help']);
|
|
176
|
+
if (!isGlobal && !/--local\b/.test(`${result.stdout ?? ''}\n${result.stderr ?? ''}`)) {
|
|
177
|
+
throw new Error(`Pi ${action} does not support --local. Upgrade Pi, then retry.`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
scopes() {
|
|
182
|
+
return detectPiPackageScopes({ home, cwd });
|
|
183
|
+
},
|
|
184
|
+
run(argv) {
|
|
185
|
+
return invoke(argv, { stdio: 'inherit', encoding: undefined });
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function optionValue(inputArgs, name) {
|
|
191
|
+
const matches = [];
|
|
192
|
+
for (let i = 0; i < inputArgs.length; i += 1) {
|
|
193
|
+
if (inputArgs[i] === name) {
|
|
194
|
+
if (!inputArgs[i + 1] || inputArgs[i + 1].startsWith('-')) {
|
|
195
|
+
throw new Error(`${name} requires a value`);
|
|
196
|
+
}
|
|
197
|
+
matches.push(inputArgs[i + 1]);
|
|
198
|
+
i += 1;
|
|
199
|
+
} else if (inputArgs[i].startsWith(`${name}=`)) {
|
|
200
|
+
matches.push(inputArgs[i].slice(name.length + 1));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (matches.length > 1) throw new Error(`${name} may be specified only once`);
|
|
204
|
+
return matches[0] ?? null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function parsePiOptions(inputArgs) {
|
|
208
|
+
const version = optionValue(inputArgs, '--pi-version');
|
|
209
|
+
if (version !== null) buildPiPackageSource(version);
|
|
210
|
+
const conflict = optionValue(inputArgs, '--pi-conflict');
|
|
211
|
+
if (conflict !== null && !['keep', 'move'].includes(conflict)) {
|
|
212
|
+
throw new Error('--pi-conflict must be keep or move');
|
|
213
|
+
}
|
|
214
|
+
return { version, conflict };
|
|
215
|
+
}
|
|
216
|
+
|
|
110
217
|
// Parse args
|
|
111
218
|
const args = process.argv.slice(2);
|
|
112
219
|
const hasGlobal = args.includes('--global') || args.includes('-g');
|
|
@@ -115,14 +222,16 @@ const hasOpencode = args.includes('--opencode');
|
|
|
115
222
|
const hasClaude = args.includes('--claude');
|
|
116
223
|
const hasGemini = args.includes('--gemini');
|
|
117
224
|
const hasCodex = args.includes('--codex');
|
|
225
|
+
const hasPi = args.includes('--pi');
|
|
118
226
|
const hasAll = args.includes('--all');
|
|
119
227
|
const hasUninstall = args.includes('--uninstall') || args.includes('-u');
|
|
120
228
|
|
|
121
229
|
// Runtime selection - can be set by flags or interactive prompt
|
|
122
230
|
let selectedRuntimes = [];
|
|
123
231
|
if (hasAll) {
|
|
124
|
-
selectedRuntimes = [
|
|
232
|
+
selectedRuntimes = [...ALL_RUNTIMES];
|
|
125
233
|
} else {
|
|
234
|
+
if (hasPi) selectedRuntimes.push('pi');
|
|
126
235
|
if (hasOpencode) selectedRuntimes.push('opencode');
|
|
127
236
|
if (hasClaude) selectedRuntimes.push('claude');
|
|
128
237
|
if (hasGemini) selectedRuntimes.push('gemini');
|
|
@@ -131,6 +240,7 @@ if (hasAll) {
|
|
|
131
240
|
|
|
132
241
|
// Helper to get directory name for a runtime (used for local/project installs)
|
|
133
242
|
function getDirName(runtime) {
|
|
243
|
+
if (runtime === 'pi') return '.pi';
|
|
134
244
|
if (runtime === 'opencode') return '.opencode';
|
|
135
245
|
if (runtime === 'gemini') return '.gemini';
|
|
136
246
|
if (runtime === 'codex') return '.codex';
|
|
@@ -205,6 +315,11 @@ function getOpencodeGlobalDir() {
|
|
|
205
315
|
* @param {string|null} explicitDir - Explicit directory from --config-dir flag
|
|
206
316
|
*/
|
|
207
317
|
function getGlobalDir(runtime, explicitDir = null) {
|
|
318
|
+
if (runtime === 'pi') {
|
|
319
|
+
// Pi owns this path; --config-dir never redirects its package settings.
|
|
320
|
+
return path.join(os.homedir(), '.pi', 'agent');
|
|
321
|
+
}
|
|
322
|
+
|
|
208
323
|
if (runtime === 'codex') {
|
|
209
324
|
// Codex: --config-dir > CODEX_HOME > ~/.codex
|
|
210
325
|
if (explicitDir) {
|
|
@@ -290,7 +405,7 @@ if (require.main === module) {
|
|
|
290
405
|
|
|
291
406
|
// Show help if requested
|
|
292
407
|
if (require.main === module && hasHelp) {
|
|
293
|
-
console.log(` ${yellow}Usage:${reset} npx fv-skills-baif [options]\n\n ${yellow}Options:${reset}\n ${orange}-g, --global${reset} Install globally (to config directory)\n ${orange}-l, --local${reset} Install locally (to current directory)\n ${orange}--claude${reset} Install for Claude Code only\n ${orange}--opencode${reset} Install for OpenCode only\n ${orange}--gemini${reset} Install for Gemini only\n ${orange}--codex${reset} Install for Codex only\n ${orange}--all${reset} Install for all runtimes\n ${orange}-u, --uninstall${reset} Uninstall FVS (remove all FVS files)\n ${orange}-c, --config-dir <path>${reset} Specify custom config directory\n ${orange}-h, --help${reset} Show this help message\n ${orange}--force-statusline${reset} Replace existing statusline config\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx fv-skills-baif\n\n ${dim}# Install for Claude Code globally${reset}\n npx fv-skills-baif --claude --global\n\n ${dim}# Install for Codex globally${reset}\n npx fv-skills-baif --codex --global\n\n ${dim}# Install for Gemini globally${reset}\n npx fv-skills-baif --gemini --global\n\n ${dim}# Install for all runtimes globally${reset}\n npx fv-skills-baif --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx fv-skills-baif --claude --global --config-dir ~/.claude-bc\n\n ${dim}# Install to current project only${reset}\n npx fv-skills-baif --claude --local\n\n ${dim}# Uninstall FVS from Claude Code globally${reset}\n npx fv-skills-baif --claude --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / GEMINI_CONFIG_DIR / CODEX_HOME environment variables.\n`);
|
|
408
|
+
console.log(` ${yellow}Usage:${reset} npx fv-skills-baif [options]\n\n ${yellow}Options:${reset}\n ${orange}-g, --global${reset} Install globally (to config directory)\n ${orange}-l, --local${reset} Install locally (to current directory)\n ${orange}--claude${reset} Install for Claude Code only\n ${orange}--opencode${reset} Install for OpenCode only\n ${orange}--gemini${reset} Install for Gemini only\n ${orange}--codex${reset} Install for Codex only\n ${orange}--pi${reset} Install as a native Pi package only\n ${orange}--all${reset} Install for all runtimes, including Pi\n ${orange}--pi-version <latest|X.Y.Z>${reset} Select updateable or pinned Pi package\n ${orange}--pi-conflict <keep|move>${reset} Resolve an opposite-scope Pi install noninteractively\n ${orange}-u, --uninstall${reset} Uninstall FVS (remove all FVS files)\n ${orange}-c, --config-dir <path>${reset} Specify custom config directory\n ${orange}-h, --help${reset} Show this help message\n ${orange}--force-statusline${reset} Replace existing statusline config\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx fv-skills-baif\n\n ${dim}# Install for Claude Code globally${reset}\n npx fv-skills-baif --claude --global\n\n ${dim}# Install for Codex globally${reset}\n npx fv-skills-baif --codex --global\n\n ${dim}# Install the updateable Pi package globally${reset}\n npx fv-skills-baif --pi --global\n\n ${dim}# Pin an exact Pi package in this project${reset}\n npx fv-skills-baif --pi --local --pi-version 2.3.4\n\n ${dim}# Install for Gemini globally${reset}\n npx fv-skills-baif --gemini --global\n\n ${dim}# Install for all runtimes globally${reset}\n npx fv-skills-baif --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx fv-skills-baif --claude --global --config-dir ~/.claude-bc\n\n ${dim}# Install to current project only${reset}\n npx fv-skills-baif --claude --local\n\n ${dim}# Uninstall FVS from Claude Code globally${reset}\n npx fv-skills-baif --claude --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / GEMINI_CONFIG_DIR / CODEX_HOME environment variables.\n Pi uses native fixed user/project settings, so --config-dir is reported and ignored for Pi.\n`);
|
|
294
409
|
process.exit(0);
|
|
295
410
|
}
|
|
296
411
|
|
|
@@ -754,6 +869,11 @@ function convertClaudeToCodexMarkdown(content) {
|
|
|
754
869
|
function getCodexSkillAdapterHeader(skillName, options = {}) {
|
|
755
870
|
const pluginName = options.pluginName || null;
|
|
756
871
|
const invocation = pluginName ? `$${pluginName}:${skillName}` : `$${skillName}`;
|
|
872
|
+
const dispatchExamples = skillName === 'model-external'
|
|
873
|
+
? '`fvs-external-modeler`'
|
|
874
|
+
: /(?:^|-)crypto-/.test(skillName)
|
|
875
|
+
? '`fvs-crypto-thinker`, `fvs-crypto-executor`'
|
|
876
|
+
: '`fvs-researcher`, `fvs-executor`';
|
|
757
877
|
const pluginCompatibility = pluginName
|
|
758
878
|
? `\n## D. Shared Plugin Syntax\n- This file is shared with Claude Code. On Codex, interpret \`/${pluginName}:<name>\` references as \`$${pluginName}:<name>\`.\n- Treat \`$ARGUMENTS\` in the shared body as \`{{FVS_ARGS}}\`.\n- \`\${CLAUDE_PLUGIN_ROOT}\` is the installed plugin root. If a host leaves that token unexpanded, resolve the plugin root as two directories above this SKILL.md.\n`
|
|
759
879
|
: '';
|
|
@@ -762,7 +882,7 @@ function getCodexSkillAdapterHeader(skillName, options = {}) {
|
|
|
762
882
|
: '';
|
|
763
883
|
const typedModelNote = pluginName
|
|
764
884
|
? 'The marketplace plugin does not install Codex agent TOML. Use this mapping only when the exact FVS agent type is registered independently; otherwise use the bundled-agent workaround.'
|
|
765
|
-
: '
|
|
885
|
+
: 'Installed agent TOML supplies only a fallback effort; it does not prove that the command-level stage selection was applied.';
|
|
766
886
|
const fallbackSteps = pluginName
|
|
767
887
|
? `1. Read \`\${CLAUDE_PLUGIN_ROOT}/agents/<agent-name>.md\` and extract its instructions. If the token is still literal, resolve the path from this SKILL.md as described above.\n2. Spawn a generic/default agent and inject those instructions as a role preamble before the task prompt.\n3. Label results clearly as \"generic-agent workaround\" so the user knows typed guarantees are not in effect.\n4. Where typed dispatch is mandatory for correctness, fail closed and report the schema limitation rather than silently degrading.`
|
|
768
888
|
: `1. Resolve your active Codex config root (the directory containing your \`config.toml\`), then read \`agents/<agent-name>.toml\` relative to that root to extract the agent's instructions.\n2. Inject those instructions as a role-preamble into a generic \`spawn_agent(message=...)\` call.\n3. Label results clearly as \"generic-agent workaround\" so the user knows typed guarantees are not in effect.\n4. Where typed dispatch is mandatory for correctness, fail closed and report the schema limitation rather than silently degrading.`;
|
|
@@ -807,13 +927,19 @@ FVS workflows use \`Task(...)\` (Claude Code syntax). Translate to Codex collabo
|
|
|
807
927
|
|
|
808
928
|
Before spawning, inspect the \`spawn_agent\` tool's visible parameter schema to determine which form is active.${typedDispatchQualification}
|
|
809
929
|
|
|
930
|
+
Selection-capability gate (before manifest confirmation):
|
|
931
|
+
- Compare the requested model with the exact active/inherited Codex model. Because \`spawn_agent\` has no inline model field, any different requested model is unresolved. Rebuild the manifest around the actual active model and ask explicitly, choose a capable external runner, or fail before dispatch in noninteractive mode.
|
|
932
|
+
- When \`reasoning_effort\` is absent from the schema, compare the requested effort with the installed/runtime default. A mismatch is unresolved and follows the same rebuild/ask-or-fail rule.
|
|
933
|
+
- Never confirm a requested model or effort and then omit it with a warning. ${typedModelNote}
|
|
934
|
+
|
|
810
935
|
Typed mapping (agent_type-capable schema only):
|
|
811
936
|
- \`Task(subagent_type="X", prompt="Y")\` -> \`spawn_agent(agent_type="X", message="Y")\`
|
|
812
|
-
- \`Task(model="...")\` -> omit
|
|
937
|
+
- \`Task(model="...")\` -> omit only after the selection-capability gate proves it equals the active/inherited model.
|
|
938
|
+
- \`Task(reasoning_effort="...")\` -> \`spawn_agent(reasoning_effort="...")\` when that field is present. If absent, dispatch only after the gate proves the installed/runtime default equals the confirmed effort.
|
|
813
939
|
- \`fork_context: false\` by default -- FVS agents load their own context via \`<files_to_read>\` blocks.
|
|
814
940
|
|
|
815
941
|
Generic-agent workaround (schema with NO agent_type field):
|
|
816
|
-
When only the generic schema is available, typed FVS agent dispatch (
|
|
942
|
+
When only the generic schema is available, typed FVS agent dispatch (${dispatchExamples}, etc.) is NOT possible. This workaround is NOT equivalent to typed execution — FVS agents carry verification-aware prompts and sandbox settings a generic subagent lacks. Fallback:
|
|
817
943
|
${fallbackSteps}
|
|
818
944
|
|
|
819
945
|
Parallel fan-out:
|
|
@@ -2639,11 +2765,14 @@ const MANIFEST_NAME = 'fvs-file-manifest.json';
|
|
|
2639
2765
|
/**
|
|
2640
2766
|
* Compute SHA256 hash of file contents
|
|
2641
2767
|
*/
|
|
2642
|
-
function
|
|
2643
|
-
const content = fs.readFileSync(filePath);
|
|
2768
|
+
function contentHash(content) {
|
|
2644
2769
|
return crypto.createHash('sha256').update(content).digest('hex');
|
|
2645
2770
|
}
|
|
2646
2771
|
|
|
2772
|
+
function fileHash(filePath) {
|
|
2773
|
+
return contentHash(fs.readFileSync(filePath));
|
|
2774
|
+
}
|
|
2775
|
+
|
|
2647
2776
|
/**
|
|
2648
2777
|
* Recursively collect all files in dir with their hashes
|
|
2649
2778
|
*/
|
|
@@ -2710,12 +2839,25 @@ function writeManifest(configDir, runtime = 'claude') {
|
|
|
2710
2839
|
return manifest;
|
|
2711
2840
|
}
|
|
2712
2841
|
|
|
2842
|
+
function matchesGeneratedCodexAgentToml(configDir, rel, digest) {
|
|
2843
|
+
if (!/^agents\/fvs-.*\.toml$/.test(rel)) return false;
|
|
2844
|
+
const markdownPath = path.join(configDir, rel.replace(/\.toml$/, '.md'));
|
|
2845
|
+
if (!fs.existsSync(markdownPath)) return false;
|
|
2846
|
+
const converted = fs.readFileSync(markdownPath, 'utf8');
|
|
2847
|
+
const { frontmatter, body } = extractFrontmatterAndBody(converted);
|
|
2848
|
+
if (!frontmatter || !/<codex_agent_role>/.test(body)) return false;
|
|
2849
|
+
const originalBody = body.replace(/^\s*<codex_agent_role>[\s\S]*?<\/codex_agent_role>\s*/, '');
|
|
2850
|
+
const reconstructed = `---\n${frontmatter}\n---\n\n${originalBody}`;
|
|
2851
|
+
const agentName = path.basename(rel, '.toml');
|
|
2852
|
+
return contentHash(generateCodexAgentToml(agentName, reconstructed)) === digest;
|
|
2853
|
+
}
|
|
2713
2854
|
|
|
2714
2855
|
/**
|
|
2715
2856
|
* Detect user-modified FVS files by comparing against install manifest.
|
|
2716
2857
|
* Backs up modified files to fvs-local-patches/ for reapply after update.
|
|
2717
2858
|
*/
|
|
2718
2859
|
function saveLocalPatches(configDir, runtime = 'claude') {
|
|
2860
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
2719
2861
|
configDir = fs.realpathSync(configDir);
|
|
2720
2862
|
const manifestPath = path.join(configDir, MANIFEST_NAME);
|
|
2721
2863
|
// An unreadable baseline must stop the update before any destructive copy.
|
|
@@ -2725,7 +2867,12 @@ function saveLocalPatches(configDir, runtime = 'claude') {
|
|
|
2725
2867
|
throw new Error('Invalid FVS manifest; repair it before updating. Existing files were preserved.');
|
|
2726
2868
|
}
|
|
2727
2869
|
const current = ownedFileHashes(configDir, runtime);
|
|
2728
|
-
const modified = Object.keys(current).filter(rel =>
|
|
2870
|
+
const modified = Object.keys(current).filter(rel => {
|
|
2871
|
+
if (current[rel] === manifest.files[rel]) return false;
|
|
2872
|
+
if (!Object.prototype.hasOwnProperty.call(manifest.files, rel) && runtime === 'codex' &&
|
|
2873
|
+
matchesGeneratedCodexAgentToml(configDir, rel, current[rel])) return false;
|
|
2874
|
+
return true;
|
|
2875
|
+
});
|
|
2729
2876
|
const patchesDir = path.join(configDir, PATCHES_DIR_NAME);
|
|
2730
2877
|
if (fs.lstatSync(patchesDir, { throwIfNoEntry: false })?.isSymbolicLink()) {
|
|
2731
2878
|
throw new Error('Local patch directory is a symlink; refusing to update');
|
|
@@ -2736,15 +2883,22 @@ function saveLocalPatches(configDir, runtime = 'claude') {
|
|
|
2736
2883
|
const addBundle = (directory, meta) => {
|
|
2737
2884
|
const hashes = generateManifest(directory);
|
|
2738
2885
|
delete hashes['backup-meta.json'];
|
|
2739
|
-
|
|
2886
|
+
const listed = new Set(meta?.files ?? []);
|
|
2887
|
+
const pending = new Set(meta?.pending ?? meta?.files ?? Object.keys(hashes));
|
|
2888
|
+
for (const rel of listed) {
|
|
2740
2889
|
if (!Object.prototype.hasOwnProperty.call(hashes, rel)) throw new Error(`Missing backed-up patch: ${rel}; refusing to update`);
|
|
2741
2890
|
if (meta.hashes?.[rel] && meta.hashes[rel] !== hashes[rel]) {
|
|
2742
2891
|
throw new Error(`Backed-up patch changed: ${rel}; inspect it before updating`);
|
|
2743
2892
|
}
|
|
2744
2893
|
}
|
|
2745
2894
|
for (const rel of Object.keys(hashes)) {
|
|
2746
|
-
if (meta && !
|
|
2747
|
-
|
|
2895
|
+
if (meta && !listed.has(rel)) {
|
|
2896
|
+
console.warn(` Unlisted local patch recovered: ${rel}`);
|
|
2897
|
+
pending.add(rel);
|
|
2898
|
+
}
|
|
2899
|
+
if (pending.has(rel)) {
|
|
2900
|
+
sources.set(rel, { file: path.join(directory, rel), kind: meta?.kinds?.[rel] ?? 'legacy' });
|
|
2901
|
+
}
|
|
2748
2902
|
}
|
|
2749
2903
|
};
|
|
2750
2904
|
if (prior?.bundle) {
|
|
@@ -2777,8 +2931,9 @@ function saveLocalPatches(configDir, runtime = 'claude') {
|
|
|
2777
2931
|
fs.mkdirSync(bundles, { recursive: true });
|
|
2778
2932
|
const staging = fs.mkdtempSync(path.join(bundles, '.pending-'));
|
|
2779
2933
|
const bundle = `bundles/bundle-${path.basename(staging).slice(9)}`;
|
|
2780
|
-
const meta = { version:
|
|
2781
|
-
from_version: manifest.version, files: [...sources.keys()].sort(), hashes: {}, kinds: {} };
|
|
2934
|
+
const meta = { version: 3, bundle, backed_up_at: new Date().toISOString(),
|
|
2935
|
+
from_version: manifest.version, files: [...sources.keys()].sort(), pending: [], hashes: {}, kinds: {} };
|
|
2936
|
+
meta.pending = [...meta.files];
|
|
2782
2937
|
for (const rel of meta.files) {
|
|
2783
2938
|
const dest = path.join(staging, rel);
|
|
2784
2939
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
@@ -2808,7 +2963,8 @@ function reportLocalPatches(configDir, runtime = 'claude') {
|
|
|
2808
2963
|
let meta;
|
|
2809
2964
|
try { meta = JSON.parse(fs.readFileSync(metaPath, 'utf8')); } catch { return []; }
|
|
2810
2965
|
|
|
2811
|
-
|
|
2966
|
+
const pending = meta.pending ?? meta.files ?? [];
|
|
2967
|
+
if (pending.length > 0) {
|
|
2812
2968
|
const reapplyCommand = runtime === 'opencode'
|
|
2813
2969
|
? '/fvs-reapply-patches'
|
|
2814
2970
|
: runtime === 'codex'
|
|
@@ -2816,7 +2972,7 @@ function reportLocalPatches(configDir, runtime = 'claude') {
|
|
|
2816
2972
|
: '/fvs:reapply-patches';
|
|
2817
2973
|
console.log('');
|
|
2818
2974
|
console.log(' ' + yellow + 'Local patches detected' + reset + ' (from v' + meta.from_version + '):');
|
|
2819
|
-
for (const f of
|
|
2975
|
+
for (const f of pending) {
|
|
2820
2976
|
console.log(' ' + orange + f + reset);
|
|
2821
2977
|
}
|
|
2822
2978
|
console.log('');
|
|
@@ -2825,7 +2981,7 @@ function reportLocalPatches(configDir, runtime = 'claude') {
|
|
|
2825
2981
|
console.log(' Or manually compare and merge the files.');
|
|
2826
2982
|
console.log('');
|
|
2827
2983
|
}
|
|
2828
|
-
return
|
|
2984
|
+
return pending;
|
|
2829
2985
|
}
|
|
2830
2986
|
|
|
2831
2987
|
/**
|
|
@@ -3335,6 +3491,147 @@ function handleStatusline(settings, isGlobal, isInteractive, callback) {
|
|
|
3335
3491
|
});
|
|
3336
3492
|
}
|
|
3337
3493
|
|
|
3494
|
+
function existingAncestor(target) {
|
|
3495
|
+
let current = path.resolve(target);
|
|
3496
|
+
while (!fs.existsSync(current)) {
|
|
3497
|
+
const parent = path.dirname(current);
|
|
3498
|
+
if (parent === current) break;
|
|
3499
|
+
current = parent;
|
|
3500
|
+
}
|
|
3501
|
+
return current;
|
|
3502
|
+
}
|
|
3503
|
+
|
|
3504
|
+
function preflightNonPiRuntime(runtime, isGlobal) {
|
|
3505
|
+
if (!['claude', 'opencode', 'gemini', 'codex'].includes(runtime)) {
|
|
3506
|
+
throw new Error(`Unsupported runtime: ${runtime}`);
|
|
3507
|
+
}
|
|
3508
|
+
for (const source of ['commands/fvs', 'agents', 'fv-skills']) {
|
|
3509
|
+
if (!fs.existsSync(path.join(__dirname, '..', source))) {
|
|
3510
|
+
throw new Error(`Installer payload is missing ${source}`);
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
const target = isGlobal ? getGlobalDir(runtime, explicitConfigDir) : path.join(process.cwd(), getDirName(runtime));
|
|
3514
|
+
if (fs.existsSync(target) && !fs.statSync(target).isDirectory()) {
|
|
3515
|
+
throw new Error(`Installer target is not a directory: ${target}`);
|
|
3516
|
+
}
|
|
3517
|
+
const ancestor = existingAncestor(target);
|
|
3518
|
+
if (!fs.statSync(ancestor).isDirectory()) {
|
|
3519
|
+
throw new Error(`Installer target parent is not a directory: ${ancestor}`);
|
|
3520
|
+
}
|
|
3521
|
+
fs.accessSync(ancestor, fs.constants.W_OK);
|
|
3522
|
+
}
|
|
3523
|
+
|
|
3524
|
+
function cliFailure(message) {
|
|
3525
|
+
console.error(` ${yellow}${message}${reset}`);
|
|
3526
|
+
process.exitCode = 1;
|
|
3527
|
+
}
|
|
3528
|
+
|
|
3529
|
+
function validatePiFlagUse(runtimes, options, isUninstall = false) {
|
|
3530
|
+
if (!runtimes.includes('pi') && (options.version !== null || options.conflict !== null)) {
|
|
3531
|
+
throw new Error('--pi-version and --pi-conflict require --pi or --all');
|
|
3532
|
+
}
|
|
3533
|
+
if (isUninstall && options.version !== null) {
|
|
3534
|
+
throw new Error('--pi-version applies only to Pi installation');
|
|
3535
|
+
}
|
|
3536
|
+
if (isUninstall && options.conflict !== null) {
|
|
3537
|
+
throw new Error('--pi-conflict applies only to Pi installation');
|
|
3538
|
+
}
|
|
3539
|
+
}
|
|
3540
|
+
|
|
3541
|
+
function validateConfigDirForRuntimes(runtimes, isGlobal) {
|
|
3542
|
+
if (explicitConfigDir && !isGlobal && runtimes.some(runtime => runtime !== 'pi')) {
|
|
3543
|
+
throw new Error('Cannot use --config-dir with --local for non-Pi runtimes');
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3546
|
+
|
|
3547
|
+
function promptSkipUnavailablePi(error, callback) {
|
|
3548
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
3549
|
+
console.log(` ${yellow}Pi preflight failed:${reset} ${error.message}\n\n` +
|
|
3550
|
+
` ${orange}1${reset}) Cancel ${dim}(default)${reset}\n` +
|
|
3551
|
+
` ${orange}2${reset}) Continue without Pi\n`);
|
|
3552
|
+
rl.question(` Choice ${dim}[1]${reset}: `, answer => {
|
|
3553
|
+
rl.close();
|
|
3554
|
+
callback(answer.trim() === '2');
|
|
3555
|
+
});
|
|
3556
|
+
}
|
|
3557
|
+
|
|
3558
|
+
function resolvePiVersionChoice(choice, exactVersion = '') {
|
|
3559
|
+
if (choice.trim() !== '2') return 'latest';
|
|
3560
|
+
buildPiPackageSource(exactVersion.trim());
|
|
3561
|
+
return exactVersion.trim();
|
|
3562
|
+
}
|
|
3563
|
+
|
|
3564
|
+
function resolvePiConflictChoice(choice) {
|
|
3565
|
+
const selected = choice.trim() || '1';
|
|
3566
|
+
return selected === '2' ? 'move' : selected === '3' ? 'cancel' : 'keep';
|
|
3567
|
+
}
|
|
3568
|
+
|
|
3569
|
+
function resolveRuntimeChoice(choice) {
|
|
3570
|
+
return choice === '6' ? [...ALL_RUNTIMES]
|
|
3571
|
+
: choice === '5' ? ['pi']
|
|
3572
|
+
: choice === '4' ? ['codex']
|
|
3573
|
+
: choice === '3' ? ['gemini']
|
|
3574
|
+
: choice === '2' ? ['opencode']
|
|
3575
|
+
: ['claude'];
|
|
3576
|
+
}
|
|
3577
|
+
|
|
3578
|
+
function promptPiVersion(callback) {
|
|
3579
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
3580
|
+
console.log(` ${yellow}Which Pi package version should be installed?${reset}\n\n` +
|
|
3581
|
+
` ${orange}1${reset}) Latest ${dim}(updateable npm source)${reset}\n` +
|
|
3582
|
+
` ${orange}2${reset}) Exact version ${dim}(pinned; skipped by Pi package updates)${reset}\n`);
|
|
3583
|
+
rl.question(` Choice ${dim}[1]${reset}: `, answer => {
|
|
3584
|
+
if (answer.trim() !== '2') {
|
|
3585
|
+
rl.close();
|
|
3586
|
+
callback(resolvePiVersionChoice(answer));
|
|
3587
|
+
return;
|
|
3588
|
+
}
|
|
3589
|
+
rl.question(' Exact version (X.Y.Z): ', version => {
|
|
3590
|
+
rl.close();
|
|
3591
|
+
try {
|
|
3592
|
+
callback(resolvePiVersionChoice(answer, version));
|
|
3593
|
+
} catch (error) {
|
|
3594
|
+
cliFailure(error.message);
|
|
3595
|
+
}
|
|
3596
|
+
});
|
|
3597
|
+
});
|
|
3598
|
+
}
|
|
3599
|
+
|
|
3600
|
+
function promptPiConflict(oppositeScope, callback) {
|
|
3601
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
3602
|
+
console.log(` ${yellow}FVS is already installed in Pi's ${oppositeScope} scope.${reset}\n\n` +
|
|
3603
|
+
` ${orange}1${reset}) Keep both ${dim}(project scope wins when both remain)${reset}\n` +
|
|
3604
|
+
` ${orange}2${reset}) Move to the requested scope\n` +
|
|
3605
|
+
` ${orange}3${reset}) Cancel\n`);
|
|
3606
|
+
rl.question(` Choice ${dim}[1]${reset}: `, answer => {
|
|
3607
|
+
rl.close();
|
|
3608
|
+
callback(resolvePiConflictChoice(answer));
|
|
3609
|
+
});
|
|
3610
|
+
}
|
|
3611
|
+
|
|
3612
|
+
function printPiManifest({ action, isGlobal, version = 'latest', conflict = null }) {
|
|
3613
|
+
const source = action === 'install' ? buildPiPackageSource(version) : `npm:${PI_PACKAGE_NAME}`;
|
|
3614
|
+
console.log(` ${yellow}Pi package plan${reset}\n` +
|
|
3615
|
+
` Action: ${action}\n` +
|
|
3616
|
+
` Scope: ${isGlobal ? 'user/global' : 'project/local'}\n` +
|
|
3617
|
+
` Source: ${source}\n` +
|
|
3618
|
+
` Opposite-scope policy: ${conflict ?? 'not needed'}\n` +
|
|
3619
|
+
` --config-dir: ${explicitConfigDir ? 'ignored for Pi' : 'not set'}\n`);
|
|
3620
|
+
}
|
|
3621
|
+
|
|
3622
|
+
function installPi(adapter, plan) {
|
|
3623
|
+
printPiManifest({ action: 'install', ...plan });
|
|
3624
|
+
adapter.run(buildPiCommand('install', plan.version, plan.isGlobal));
|
|
3625
|
+
if (plan.conflict === 'move') {
|
|
3626
|
+
adapter.run(buildPiCommand('remove', 'latest', !plan.isGlobal));
|
|
3627
|
+
}
|
|
3628
|
+
}
|
|
3629
|
+
|
|
3630
|
+
function uninstallPi(adapter, isGlobal) {
|
|
3631
|
+
printPiManifest({ action: 'remove', isGlobal });
|
|
3632
|
+
adapter.run(buildPiCommand('remove', 'latest', isGlobal));
|
|
3633
|
+
}
|
|
3634
|
+
|
|
3338
3635
|
/**
|
|
3339
3636
|
* Prompt for runtime selection
|
|
3340
3637
|
*/
|
|
@@ -3358,24 +3655,14 @@ function promptRuntime(callback) {
|
|
|
3358
3655
|
${orange}2${reset}) OpenCode ${dim}(~/.config/opencode)${reset} - open source, free models
|
|
3359
3656
|
${orange}3${reset}) Gemini ${dim}(~/.gemini)${reset}
|
|
3360
3657
|
${orange}4${reset}) Codex ${dim}(~/.codex)${reset}
|
|
3361
|
-
${orange}5${reset})
|
|
3658
|
+
${orange}5${reset}) Pi ${dim}(native package manager)${reset}
|
|
3659
|
+
${orange}6${reset}) All
|
|
3362
3660
|
`);
|
|
3363
3661
|
|
|
3364
3662
|
rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
|
|
3365
3663
|
answered = true;
|
|
3366
3664
|
rl.close();
|
|
3367
|
-
|
|
3368
|
-
if (choice === '5') {
|
|
3369
|
-
callback(['claude', 'opencode', 'gemini', 'codex']);
|
|
3370
|
-
} else if (choice === '4') {
|
|
3371
|
-
callback(['codex']);
|
|
3372
|
-
} else if (choice === '3') {
|
|
3373
|
-
callback(['gemini']);
|
|
3374
|
-
} else if (choice === '2') {
|
|
3375
|
-
callback(['opencode']);
|
|
3376
|
-
} else {
|
|
3377
|
-
callback(['claude']);
|
|
3378
|
-
}
|
|
3665
|
+
callback(resolveRuntimeChoice(answer.trim() || '1'));
|
|
3379
3666
|
});
|
|
3380
3667
|
}
|
|
3381
3668
|
|
|
@@ -3385,7 +3672,7 @@ function promptRuntime(callback) {
|
|
|
3385
3672
|
function promptLocation(runtimes) {
|
|
3386
3673
|
if (!process.stdin.isTTY) {
|
|
3387
3674
|
console.log(` ${yellow}Non-interactive terminal detected, defaulting to global install${reset}\n`);
|
|
3388
|
-
|
|
3675
|
+
startInstall(runtimes, true, false);
|
|
3389
3676
|
return;
|
|
3390
3677
|
}
|
|
3391
3678
|
|
|
@@ -3420,21 +3707,128 @@ function promptLocation(runtimes) {
|
|
|
3420
3707
|
rl.close();
|
|
3421
3708
|
const choice = answer.trim() || '1';
|
|
3422
3709
|
const isGlobal = choice !== '2';
|
|
3423
|
-
|
|
3710
|
+
startInstall(runtimes, isGlobal, true);
|
|
3424
3711
|
});
|
|
3425
3712
|
}
|
|
3426
3713
|
|
|
3714
|
+
function startInstall(runtimes, isGlobal, isInteractive) {
|
|
3715
|
+
let options;
|
|
3716
|
+
try {
|
|
3717
|
+
options = parsePiOptions(args);
|
|
3718
|
+
validatePiFlagUse(runtimes, options);
|
|
3719
|
+
validateConfigDirForRuntimes(runtimes, isGlobal);
|
|
3720
|
+
} catch (error) {
|
|
3721
|
+
cliFailure(error.message);
|
|
3722
|
+
return;
|
|
3723
|
+
}
|
|
3724
|
+
|
|
3725
|
+
const nonPi = runtimes.filter(runtime => runtime !== 'pi');
|
|
3726
|
+
const preflightNonPi = () => {
|
|
3727
|
+
for (const runtime of nonPi) preflightNonPiRuntime(runtime, isGlobal);
|
|
3728
|
+
};
|
|
3729
|
+
if (!runtimes.includes('pi')) {
|
|
3730
|
+
try {
|
|
3731
|
+
preflightNonPi();
|
|
3732
|
+
installAllRuntimes(runtimes, isGlobal, isInteractive, null);
|
|
3733
|
+
} catch (error) {
|
|
3734
|
+
cliFailure(error.message);
|
|
3735
|
+
}
|
|
3736
|
+
return;
|
|
3737
|
+
}
|
|
3738
|
+
|
|
3739
|
+
const adapter = createPiAdapter();
|
|
3740
|
+
try {
|
|
3741
|
+
adapter.preflight({ isGlobal });
|
|
3742
|
+
} catch (error) {
|
|
3743
|
+
const isAllSelection = runtimes.length === ALL_RUNTIMES.length &&
|
|
3744
|
+
ALL_RUNTIMES.every(runtime => runtimes.includes(runtime));
|
|
3745
|
+
if (isAllSelection && process.stdin.isTTY) {
|
|
3746
|
+
promptSkipUnavailablePi(error, shouldSkip => {
|
|
3747
|
+
if (!shouldSkip) {
|
|
3748
|
+
console.log(`\n ${yellow}Installation cancelled${reset}\n`);
|
|
3749
|
+
return;
|
|
3750
|
+
}
|
|
3751
|
+
try {
|
|
3752
|
+
preflightNonPi();
|
|
3753
|
+
installAllRuntimes(nonPi, isGlobal, isInteractive, null);
|
|
3754
|
+
} catch (preflightError) {
|
|
3755
|
+
cliFailure(preflightError.message);
|
|
3756
|
+
}
|
|
3757
|
+
});
|
|
3758
|
+
} else {
|
|
3759
|
+
cliFailure(error.message);
|
|
3760
|
+
}
|
|
3761
|
+
return;
|
|
3762
|
+
}
|
|
3763
|
+
|
|
3764
|
+
try {
|
|
3765
|
+
preflightNonPi();
|
|
3766
|
+
} catch (error) {
|
|
3767
|
+
cliFailure(error.message);
|
|
3768
|
+
return;
|
|
3769
|
+
}
|
|
3770
|
+
|
|
3771
|
+
const continueWithVersion = version => {
|
|
3772
|
+
let scopes;
|
|
3773
|
+
try {
|
|
3774
|
+
scopes = adapter.scopes();
|
|
3775
|
+
} catch (error) {
|
|
3776
|
+
cliFailure(error.message);
|
|
3777
|
+
return;
|
|
3778
|
+
}
|
|
3779
|
+
const oppositeScope = isGlobal ? 'project' : 'user';
|
|
3780
|
+
const hasConflict = scopes[oppositeScope].length > 0;
|
|
3781
|
+
const execute = conflict => {
|
|
3782
|
+
if (conflict === 'cancel') {
|
|
3783
|
+
console.log(`\n ${yellow}Installation cancelled${reset}\n`);
|
|
3784
|
+
return;
|
|
3785
|
+
}
|
|
3786
|
+
try {
|
|
3787
|
+
installAllRuntimes(runtimes, isGlobal, isInteractive, {
|
|
3788
|
+
adapter, isGlobal, version, conflict: hasConflict ? conflict : null,
|
|
3789
|
+
});
|
|
3790
|
+
} catch (error) {
|
|
3791
|
+
cliFailure(error.message);
|
|
3792
|
+
}
|
|
3793
|
+
};
|
|
3794
|
+
if (!hasConflict) {
|
|
3795
|
+
execute(null);
|
|
3796
|
+
} else if (options.conflict) {
|
|
3797
|
+
execute(options.conflict);
|
|
3798
|
+
} else if (process.stdin.isTTY) {
|
|
3799
|
+
promptPiConflict(oppositeScope, execute);
|
|
3800
|
+
} else {
|
|
3801
|
+
cliFailure('Opposite-scope Pi install exists; pass --pi-conflict keep|move');
|
|
3802
|
+
}
|
|
3803
|
+
};
|
|
3804
|
+
|
|
3805
|
+
if (options.version) {
|
|
3806
|
+
continueWithVersion(options.version);
|
|
3807
|
+
} else if (process.stdin.isTTY) {
|
|
3808
|
+
promptPiVersion(continueWithVersion);
|
|
3809
|
+
} else {
|
|
3810
|
+
continueWithVersion('latest');
|
|
3811
|
+
}
|
|
3812
|
+
}
|
|
3813
|
+
|
|
3427
3814
|
/**
|
|
3428
3815
|
* Install FVS for all selected runtimes
|
|
3429
3816
|
*/
|
|
3430
|
-
function installAllRuntimes(runtimes, isGlobal, isInteractive) {
|
|
3817
|
+
function installAllRuntimes(runtimes, isGlobal, isInteractive, piPlan = null) {
|
|
3431
3818
|
const results = [];
|
|
3432
3819
|
|
|
3433
|
-
|
|
3820
|
+
if (piPlan) installPi(piPlan.adapter, piPlan);
|
|
3821
|
+
|
|
3822
|
+
for (const runtime of runtimes.filter(runtime => runtime !== 'pi')) {
|
|
3434
3823
|
const result = install(isGlobal, runtime);
|
|
3435
3824
|
results.push(result);
|
|
3436
3825
|
}
|
|
3437
3826
|
|
|
3827
|
+
if (results.length === 0) {
|
|
3828
|
+
console.log(` ${green}Done!${reset} Start a new Pi session and run ${orange}/skill:fvs-help${reset}.\n`);
|
|
3829
|
+
return;
|
|
3830
|
+
}
|
|
3831
|
+
|
|
3438
3832
|
// Handle statusline for Claude & Gemini (OpenCode uses themes)
|
|
3439
3833
|
const claudeResult = results.find(r => r.runtime === 'claude');
|
|
3440
3834
|
const geminiResult = results.find(r => r.runtime === 'gemini');
|
|
@@ -3475,44 +3869,57 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) {
|
|
|
3475
3869
|
}
|
|
3476
3870
|
}
|
|
3477
3871
|
|
|
3872
|
+
function startUninstall(runtimes, isGlobal) {
|
|
3873
|
+
let options;
|
|
3874
|
+
try {
|
|
3875
|
+
options = parsePiOptions(args);
|
|
3876
|
+
validatePiFlagUse(runtimes, options, true);
|
|
3877
|
+
validateConfigDirForRuntimes(runtimes, isGlobal);
|
|
3878
|
+
const adapter = runtimes.includes('pi') ? createPiAdapter() : null;
|
|
3879
|
+
if (adapter) adapter.preflight({ isGlobal });
|
|
3880
|
+
for (const runtime of runtimes.filter(runtime => runtime !== 'pi')) {
|
|
3881
|
+
preflightNonPiRuntime(runtime, isGlobal);
|
|
3882
|
+
}
|
|
3883
|
+
if (adapter) uninstallPi(adapter, isGlobal);
|
|
3884
|
+
for (const runtime of runtimes.filter(runtime => runtime !== 'pi')) uninstall(isGlobal, runtime);
|
|
3885
|
+
} catch (error) {
|
|
3886
|
+
cliFailure(error.message);
|
|
3887
|
+
}
|
|
3888
|
+
}
|
|
3889
|
+
|
|
3478
3890
|
// Main logic
|
|
3479
3891
|
if (require.main === module) {
|
|
3480
3892
|
if (hasGlobal && hasLocal) {
|
|
3481
3893
|
console.error(` ${yellow}Cannot specify both --global and --local${reset}`);
|
|
3482
3894
|
process.exit(1);
|
|
3483
|
-
} else if (explicitConfigDir && hasLocal) {
|
|
3484
|
-
console.error(` ${yellow}Cannot use --config-dir with --local${reset}`);
|
|
3485
|
-
process.exit(1);
|
|
3486
3895
|
} else if (hasUninstall) {
|
|
3487
3896
|
if (!hasGlobal && !hasLocal) {
|
|
3488
3897
|
console.error(` ${yellow}--uninstall requires --global or --local${reset}`);
|
|
3489
3898
|
process.exit(1);
|
|
3490
3899
|
}
|
|
3491
3900
|
const runtimes = selectedRuntimes.length > 0 ? selectedRuntimes : ['claude'];
|
|
3492
|
-
|
|
3493
|
-
uninstall(hasGlobal, runtime);
|
|
3494
|
-
}
|
|
3901
|
+
startUninstall(runtimes, hasGlobal);
|
|
3495
3902
|
} else if (selectedRuntimes.length > 0) {
|
|
3496
3903
|
if (!hasGlobal && !hasLocal) {
|
|
3497
3904
|
promptLocation(selectedRuntimes);
|
|
3498
3905
|
} else {
|
|
3499
|
-
|
|
3906
|
+
startInstall(selectedRuntimes, hasGlobal, false);
|
|
3500
3907
|
}
|
|
3501
3908
|
} else if (hasGlobal || hasLocal) {
|
|
3502
3909
|
// No runtime specified but location is — prompt for runtime
|
|
3503
3910
|
if (!process.stdin.isTTY) {
|
|
3504
3911
|
console.log(` ${yellow}Non-interactive terminal detected, defaulting to Claude Code${reset}\n`);
|
|
3505
|
-
|
|
3912
|
+
startInstall(['claude'], hasGlobal, false);
|
|
3506
3913
|
} else {
|
|
3507
3914
|
promptRuntime((runtimes) => {
|
|
3508
|
-
|
|
3915
|
+
startInstall(runtimes, hasGlobal, true);
|
|
3509
3916
|
});
|
|
3510
3917
|
}
|
|
3511
3918
|
} else {
|
|
3512
3919
|
// Interactive
|
|
3513
3920
|
if (!process.stdin.isTTY) {
|
|
3514
3921
|
console.log(` ${yellow}Non-interactive terminal detected, defaulting to Claude Code global install${reset}\n`);
|
|
3515
|
-
|
|
3922
|
+
startInstall(['claude'], true, false);
|
|
3516
3923
|
} else {
|
|
3517
3924
|
promptRuntime((runtimes) => {
|
|
3518
3925
|
promptLocation(runtimes);
|
|
@@ -3550,6 +3957,16 @@ module.exports = {
|
|
|
3550
3957
|
FVS_CODEX_MARKER,
|
|
3551
3958
|
CODEX_AGENT_SANDBOX,
|
|
3552
3959
|
FVS_CODEX_AGENT_EFFORT,
|
|
3960
|
+
PI_PACKAGE_NAME,
|
|
3961
|
+
ALL_RUNTIMES,
|
|
3962
|
+
buildPiPackageSource,
|
|
3963
|
+
buildPiCommand,
|
|
3964
|
+
detectPiPackageScopes,
|
|
3965
|
+
createPiAdapter,
|
|
3966
|
+
parsePiOptions,
|
|
3967
|
+
resolvePiVersionChoice,
|
|
3968
|
+
resolvePiConflictChoice,
|
|
3969
|
+
resolveRuntimeChoice,
|
|
3553
3970
|
CODEX_HOOKS_FEATURE_KEY,
|
|
3554
3971
|
CODEX_HOOKS_FEATURE_LEGACY_KEYS,
|
|
3555
3972
|
};
|