@haposoft/cafekit 0.18.1 → 0.18.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -1
- package/bin/__tests__/adv-skill.test.js +371 -0
- package/bin/__tests__/agent-authority.test.js +38 -0
- package/bin/__tests__/agent-instructions.test.js +54 -0
- package/bin/__tests__/claude-plugin-build.test.js +151 -0
- package/bin/__tests__/claude-settings.test.js +123 -0
- package/bin/__tests__/codex-ask-guard.test.js +123 -0
- package/bin/__tests__/codex-ask-projection.test.js +51 -0
- package/bin/__tests__/codex-hooks-ownership.test.js +80 -1
- package/bin/__tests__/codex-hooks.test.js +812 -12
- package/bin/__tests__/codex-implicit-policy.test.js +160 -0
- package/bin/__tests__/codex-native-hook-boundaries.test.js +136 -0
- package/bin/__tests__/codex-native.test.js +198 -58
- package/bin/__tests__/codex-plugin-build.test.js +230 -0
- package/bin/__tests__/codex-plugin-context.test.js +135 -0
- package/bin/__tests__/codex-plugin-hooks.test.js +220 -0
- package/bin/__tests__/codex-projection-parity.test.js +314 -0
- package/bin/__tests__/codex-quiet-hooks.test.js +170 -0
- package/bin/__tests__/codex-research-projection.test.js +70 -0
- package/bin/__tests__/codex-runtime-layout.test.js +81 -0
- package/bin/__tests__/codex-setup-agents.test.js +216 -0
- package/bin/__tests__/codex-specs-projection.test.js +76 -0
- package/bin/__tests__/develop-contract.test.js +1 -1
- package/bin/__tests__/docs-sync-on-demand.test.js +25 -20
- package/bin/__tests__/inspect-paths.test.js +183 -0
- package/bin/__tests__/installer-safety.test.js +335 -0
- package/bin/__tests__/legacy-hooks-retired.test.js +81 -21
- package/bin/__tests__/migrate-apply.test.js +373 -0
- package/bin/__tests__/migrate-cli.test.js +152 -0
- package/bin/__tests__/migrate-merged.test.js +269 -0
- package/bin/__tests__/migrate-plan.test.js +271 -0
- package/bin/__tests__/omp-bridge.test.js +147 -17
- package/bin/__tests__/omp-hooks.test.js +35 -5
- package/bin/__tests__/orca-session.test.js +7 -0
- package/bin/__tests__/plugin-safety-keys.test.js +121 -0
- package/bin/__tests__/privacy-paths.test.js +501 -0
- package/bin/__tests__/usage-hook-retired.test.js +29 -9
- package/bin/__tests__/vibe-run.test.js +215 -0
- package/bin/__tests__/vibe-skill.test.js +549 -0
- package/bin/install.js +124 -1
- package/bin/lib/backup.js +43 -24
- package/bin/lib/codex-install.js +141 -12
- package/bin/lib/context.js +6 -1
- package/bin/lib/lock.js +98 -18
- package/bin/lib/managed-writer.js +16 -10
- package/bin/lib/migrate-apply.js +202 -0
- package/bin/lib/migrate-merged.js +342 -0
- package/bin/lib/migrate-plan.js +419 -0
- package/bin/lib/plugin-inventory.js +33 -0
- package/bin/phases/claude-runtime.js +93 -25
- package/bin/phases/claude-settings.js +22 -1
- package/bin/phases/codex-hooks.js +134 -33
- package/bin/phases/codex-runtime.js +110 -85
- package/bin/phases/copy-payload.js +50 -4
- package/bin/phases/omp-runtime.js +4 -26
- package/bin/phases/root-config.js +5 -2
- package/package.json +1 -1
- package/src/claude/CLAUDE.md +5 -1
- package/src/claude/agents/advisor.md +55 -0
- package/src/claude/agents/debugger.md +2 -3
- package/src/claude/agents/deployer.md +2 -1
- package/src/claude/agents/docs-keeper.md +12 -0
- package/src/claude/agents/inspector.md +1 -1
- package/src/claude/agents/project-manager.md +2 -2
- package/src/claude/agents/researcher.md +0 -1
- package/src/claude/agents/spec-maker.md +4 -1
- package/src/claude/gitignore +3 -0
- package/src/claude/hooks/__tests__/installed-root.test.js +111 -0
- package/src/claude/hooks/__tests__/plugin-context.test.js +96 -0
- package/src/claude/hooks/__tests__/plugin-options.test.js +141 -0
- package/src/claude/hooks/__tests__/plugin-state-isolation.test.js +125 -0
- package/src/claude/hooks/__tests__/precompact.test.js +213 -11
- package/src/claude/hooks/__tests__/privacy-block.test.js +60 -9
- package/src/claude/hooks/__tests__/session-writeenv.test.js +25 -0
- package/src/claude/hooks/__tests__/setup-statusline.test.js +179 -0
- package/src/claude/hooks/__tests__/spec-gate.test.js +227 -11
- package/src/claude/hooks/__tests__/spec-state-touched.test.js +204 -8
- package/src/claude/hooks/__tests__/state.test.js +207 -7
- package/src/claude/hooks/__tests__/update-check.test.js +88 -0
- package/src/claude/hooks/agent.cjs +11 -7
- package/src/claude/hooks/inspect-block.cjs +20 -66
- package/src/claude/hooks/lib/hook-state-dir.cjs +19 -1
- package/src/claude/hooks/lib/inspect-paths.cjs +304 -0
- package/src/claude/hooks/lib/parser.cjs +1 -1
- package/src/claude/hooks/lib/plugin-options.cjs +47 -0
- package/src/claude/hooks/lib/privacy-command-analysis.cjs +317 -24
- package/src/claude/hooks/lib/privacy-paths.cjs +673 -0
- package/src/claude/hooks/lib/project-root.cjs +15 -0
- package/src/claude/hooks/lib/rules-reservation.cjs +36 -0
- package/src/claude/hooks/lib/spec-touch.cjs +80 -0
- package/src/claude/hooks/lib/update-check.cjs +77 -0
- package/src/claude/hooks/plugin-context.cjs +80 -0
- package/src/claude/hooks/precompact.cjs +8 -15
- package/src/claude/hooks/privacy-block.cjs +25 -206
- package/src/claude/hooks/rules.cjs +10 -29
- package/src/claude/hooks/session.cjs +37 -117
- package/src/claude/hooks/spec-gate.cjs +9 -26
- package/src/claude/hooks/spec-state.cjs +44 -59
- package/src/claude/hooks/state.cjs +101 -52
- package/src/claude/migration-manifest.json +14 -2
- package/src/claude/plugin/commands/setup-statusline.md +12 -0
- package/src/claude/plugin/core.md +71 -0
- package/src/claude/rules/hook-protocols.md +1 -1
- package/src/claude/rules/manage-docs.md +1 -2
- package/src/claude/rules/review-audit-self-decision.md +1 -1
- package/src/claude/rules/skill-workflow-routing.md +1 -1
- package/src/claude/rules/state-sync.md +14 -0
- package/src/claude/rules/workflow.md +1 -1
- package/src/claude/runtime.json +0 -3
- package/src/claude/scripts/generate-skill-catalog.cjs +7 -2
- package/src/claude/scripts/provenance.cjs +54 -16
- package/src/claude/scripts/setup-statusline.cjs +88 -0
- package/src/claude/settings/settings.json +1 -1
- package/src/claude/skills/adv/SKILL.md +139 -0
- package/src/claude/skills/adv/references/optional-modes.md +41 -0
- package/src/claude/skills/adv/references/ultra-mode.md +29 -0
- package/src/claude/skills/ai-multimodal/scripts/check_setup.py +8 -68
- package/src/claude/skills/ai-multimodal/scripts/gemini_batch_process.py +6 -28
- package/src/claude/skills/brainstorm/SKILL.md +1 -1
- package/src/claude/skills/code-review/SKILL.md +4 -1
- package/src/claude/skills/code-review/references/pr-fix-reply.md +42 -0
- package/src/claude/skills/develop/SKILL.md +5 -3
- package/src/claude/skills/develop/references/quality-gate.md +1 -1
- package/src/claude/skills/fix/SKILL.md +2 -2
- package/src/claude/skills/git/SKILL.md +5 -3
- package/src/claude/skills/git/references/merge-pr.md +125 -0
- package/src/claude/skills/git/references/worktree-blueprint.md +1 -1
- package/src/claude/skills/specs/SKILL.md +2 -2
- package/src/claude/skills/specs/references/review.md +1 -1
- package/src/claude/skills/specs/references/templates.md +2 -0
- package/src/claude/skills/vibe/SKILL.md +69 -0
- package/src/claude/skills/vibe/references/pipeline-skill-integration.md +34 -0
- package/src/claude/skills/vibe/references/report-templates.md +84 -0
- package/src/claude/skills/vibe/references/subagent-orchestration.md +23 -0
- package/src/codex/AGENTS.md +9 -1
- package/src/codex/hooks/ask-guard.cjs +54 -0
- package/src/codex/hooks/inspect-block.cjs +15 -8
- package/src/codex/hooks/lib/hook-context.cjs +118 -4
- package/src/codex/hooks/lib/inspect-paths.cjs +1 -199
- package/src/codex/hooks/lib/plugin-options.cjs +2 -0
- package/src/codex/hooks/lib/privacy-paths.cjs +1 -0
- package/src/codex/hooks/lib/privacy-state.cjs +116 -17
- package/src/codex/hooks/lib/rules-reservation.cjs +38 -0
- package/src/codex/hooks/lib/secret-keywords.cjs +1 -0
- package/src/codex/hooks/lib/spec-session-touch.cjs +100 -0
- package/src/codex/hooks/lib/spec-touch.cjs +3 -0
- package/src/codex/hooks/lib/state-store.cjs +5 -4
- package/src/codex/hooks/lib/update-check.cjs +1 -0
- package/src/codex/hooks/precompact.cjs +40 -0
- package/src/codex/hooks/privacy-approval.cjs +28 -17
- package/src/codex/hooks/privacy-block.cjs +11 -216
- package/src/codex/hooks/rules.cjs +14 -34
- package/src/codex/hooks/secret-output-guardrail.cjs +1 -1
- package/src/codex/hooks/session.cjs +59 -11
- package/src/codex/hooks/spec-gate.cjs +21 -6
- package/src/codex/hooks/spec-state.cjs +32 -6
- package/src/codex/hooks/state.cjs +25 -4
- package/src/codex/hooks.json +39 -7
- package/src/codex/plugin/core.md +76 -0
- package/src/codex/plugin/hooks/plugin-context.cjs +63 -0
- package/src/codex/plugin/skills/setup-agents/SKILL.md +17 -0
- package/src/codex/rules/hook-protocols.md +13 -6
- package/src/codex/scripts/setup-agents.cjs +124 -0
- package/src/common/AGENTS.md +1 -1
- package/src/omp/extensions/{cafekit-bridge.mjs → cafekit-bridge.js} +85 -9
- package/src/omp/hooks/privacy-block.cjs +26 -207
- package/src/codex/rules/state-sync.md +0 -60
package/bin/install.js
CHANGED
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
const { spawnSync } = require('child_process');
|
|
19
|
-
const
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const { buildContext, parseInstallerArgs, PLATFORMS, packageJson } = require('./lib/context');
|
|
20
21
|
const lock = require('./lib/lock');
|
|
21
22
|
const backup = require('./lib/backup');
|
|
22
23
|
const manifestLib = require('./lib/manifest');
|
|
@@ -45,6 +46,28 @@ const { runPostInstall } = require('./phases/post-install');
|
|
|
45
46
|
const { setupSkillDeps } = require('./phases/skills-setup');
|
|
46
47
|
const { printSummary } = require('./phases/summary');
|
|
47
48
|
|
|
49
|
+
function preflightTrackedRootFiles(ctx) {
|
|
50
|
+
const files = ['AGENTS.md'];
|
|
51
|
+
if (ctx.platforms.includes('claude')) files.push('CLAUDE.md');
|
|
52
|
+
for (const file of files) {
|
|
53
|
+
try {
|
|
54
|
+
fs.lstatSync(file);
|
|
55
|
+
continue;
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (error.code !== 'ENOENT') throw error;
|
|
58
|
+
}
|
|
59
|
+
const result = spawnSync('git', ['ls-files', '-v', '--error-unmatch', '--', file], {
|
|
60
|
+
cwd: process.cwd(), encoding: 'utf8', timeout: 3000, shell: false,
|
|
61
|
+
});
|
|
62
|
+
if (!result.error && result.status === 0) {
|
|
63
|
+
if (result.stdout[0]?.toUpperCase() === 'S') continue;
|
|
64
|
+
throw new Error(`${file} is tracked by git but missing from the working tree; restore it with \`git checkout -- ${file}\`, or \`git rm ${file}\` if the deletion is intended, then rerun the installer.`);
|
|
65
|
+
}
|
|
66
|
+
if (!result.error && (result.status === 1 || /not a git repository/i.test(result.stderr || ''))) continue;
|
|
67
|
+
ctx.ui.warn(`Could not confirm git state for ${file}; installing a fresh copy.`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
48
71
|
/** Install a single platform: payload + runtime + metadata, under one spinner. */
|
|
49
72
|
function installPlatform(ctx, platformKey) {
|
|
50
73
|
const platform = PLATFORMS[platformKey];
|
|
@@ -97,6 +120,7 @@ function printHelp() {
|
|
|
97
120
|
console.log(`CafeKit installer (cafekit) v${packageJson.version}
|
|
98
121
|
|
|
99
122
|
Usage: npx @haposoft/cafekit [options]
|
|
123
|
+
npx @haposoft/cafekit@latest migrate [--apply]
|
|
100
124
|
|
|
101
125
|
Installs CafeKit skills, agents, rules and runtime into the current project
|
|
102
126
|
for Claude Code and/or Codex CLI. Re-runs are selective: managed files are updated,
|
|
@@ -116,12 +140,22 @@ Options:
|
|
|
116
140
|
-h, --help Show this help
|
|
117
141
|
-v, --version Print version
|
|
118
142
|
|
|
143
|
+
Commands:
|
|
144
|
+
migrate Preview removing this repository's npx install so the
|
|
145
|
+
CafeKit Claude Code and Codex plugins take over; edited
|
|
146
|
+
files and runtime.json stay. Writes nothing.
|
|
147
|
+
migrate --apply Remove it, with a backup in .cafekit-backup/ that is
|
|
148
|
+
restored if anything fails.
|
|
149
|
+
|
|
119
150
|
Docs: https://github.com/haposoft/cafekit`);
|
|
120
151
|
}
|
|
121
152
|
|
|
122
153
|
async function main() {
|
|
123
154
|
// Info flags short-circuit before any side effects.
|
|
124
155
|
const argv = process.argv.slice(2);
|
|
156
|
+
if (argv[0] === 'migrate') {
|
|
157
|
+
process.exit(await runMigrate(argv.slice(1)));
|
|
158
|
+
}
|
|
125
159
|
if (argv.includes('--help') || argv.includes('-h')) {
|
|
126
160
|
printHelp();
|
|
127
161
|
process.exit(0);
|
|
@@ -131,6 +165,15 @@ async function main() {
|
|
|
131
165
|
process.exit(0);
|
|
132
166
|
}
|
|
133
167
|
|
|
168
|
+
// Reject a mistyped argument before the lock and before anything is written.
|
|
169
|
+
try {
|
|
170
|
+
parseInstallerArgs(process.argv);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
console.error(`✗ ${error.message}`);
|
|
173
|
+
console.error(' Run npx @haposoft/cafekit --help for the options.');
|
|
174
|
+
process.exit(error.exitCode || 1);
|
|
175
|
+
}
|
|
176
|
+
|
|
134
177
|
// ── Concurrency guard (before context; plain output) ────
|
|
135
178
|
const got = lock.acquire();
|
|
136
179
|
if (!got.acquired) {
|
|
@@ -175,6 +218,8 @@ async function main() {
|
|
|
175
218
|
await selectDocumentSkills(ctx);
|
|
176
219
|
if (ctx.cancelled) { lock.release(); process.exit(0); }
|
|
177
220
|
|
|
221
|
+
preflightTrackedRootFiles(ctx);
|
|
222
|
+
|
|
178
223
|
// ── Pre-run snapshot for rollback ─────────────────────
|
|
179
224
|
// Capture platform folders AND the root files the pipeline mutates
|
|
180
225
|
// (CLAUDE.md, .gitignore) so a mid-run failure rolls back cleanly.
|
|
@@ -226,4 +271,82 @@ async function main() {
|
|
|
226
271
|
process.exit(exitCode);
|
|
227
272
|
}
|
|
228
273
|
|
|
274
|
+
function printMigrateHelp(stream = console.log) {
|
|
275
|
+
stream(`Usage: npx @haposoft/cafekit@latest migrate [--apply]
|
|
276
|
+
|
|
277
|
+
Removes this repository's CafeKit npx install so the Claude Code and Codex
|
|
278
|
+
plugins take over. Without --apply it only prints the plan.
|
|
279
|
+
|
|
280
|
+
Removed: installed files that still match their install hash and have a plugin
|
|
281
|
+
equivalent, CafeKit hook entries, and the CafeKit instruction blocks.
|
|
282
|
+
Kept: edited files, runtime.json, files you added, skills no plugin ships yet,
|
|
283
|
+
and every user entry in settings.json, hooks.json, AGENTS.md and CLAUDE.md.
|
|
284
|
+
A repository with an .omp/ install is refused.`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** `migrate [--apply]`: returns the exit code (0 done or nothing to do, 1 failed or refused, 2 bad usage). */
|
|
288
|
+
async function runMigrate(args) {
|
|
289
|
+
let apply = false;
|
|
290
|
+
for (const arg of args) {
|
|
291
|
+
if (arg === '--apply') apply = true;
|
|
292
|
+
else if (arg === '--help' || arg === '-h') {
|
|
293
|
+
printMigrateHelp();
|
|
294
|
+
return 0;
|
|
295
|
+
} else {
|
|
296
|
+
console.error(`✗ Unknown argument for migrate: ${arg}`);
|
|
297
|
+
printMigrateHelp(console.error);
|
|
298
|
+
return 2;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const { planMigration, planSteps, applyMigration } = require('./lib/migrate-apply');
|
|
303
|
+
const { renderManifestPlan } = require('./lib/migrate-plan');
|
|
304
|
+
const { renderMergedPlan } = require('./lib/migrate-merged');
|
|
305
|
+
const root = process.cwd();
|
|
306
|
+
let plan;
|
|
307
|
+
try {
|
|
308
|
+
plan = planMigration(root);
|
|
309
|
+
} catch (error) {
|
|
310
|
+
console.error(`✗ migrate could not read this repository: ${error.message}`);
|
|
311
|
+
return 1;
|
|
312
|
+
}
|
|
313
|
+
const steps = planSteps(plan);
|
|
314
|
+
console.log(`CafeKit migrate — ${apply ? 'apply' : 'dry run (nothing is written)'}\n`);
|
|
315
|
+
// The merged-file planner may mark hook scripts kept, so the manifest plan renders after it.
|
|
316
|
+
const manifestText = renderManifestPlan(plan.manifest);
|
|
317
|
+
if (manifestText) console.log(manifestText);
|
|
318
|
+
const mergedText = renderMergedPlan(plan.merged);
|
|
319
|
+
if (mergedText) console.log(`\nMerged files:\n${mergedText}`);
|
|
320
|
+
|
|
321
|
+
// An install migrate cannot read (refused) or does not manage (unmanaged) would be left
|
|
322
|
+
// behind while the rest goes, so nothing is changed until it is fixed.
|
|
323
|
+
const blocked = Object.values(plan.manifest.platforms).filter((platform) => ['refused', 'unmanaged'].includes(platform.status));
|
|
324
|
+
if (plan.merged.refused || blocked.length > 0) {
|
|
325
|
+
if (plan.merged.refused) console.error(`\n✗ Refused: ${plan.merged.refused}`);
|
|
326
|
+
for (const platform of blocked) console.error(`\n✗ ${platform.name} cannot be migrated: ${platform.reason}`);
|
|
327
|
+
console.error(' Nothing was changed. Fix or remove that install by hand, then run migrate again.');
|
|
328
|
+
return 1;
|
|
329
|
+
}
|
|
330
|
+
if (steps.length === 0) {
|
|
331
|
+
console.log('\nNothing to migrate: no CafeKit npx file here is planned for removal.');
|
|
332
|
+
return 0;
|
|
333
|
+
}
|
|
334
|
+
if (!apply) {
|
|
335
|
+
console.log(`\n${steps.length} changes planned. Folders left empty are removed too.`);
|
|
336
|
+
console.log('Run `npx @haposoft/cafekit@latest migrate --apply` to apply them, with a backup.');
|
|
337
|
+
return 0;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const result = await applyMigration(root, { plan });
|
|
341
|
+
if (result.ok) {
|
|
342
|
+
console.log(`\n✓ Removed ${result.removed} files and rewrote ${result.written}. Backup: ${result.backupDir}`);
|
|
343
|
+
console.log(' Install the plugins if you have not: see the CafeKit README.');
|
|
344
|
+
return 0;
|
|
345
|
+
}
|
|
346
|
+
console.error(`\n✗ migrate stopped (${result.reason}): ${result.message}`);
|
|
347
|
+
if (result.backupDir) console.error(` Backup: ${result.backupDir}`);
|
|
348
|
+
for (const step of result.manualSteps || []) console.error(` ${step}`);
|
|
349
|
+
return 1;
|
|
350
|
+
}
|
|
351
|
+
|
|
229
352
|
main();
|
package/bin/lib/backup.js
CHANGED
|
@@ -47,30 +47,30 @@ function validateRunId(runId) {
|
|
|
47
47
|
* Recursively copy a directory tree (verbatim, no transforms). Used for
|
|
48
48
|
* snapshot and restore. Skips if the source does not exist.
|
|
49
49
|
*/
|
|
50
|
-
function copyTree(src, dest) {
|
|
50
|
+
function copyTree(src, dest, io = fs) {
|
|
51
51
|
let stat;
|
|
52
52
|
try {
|
|
53
|
-
stat =
|
|
53
|
+
stat = io.lstatSync(src);
|
|
54
54
|
} catch (error) {
|
|
55
55
|
if (error.code === 'ENOENT') return;
|
|
56
56
|
throw error;
|
|
57
57
|
}
|
|
58
58
|
if (stat.isSymbolicLink()) {
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
io.mkdirSync(path.dirname(dest), { recursive: true });
|
|
60
|
+
io.symlinkSync(io.readlinkSync(src), dest);
|
|
61
61
|
return;
|
|
62
62
|
}
|
|
63
63
|
if (stat.isDirectory()) {
|
|
64
|
-
|
|
65
|
-
for (const entry of
|
|
66
|
-
copyTree(path.join(src, entry), path.join(dest, entry));
|
|
64
|
+
io.mkdirSync(dest, { recursive: true });
|
|
65
|
+
for (const entry of io.readdirSync(src)) {
|
|
66
|
+
copyTree(path.join(src, entry), path.join(dest, entry), io);
|
|
67
67
|
}
|
|
68
68
|
} else {
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
io.mkdirSync(path.dirname(dest), { recursive: true });
|
|
70
|
+
io.copyFileSync(src, dest);
|
|
71
71
|
// Preserve restrictive perms (e.g. .env at 0o600) on the backup copy.
|
|
72
72
|
try {
|
|
73
|
-
|
|
73
|
+
io.chmodSync(dest, stat.mode);
|
|
74
74
|
} catch {
|
|
75
75
|
/* best-effort: chmod may fail on some filesystems */
|
|
76
76
|
}
|
|
@@ -111,35 +111,54 @@ function snapshot(folders, runId) {
|
|
|
111
111
|
* Restore every explicit target to its pre-run state. Existing targets are
|
|
112
112
|
* replaced from backup; targets absent before the run are removed.
|
|
113
113
|
*
|
|
114
|
+
* A target that fails does not stop the others: every target is attempted, and
|
|
115
|
+
* the failures are thrown together at the end as `error.restoreErrors`
|
|
116
|
+
* ([{ target, message }]) so the caller can name what to put back by hand.
|
|
117
|
+
*
|
|
114
118
|
* @param {string} backupDir path returned by snapshot()
|
|
119
|
+
* @param {{fs?: object}} options file-system implementation (tests inject failures)
|
|
115
120
|
*/
|
|
116
|
-
function restore(backupDir) {
|
|
117
|
-
|
|
121
|
+
function restore(backupDir, options = {}) {
|
|
122
|
+
const io = options.fs || fs;
|
|
123
|
+
if (!backupDir || !io.existsSync(backupDir)) return;
|
|
118
124
|
|
|
119
125
|
const metadataPath = path.join(backupDir, SNAPSHOT_METADATA);
|
|
120
|
-
const metadata = JSON.parse(
|
|
126
|
+
const metadata = JSON.parse(io.readFileSync(metadataPath, 'utf8'));
|
|
121
127
|
if (metadata?.schemaVersion !== 1 || !Array.isArray(metadata.targets)) {
|
|
122
128
|
throw new Error(`Invalid backup metadata: ${metadataPath}`);
|
|
123
129
|
}
|
|
124
|
-
|
|
125
130
|
for (const record of metadata.targets) {
|
|
126
131
|
if (!record || typeof record.existed !== 'boolean') {
|
|
127
132
|
throw new Error(`Invalid backup target metadata: ${metadataPath}`);
|
|
128
133
|
}
|
|
134
|
+
validateTarget(record.target);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const restoreErrors = [];
|
|
138
|
+
for (const record of metadata.targets) {
|
|
129
139
|
const target = validateTarget(record.target);
|
|
130
|
-
const source = path.join(backupDir, SNAPSHOT_DATA, target);
|
|
131
|
-
let sourceExists = true;
|
|
132
140
|
try {
|
|
133
|
-
|
|
141
|
+
const source = path.join(backupDir, SNAPSHOT_DATA, target);
|
|
142
|
+
let sourceExists = true;
|
|
143
|
+
try {
|
|
144
|
+
io.lstatSync(source);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
if (error.code === 'ENOENT') sourceExists = false;
|
|
147
|
+
else throw error;
|
|
148
|
+
}
|
|
149
|
+
if (record.existed && !sourceExists) {
|
|
150
|
+
throw new Error(`Backup data missing for target: ${target}`);
|
|
151
|
+
}
|
|
152
|
+
io.rmSync(target, { recursive: true, force: true });
|
|
153
|
+
if (record.existed) copyTree(source, target, io);
|
|
134
154
|
} catch (error) {
|
|
135
|
-
|
|
136
|
-
else throw error;
|
|
137
|
-
}
|
|
138
|
-
if (record.existed && !sourceExists) {
|
|
139
|
-
throw new Error(`Backup data missing for target: ${target}`);
|
|
155
|
+
restoreErrors.push({ target, message: error.message });
|
|
140
156
|
}
|
|
141
|
-
|
|
142
|
-
|
|
157
|
+
}
|
|
158
|
+
if (restoreErrors.length > 0) {
|
|
159
|
+
const error = new Error(`Could not restore ${restoreErrors.map((item) => item.target).join(', ')} from ${backupDir}`);
|
|
160
|
+
error.restoreErrors = restoreErrors;
|
|
161
|
+
throw error;
|
|
143
162
|
}
|
|
144
163
|
}
|
|
145
164
|
|
package/bin/lib/codex-install.js
CHANGED
|
@@ -8,6 +8,22 @@ const { preserveAddressingSection } = require('./instruction-blocks');
|
|
|
8
8
|
const CODEX_BLOCK_START = '<!-- CAFEKIT CODEX START -->';
|
|
9
9
|
const CODEX_BLOCK_END = '<!-- CAFEKIT CODEX END -->';
|
|
10
10
|
const INSTRUCTION_EXTENSIONS = new Set(['.md', '.mdx', '.txt']);
|
|
11
|
+
const CODEX_ASK_DESCRIPTION_BOUNDARY = 'A request to fix or change something is still answered without editing any file; point the user to $cf-fix.';
|
|
12
|
+
const CODEX_ASK_NOTE = 'Rule 1 (Codex): never edit, create, or delete files in this skill, '
|
|
13
|
+
+ 'even when the user says fix it or sửa giúp luôn. Do not run commands that write files. '
|
|
14
|
+
+ 'Answer the cause with evidence and tell the user to run $cf-fix.';
|
|
15
|
+
const CODEX_RESEARCH_DEPTH_MARKER = 'Codex research depth: treat a settled repository decision that remains in force as a known option.';
|
|
16
|
+
const CODEX_RESEARCH_DEPTH_NOTE = CODEX_RESEARCH_DEPTH_MARKER
|
|
17
|
+
+ ' Use Quick for that decision. An outdated supporting rationale alone does not raise depth '
|
|
18
|
+
+ 'unless correcting it changes the recommendation. Conflicting evidence that changes the '
|
|
19
|
+
+ 'recommendation still requires escalation under the depth table.';
|
|
20
|
+
const CODEX_SPECS_SCOPE_MARKER = 'Codex scope gate: put the GATE-SCOPE question in the final answer of the turn.';
|
|
21
|
+
const CODEX_SPECS_SCOPE_NOTE = CODEX_SPECS_SCOPE_MARKER
|
|
22
|
+
+ ' Ask it there, not in commentary or through `request_user_input`. Do not send a summary after the question. '
|
|
23
|
+
+ 'For each item needing a decision, use `[NEEDS CLARIFICATION: <question>]`, replacing <question> '
|
|
24
|
+
+ 'with the real question, not a placeholder or a quotation of this note. Offer EXPAND, KEEP, and CUT together. '
|
|
25
|
+
+ 'Preserve the exact words NEEDS CLARIFICATION, EXPAND, KEEP, and CUT; do not translate them. '
|
|
26
|
+
+ 'Wait for the user to decide; do not choose scope or implement code on their behalf.';
|
|
11
27
|
|
|
12
28
|
const AGENT_NAMES = [
|
|
13
29
|
'brainstormer',
|
|
@@ -200,11 +216,15 @@ function normalizeAskUserQuestion(content) {
|
|
|
200
216
|
const oneSeparator = '(?:[^\\S\\r\\n]+|[^\\S\\r\\n]*\\r?\\n[^\\S\\r\\n]*)';
|
|
201
217
|
const pattern = new RegExp(
|
|
202
218
|
`(?:(\\b(?:${determinerPattern}|[0-9]+)\\b)(${oneSeparator}))?`
|
|
203
|
-
+ `(\\bAskUserQuestion\\b|${masked.askMarkerPattern})`,
|
|
219
|
+
+ `(\\bAskUserQuestion\\b|${masked.askMarkerPattern}|\\bnative(?:\\s+structured user-input|\\s+input)\\s+tool\\b)`,
|
|
204
220
|
'g'
|
|
205
221
|
);
|
|
206
|
-
const
|
|
207
|
-
|
|
222
|
+
const withoutRuntimePossessive = masked.content.replace(
|
|
223
|
+
/\bthe runtime['’]s\s+native(?:\s+structured user-input|\s+input)\s+tool\b/g,
|
|
224
|
+
"a question in the turn's final answer"
|
|
225
|
+
);
|
|
226
|
+
const normalized = withoutRuntimePossessive.replace(pattern, (match, determiner, spacing, _token, offset, source) => {
|
|
227
|
+
const literalIndex = match.search(/AskUserQuestion|\bnative\b/);
|
|
208
228
|
const tokenOffset = offset + (literalIndex >= 0
|
|
209
229
|
? literalIndex
|
|
210
230
|
: match.indexOf(masked.askMarkerPrefix));
|
|
@@ -219,18 +239,33 @@ function normalizeAskUserQuestion(content) {
|
|
|
219
239
|
const normalizedDeterminer = /^an$/i.test(determiner)
|
|
220
240
|
? (determiner[0] === 'A' ? 'A' : 'a')
|
|
221
241
|
: determiner;
|
|
222
|
-
return `${normalizedDeterminer}${spacing}
|
|
242
|
+
return `${normalizedDeterminer}${spacing}question in the turn's final answer`;
|
|
223
243
|
}
|
|
224
244
|
|
|
225
245
|
const trailing = source.slice(offset + match.length);
|
|
226
246
|
const pluralContext = new RegExp(`^${oneSeparator}(?:calls|batches)\\b`, 'i').test(trailing);
|
|
227
|
-
return `${pluralContext ? '' : 'a '}
|
|
247
|
+
return `${pluralContext ? '' : 'a '}question in the turn's final answer`;
|
|
228
248
|
});
|
|
229
249
|
return masked.restore(normalized);
|
|
230
250
|
}
|
|
231
251
|
|
|
232
|
-
function normalizeRuntimePaths(content) {
|
|
233
|
-
|
|
252
|
+
function normalizeRuntimePaths(content, sourcePath = '') {
|
|
253
|
+
const stateRootsPattern = /const RUNTIME_STATE_ROOTS = Object\.freeze\(\[[\s\S]*?\]\);/;
|
|
254
|
+
const sharedStateRoots = path.basename(sourcePath) === 'provenance.cjs'
|
|
255
|
+
? content.match(stateRootsPattern)?.[0] : null;
|
|
256
|
+
const preservedRoots = [];
|
|
257
|
+
let markerPrefix = '\uE000CAFEKIT_HOST_';
|
|
258
|
+
while (content.includes(markerPrefix)) markerPrefix += '_';
|
|
259
|
+
const rootToken = /(?<![\w/\\])\.claude(?:[/\\])?(?![\w/\\.-])/g;
|
|
260
|
+
const instruction = isInstructionAsset(sourcePath) ? content.replace(/^.*$/gm, line => {
|
|
261
|
+
if (!/(?<![\w/\\])\.codex(?:[/\\])?(?![\w/\\.-])/.test(line)) return line;
|
|
262
|
+
return line.replace(rootToken, original => {
|
|
263
|
+
const marker = `${markerPrefix}${preservedRoots.length}\uE001`;
|
|
264
|
+
preservedRoots.push([marker, original]);
|
|
265
|
+
return marker;
|
|
266
|
+
});
|
|
267
|
+
}) : content;
|
|
268
|
+
let next = normalizeSourcePaths(instruction, {
|
|
234
269
|
runtimeRoot: '.codex',
|
|
235
270
|
skillsRoot: '.agents/skills'
|
|
236
271
|
})
|
|
@@ -239,8 +274,21 @@ function normalizeRuntimePaths(content) {
|
|
|
239
274
|
.replace(/(?<!~\/)\.claude(?=[/\\])/g, '.codex')
|
|
240
275
|
.replace(/(['"])\.claude\1/g, '$1.codex$1')
|
|
241
276
|
.replace(/`\.claude`/g, '`.codex`');
|
|
277
|
+
// This table describes every runtime's generated state, not the install root.
|
|
278
|
+
if (sharedStateRoots) next = next.replace(stateRootsPattern, () => sharedStateRoots);
|
|
279
|
+
if (/\/ai-multimodal\/scripts\/(?:media_optimizer|document_converter)\.py$/.test(sourcePath.replace(/\\/g, '/'))) {
|
|
280
|
+
// Shared skills live under .agents; native support/config still lives in .codex.
|
|
281
|
+
next = next.replace(/\bclaude_dir = skills_dir\.parent\b/g,
|
|
282
|
+
"claude_dir = skills_dir.parent.parent / '.codex'");
|
|
283
|
+
}
|
|
284
|
+
for (const [marker, original] of preservedRoots) next = next.split(marker).join(original);
|
|
285
|
+
return next;
|
|
242
286
|
}
|
|
243
287
|
|
|
288
|
+
// Matches the vibe invocation list across a hard line wrap.
|
|
289
|
+
const VIBE_INVOCATION_CLAUDE = new RegExp(
|
|
290
|
+
'\\(`/cf:vibe` from the plugin, `/vibe` from a project install, `\\$cf-vibe` on Codex\\)'.replace(/ /g, '\\s+'), 'g');
|
|
291
|
+
|
|
244
292
|
function normalizeSkillNames(content) {
|
|
245
293
|
return content
|
|
246
294
|
.replace(/\/cf:([a-z0-9-]+)/gi, (_match, name) => `$cf-${name}`)
|
|
@@ -262,6 +310,7 @@ function normalizeAgentNames(content) {
|
|
|
262
310
|
|
|
263
311
|
function normalizeAgentInvocations(content) {
|
|
264
312
|
return content
|
|
313
|
+
.replace(/\bTask\s*\(\s*Explore\s*\)/g, 'spawn_agent(agent_type="explorer", fork_turns="none", task_name="explore", message="Describe the scan")')
|
|
265
314
|
.replace(/\bAgent\s*\(/g, 'spawn_agent(')
|
|
266
315
|
.replace(/\bsubagent_type\s*=\s*"([^"]+)"/g, (_match, name) => (
|
|
267
316
|
`agent_type="${normalizeAgentRole(name)}", fork_turns="none"`
|
|
@@ -286,15 +335,63 @@ function isInstructionAsset(sourcePath) {
|
|
|
286
335
|
}
|
|
287
336
|
|
|
288
337
|
function normalizeCodexBody(content, sourcePath = '') {
|
|
289
|
-
let next = normalizeRuntimePaths(String(content));
|
|
338
|
+
let next = normalizeRuntimePaths(String(content), sourcePath);
|
|
290
339
|
next = next
|
|
291
|
-
.replace(/\bCLAUDE\.md\b/g,
|
|
340
|
+
.replace(/^.*$/gm, line => line.replace(/\bCLAUDE\.md\b/g, (name, offset) => (
|
|
341
|
+
isInstructionAsset(sourcePath) && /(?<![\w/\\])AGENTS\.md\b/.test(line)
|
|
342
|
+
&& !/[\w/\\]/.test(line[offset - 1] || '') ? name : 'AGENTS.md'
|
|
343
|
+
)))
|
|
292
344
|
.replace(/\bClaude Code\b/g, 'Codex CLI');
|
|
293
345
|
|
|
294
346
|
if (!isInstructionAsset(sourcePath)) return next;
|
|
295
347
|
|
|
348
|
+
if (/(?:^|\/)rules\/orchestrator\.md$/.test(String(sourcePath).replace(/\\/g, '/'))) {
|
|
349
|
+
next = next
|
|
350
|
+
.replace('When the current session or a subagent runs on a model below `fable` (e.g. `opus`, `sonnet`, `haiku`) and hits a hard problem',
|
|
351
|
+
'When the current session or a subagent hits a hard problem')
|
|
352
|
+
.replace('It runs autonomously on the strongest available model',
|
|
353
|
+
'It runs autonomously with high reasoning effort on the session model unless configuration selects another');
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// The vibe-run exception lists every way to invoke vibe; on Codex only the `$cf-vibe` mention exists, and the
|
|
357
|
+
// generic rename below would turn the Claude names into the false "`$cf-vibe` from the plugin".
|
|
358
|
+
next = next.replace(VIBE_INVOCATION_CLAUDE, () => '(`$cf-vibe`)');
|
|
296
359
|
next = normalizeAgentInvocations(normalizeAgentNames(normalizeSkillNames(next)));
|
|
297
|
-
|
|
360
|
+
next = next.replace(/`?CLAUDE_CODE_SUBAGENT_MODEL`?/g,
|
|
361
|
+
'the configured agent model (`model` in `.codex/agents/<agent>.toml`, or `[agents].default_subagent_model` in native Codex configuration)');
|
|
362
|
+
next = applyReplacements(normalizeAskUserQuestion(next), INSTRUCTION_REPLACEMENTS);
|
|
363
|
+
const { frontmatter, body } = splitFrontmatter(next);
|
|
364
|
+
if (frontmatter.when_to_use && frontmatter.description) {
|
|
365
|
+
const description = `${frontmatter.description} Use when: ${frontmatter.when_to_use}`;
|
|
366
|
+
if (description.length <= 1024) {
|
|
367
|
+
const header = next.slice(0, next.length - body.length)
|
|
368
|
+
.replace(/^description:[^\r\n]*(?:\r?\n[ \t]+[^\r\n]*)*/m,
|
|
369
|
+
() => `description: ${JSON.stringify(description)}`);
|
|
370
|
+
next = header + body;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
// Only the installed ask skill receives this Codex-specific boundary, never eval prompts.
|
|
374
|
+
if (/(?:^|\/)skills\/ask\/SKILL\.md$/.test(String(sourcePath).replace(/\\/g, '/'))) {
|
|
375
|
+
const { frontmatter, body } = splitFrontmatter(next);
|
|
376
|
+
const description = `${frontmatter.description} ${CODEX_ASK_DESCRIPTION_BOUNDARY}`;
|
|
377
|
+
if (description.length > 1024) throw new Error('Codex ask description exceeds 1024 characters');
|
|
378
|
+
const header = next.slice(0, next.length - body.length)
|
|
379
|
+
.replace(/^description:[^\r\n]*$/m, () => `description: ${JSON.stringify(description)}`);
|
|
380
|
+
next = `${header}\n${CODEX_ASK_NOTE}\n\n${body}`;
|
|
381
|
+
}
|
|
382
|
+
// Only the installed research skill receives this note, never prompts or templates.
|
|
383
|
+
if (/(?:^|\/)skills\/research\/SKILL\.md$/.test(String(sourcePath).replace(/\\/g, '/'))) {
|
|
384
|
+
const { body } = splitFrontmatter(next);
|
|
385
|
+
const header = next.slice(0, next.length - body.length);
|
|
386
|
+
next = `${header}\n${CODEX_RESEARCH_DEPTH_NOTE}\n\n${body}`;
|
|
387
|
+
}
|
|
388
|
+
// Only the installed specs skill receives this note, never prompts or templates.
|
|
389
|
+
if (/(?:^|\/)skills\/specs\/SKILL\.md$/.test(String(sourcePath).replace(/\\/g, '/')) && !next.includes(CODEX_SPECS_SCOPE_NOTE)) {
|
|
390
|
+
const { body } = splitFrontmatter(next);
|
|
391
|
+
const header = next.slice(0, next.length - body.length);
|
|
392
|
+
next = `${header}\n${CODEX_SPECS_SCOPE_NOTE}\n\n${body}`;
|
|
393
|
+
}
|
|
394
|
+
return next;
|
|
298
395
|
}
|
|
299
396
|
|
|
300
397
|
function getCodexCopyOptions(baseOptions = {}) {
|
|
@@ -307,14 +404,42 @@ function getCodexCopyOptions(baseOptions = {}) {
|
|
|
307
404
|
function convertCodexAgentContent(content, fileName = '') {
|
|
308
405
|
const { frontmatter, body } = splitFrontmatter(content);
|
|
309
406
|
const name = codexAgentName(frontmatter.name || fileName);
|
|
310
|
-
|
|
407
|
+
let description = normalizeCodexBody(
|
|
311
408
|
frontmatter.description || `CafeKit Codex agent: ${name}`
|
|
312
409
|
);
|
|
313
|
-
|
|
410
|
+
let agentBody = body;
|
|
411
|
+
if (['git_ops', 'project_manager'].includes(name)) {
|
|
412
|
+
let inTeamSection = false;
|
|
413
|
+
agentBody = body.split('\n').flatMap(line => {
|
|
414
|
+
if (/^## /.test(line)) {
|
|
415
|
+
inTeamSection = /^## (?:Team Mode|Collaborative Interlocking \(Swarm Protocol\))\s*$/.test(line);
|
|
416
|
+
}
|
|
417
|
+
if (!inTeamSection) return [line];
|
|
418
|
+
if (/\b(?:TaskList|TaskGet|TaskUpdate|SendMessage|shutdown_request)\b|When instantiated as a team member|When triggered as an active teammate/.test(line)) return [];
|
|
419
|
+
return [line.replace(/^(\s*)\d+\. /, '$1- ')];
|
|
420
|
+
}).join('\n');
|
|
421
|
+
}
|
|
422
|
+
let instructions = normalizeCodexBody(agentBody).trim();
|
|
423
|
+
const readOnly = ['brainstormer', 'code_auditor', 'inspector', 'researcher'].includes(name);
|
|
424
|
+
if (readOnly) {
|
|
425
|
+
instructions += '\n\n## Read-only role\n\nRead-only role: never create, never edit, and never delete files, and never run commands that write to disk; return findings in your reply.';
|
|
426
|
+
}
|
|
427
|
+
const reasoning = [];
|
|
428
|
+
if (name === 'strategist') {
|
|
429
|
+
description = 'Autonomous advisory counsel with high reasoning effort; inherits the session model unless native spawn or agent defaults select another. No session model switch or user interview. Mention @strategist or spawn it for hard design, debugging, or trade-off calls. Advisory-only; returns advice, not code.';
|
|
430
|
+
instructions = instructions
|
|
431
|
+
.replace('consulted for counsel and running on the strongest available model',
|
|
432
|
+
'consulted for counsel with high reasoning effort on the configured native model')
|
|
433
|
+
.replace(/## Runtime note\s+[\s\S]*$/,
|
|
434
|
+
'## Runtime note\n\nThis agent sets `model_reasoning_effort = "high"` and omits `model`. It inherits the session model unless an explicit native spawn or `[agents].default_subagent_model` selects another. No strongest-model selection is guaranteed. For deployment control, set `model` in `.codex/agents/strategist.toml` or configure native agent defaults in native Codex configuration; choose a model that supports high reasoning effort. Advice remains advisory-only and never counts as execution proof.');
|
|
435
|
+
reasoning.push('model_reasoning_effort = "high"');
|
|
436
|
+
}
|
|
314
437
|
|
|
315
438
|
return [
|
|
316
439
|
`name = ${JSON.stringify(name)}`,
|
|
317
440
|
`description = ${JSON.stringify(description)}`,
|
|
441
|
+
...(readOnly ? ['sandbox_mode = "read-only"'] : []),
|
|
442
|
+
...reasoning,
|
|
318
443
|
`developer_instructions = ${JSON.stringify(instructions)}`,
|
|
319
444
|
''
|
|
320
445
|
].join('\n');
|
|
@@ -365,6 +490,10 @@ function transformManagedCodexContent(content, transform) {
|
|
|
365
490
|
}
|
|
366
491
|
|
|
367
492
|
module.exports = {
|
|
493
|
+
CODEX_RESEARCH_DEPTH_MARKER,
|
|
494
|
+
CODEX_RESEARCH_DEPTH_NOTE,
|
|
495
|
+
CODEX_SPECS_SCOPE_MARKER,
|
|
496
|
+
CODEX_SPECS_SCOPE_NOTE,
|
|
368
497
|
normalizeCodexBody,
|
|
369
498
|
getCodexCopyOptions,
|
|
370
499
|
codexAgentName,
|
package/bin/lib/context.js
CHANGED
|
@@ -128,7 +128,7 @@ const PLATFORMS = {
|
|
|
128
128
|
detectFiles: ['.omp'],
|
|
129
129
|
commandsDir: null,
|
|
130
130
|
// omp discovers .claude/skills and .agents/skills itself, so CafeKit copies
|
|
131
|
-
// no skill payload for it. See specs/omp-runtime-support/plan.md.
|
|
131
|
+
// no skill payload for it. See specs/archive/omp-runtime-support/plan.md.
|
|
132
132
|
skillsDir: '.agents/skills',
|
|
133
133
|
agentsDir: null,
|
|
134
134
|
skillsRef: '.agents/skills',
|
|
@@ -245,6 +245,7 @@ function getCopyOptions(platformKey, baseOptions = {}) {
|
|
|
245
245
|
* --upgrade / -u / --force / -f / --force-overwrite → forceOverwrite:true
|
|
246
246
|
* (overwrite user-modified managed files, backing up the user copy first)
|
|
247
247
|
* --dry-run → dryRun:true
|
|
248
|
+
* an unknown positional argument → throws with exitCode 2
|
|
248
249
|
*
|
|
249
250
|
* `upgrade` is kept as an alias of `forceOverwrite` for backward compatibility
|
|
250
251
|
* with existing docs and muscle memory.
|
|
@@ -283,6 +284,10 @@ function parseInstallerArgs(argv) {
|
|
|
283
284
|
args.lang = argv[++i];
|
|
284
285
|
} else if (arg.startsWith('--lang=')) {
|
|
285
286
|
args.lang = arg.slice('--lang='.length);
|
|
287
|
+
} else if (!arg.startsWith('-')) {
|
|
288
|
+
// A typo of a subcommand must not run an install.
|
|
289
|
+
const hint = arg === 'migrate' ? ' (migrate must be the first argument)' : '';
|
|
290
|
+
throw Object.assign(new Error(`Unknown argument: ${JSON.stringify(arg)}${hint}`), { exitCode: 2 });
|
|
286
291
|
}
|
|
287
292
|
}
|
|
288
293
|
|