ultra-builder-pro-cli 0.25.1 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.ultra-template/contexts/TEMPLATE.md +87 -0
- package/.ultra-template/decisions/.gitkeep +1 -0
- package/.ultra-template/evidence/.gitkeep +1 -0
- package/.ultra-template/north-star.md +46 -0
- package/.ultra-template/research/.gitkeep +1 -0
- package/.ultra-template/specs/discovery.md +2 -2
- package/.ultra-template/tasks.json +3 -0
- package/.ultra-template/test-report.json +27 -0
- package/CHANGELOG.md +40 -0
- package/README.md +217 -579
- package/adapters/_shared/delegate-profiles/kimi-read-only.md +15 -0
- package/adapters/_shared/delegate-profiles/kimi-write.md +17 -0
- package/adapters/_shared/file-ops.cjs +76 -2
- package/adapters/_shared/host-profile.cjs +97 -0
- package/adapters/_shared/plugin-core.cjs +204 -0
- package/adapters/_shared/runtime-assets.cjs +31 -96
- package/adapters/claude.js +92 -291
- package/adapters/codex.js +309 -842
- package/adapters/grok.js +143 -1469
- package/adapters/kimi.js +124 -1053
- package/adapters/opencode.js +230 -742
- package/bin/delegate-worker.cjs +323 -0
- package/bin/delegate.cjs +456 -0
- package/bin/install.js +38 -11
- package/docs/ARCHITECTURE.md +122 -216
- package/docs/ARTIFACT-AUTHORITY.md +90 -268
- package/docs/DECISIONS.md +75 -87
- package/docs/PHILOSOPHY.md +177 -0
- package/docs/PLUGIN-ISOLATION-CONTRACT.md +62 -39
- package/docs/RUNTIME-COMPAT-MATRIX.md +99 -156
- package/docs/SKILL-AUTHORING.md +94 -73
- package/docs/WORKFLOW-LIFECYCLE.md +93 -189
- package/hooks/README.md +17 -37
- package/hooks/_common.py +113 -0
- package/hooks/adapters/codex.py +9 -16
- package/hooks/adapters/grok.py +5 -5
- package/hooks/adapters/kimi.py +8 -14
- package/hooks/block_dangerous_commands.py +95 -0
- package/hooks/compact_context.py +65 -0
- package/hooks/mid_workflow_recall.py +25 -0
- package/hooks/post_edit_guard.py +105 -0
- package/hooks/session_context.py +30 -0
- package/package.json +17 -52
- package/skills/ultra-change/SKILL.md +97 -50
- package/skills/ultra-change/references/change-contract.md +40 -1
- package/skills/ultra-delegate/SKILL.md +82 -0
- package/skills/ultra-delegate/references/delegation-contract.md +51 -0
- package/skills/ultra-delegate/scripts/delegate_wait.py +86 -0
- package/skills/ultra-deliver/SKILL.md +61 -42
- package/skills/ultra-deliver/references/baseline-reconciliation.md +39 -20
- package/skills/ultra-dev/SKILL.md +96 -62
- package/skills/ultra-dev/references/debugging.md +22 -0
- package/skills/ultra-domain-modeling/SKILL.md +79 -0
- package/skills/ultra-grilling/SKILL.md +79 -0
- package/skills/ultra-init/SKILL.md +94 -37
- package/skills/ultra-init/references/brownfield-adoption.md +33 -83
- package/skills/ultra-init/scripts/init_project.cjs +84 -0
- package/skills/ultra-plan/SKILL.md +88 -68
- package/skills/ultra-research/SKILL.md +62 -48
- package/skills/ultra-research/references/00-problem-validation.md +5 -5
- package/skills/ultra-research/references/01-opportunity-discovery.md +3 -3
- package/skills/ultra-research/references/02-market-assessment.md +5 -4
- package/skills/ultra-research/references/03-competitive-landscape.md +3 -3
- package/skills/ultra-research/references/04-product-strategy.md +3 -3
- package/skills/ultra-research/references/05-assumptions-validation.md +5 -5
- package/skills/ultra-research/references/10-user-personas.md +3 -2
- package/skills/ultra-research/references/11-user-scenarios.md +3 -3
- package/skills/ultra-research/references/20-user-stories.md +4 -3
- package/skills/ultra-research/references/21-features-scope.md +3 -2
- package/skills/ultra-research/references/22-success-metrics.md +3 -3
- package/skills/ultra-research/references/30-architecture-context.md +3 -3
- package/skills/ultra-research/references/31-solution-strategy.md +3 -3
- package/skills/ultra-research/references/32-building-blocks.md +3 -3
- package/skills/ultra-research/references/40-deployment.md +3 -3
- package/skills/ultra-research/references/41-quality-risks.md +3 -3
- package/skills/ultra-research/references/99-synthesis.md +7 -7
- package/skills/ultra-review/SKILL.md +59 -32
- package/skills/ultra-review/references/code.md +17 -0
- package/skills/ultra-review/references/comments.md +12 -0
- package/skills/ultra-review/references/design.md +12 -0
- package/skills/ultra-review/references/errors.md +13 -0
- package/skills/ultra-review/references/spec.md +13 -0
- package/skills/ultra-review/references/tests.md +12 -0
- package/skills/ultra-review/references/worker-packet.md +40 -0
- package/skills/ultra-review/scripts/review_wait.py +26 -15
- package/skills/ultra-status/SKILL.md +61 -36
- package/skills/ultra-tdd/SKILL.md +81 -0
- package/skills/ultra-tdd/references/templates/README.md +20 -0
- package/skills/ultra-tdd/references/templates/feature-flag-default-audit.sh +79 -0
- package/skills/ultra-tdd/references/templates/persistence-real.ts +78 -0
- package/skills/ultra-tdd/references/templates/testcontainer-postgres.py +78 -0
- package/skills/ultra-tdd/references/templates/testcontainer-postgres.ts +73 -0
- package/skills/ultra-tdd/references/templates/vertical-slice.ts +66 -0
- package/skills/ultra-tdd/references/test-execution.md +18 -0
- package/skills/ultra-test/SKILL.md +70 -30
- package/skills/ultra-test/references/export-syntax.md +18 -0
- package/skills/ultra-test/scripts/worktree_digest.cjs +54 -0
- package/skills/ultra-think/SKILL.md +49 -27
- package/skills/ultra-think/references/autonomy-boundary.md +48 -0
- package/skills/ultra-think/references/interaction-boundary.md +18 -33
- package/.ultra-template/docs/research/README.md +0 -55
- package/.ultra-template/reports/templates/delivery-report.json +0 -17
- package/.ultra-template/reports/templates/test-report.json +0 -18
- package/.ultra-template/tasks/tasks.json +0 -15
- package/.ultra-template/templates/task-context.md +0 -19
- package/adapters/_shared/codex-assets.cjs +0 -725
- package/adapters/_shared/codex-ultra-tools-entry.cjs +0 -11
- package/adapters/_shared/interaction-contract.cjs +0 -129
- package/adapters/_shared/path-rewrite.cjs +0 -79
- package/adapters/_shared/runtime-native.cjs +0 -132
- package/adapters/_shared/settings-merge.cjs +0 -76
- package/agents/code-reviewer.md +0 -42
- package/agents/debugger.md +0 -40
- package/agents/review-code.md +0 -49
- package/agents/review-comments.md +0 -38
- package/agents/review-coordinator.md +0 -56
- package/agents/review-design.md +0 -40
- package/agents/review-errors.md +0 -43
- package/agents/review-spec.md +0 -39
- package/agents/review-tests.md +0 -40
- package/agents/tdd-runner.md +0 -38
- package/bin/orchestrator.js +0 -383
- package/commands/ultra-change.md +0 -10
- package/commands/ultra-deliver.md +0 -10
- package/commands/ultra-dev.md +0 -10
- package/commands/ultra-doctor.md +0 -10
- package/commands/ultra-init.md +0 -10
- package/commands/ultra-plan.md +0 -10
- package/commands/ultra-research.md +0 -10
- package/commands/ultra-review.md +0 -10
- package/commands/ultra-status.md +0 -10
- package/commands/ultra-test.md +0 -10
- package/commands/ultra-think.md +0 -10
- package/docs/AGENT-CONTEXT.md +0 -191
- package/docs/COMMIT-HASH-BACKFILL.md +0 -51
- package/docs/LEGACY-CLI-CROSSWALK.md +0 -175
- package/docs/ROADMAP.md +0 -101
- package/docs/STATE-DB-ACCESS-POLICY.md +0 -161
- package/hooks/active_task_context.py +0 -152
- package/hooks/context_envelope.py +0 -100
- package/hooks/health_check.py +0 -196
- package/hooks/pre_stop_check.py +0 -66
- package/hooks/runtime_paths.py +0 -396
- package/hooks/subagent_tracker.py +0 -88
- package/hooks/workflow_checkpoint.py +0 -76
- package/hooks/workflow_context.py +0 -48
- package/hooks/workflow_resume.py +0 -84
- package/mcp-server/hook-context.cjs +0 -38
- package/mcp-server/hook-event.cjs +0 -34
- package/mcp-server/lib/archive-journal.cjs +0 -1119
- package/mcp-server/lib/archive-mutation-worker.py +0 -365
- package/mcp-server/lib/artifact-registry.cjs +0 -1232
- package/mcp-server/lib/baseline-workflow.cjs +0 -1193
- package/mcp-server/lib/canonical-json.cjs +0 -19
- package/mcp-server/lib/change-authority.cjs +0 -26
- package/mcp-server/lib/change-packet.cjs +0 -790
- package/mcp-server/lib/change-workflow.cjs +0 -65
- package/mcp-server/lib/context-envelope.cjs +0 -581
- package/mcp-server/lib/context-paths.cjs +0 -142
- package/mcp-server/lib/decision-records.cjs +0 -423
- package/mcp-server/lib/delivery-transaction.cjs +0 -471
- package/mcp-server/lib/doctor-backup-worker.cjs +0 -328
- package/mcp-server/lib/doctor.cjs +0 -627
- package/mcp-server/lib/git-bootstrap.cjs +0 -651
- package/mcp-server/lib/init-project.cjs +0 -1021
- package/mcp-server/lib/kernel-change-workflow.cjs +0 -2029
- package/mcp-server/lib/legacy-change-workflow.cjs +0 -3032
- package/mcp-server/lib/managed-file-write.cjs +0 -172
- package/mcp-server/lib/plan-checkpoint.cjs +0 -177
- package/mcp-server/lib/plan-publication-worker.cjs +0 -830
- package/mcp-server/lib/plan-store.cjs +0 -794
- package/mcp-server/lib/prd-parser.cjs +0 -83
- package/mcp-server/lib/pricing.cjs +0 -44
- package/mcp-server/lib/project-context.cjs +0 -178
- package/mcp-server/lib/projector.cjs +0 -386
- package/mcp-server/lib/provider-refs.cjs +0 -42
- package/mcp-server/lib/runtime-paths.cjs +0 -1714
- package/mcp-server/lib/runtime-state.cjs +0 -222
- package/mcp-server/lib/safe-project-file.cjs +0 -385
- package/mcp-server/lib/stage-checkpoints.cjs +0 -463
- package/mcp-server/lib/state-authority.cjs +0 -95
- package/mcp-server/lib/state-db.cjs +0 -1580
- package/mcp-server/lib/state-ops.cjs +0 -1428
- package/mcp-server/lib/task-contract.cjs +0 -404
- package/mcp-server/lib/task-expander.cjs +0 -105
- package/mcp-server/lib/task-ledger.cjs +0 -1964
- package/mcp-server/lib/telemetry.cjs +0 -85
- package/mcp-server/lib/topo.cjs +0 -131
- package/mcp-server/lib/ultra-facade.cjs +0 -2684
- package/mcp-server/lib/worker-packet.cjs +0 -358
- package/mcp-server/server.cjs +0 -437
- package/orchestrator/auto-merge.cjs +0 -128
- package/orchestrator/daemon.cjs +0 -264
- package/orchestrator/dispatch-rules.cjs +0 -78
- package/orchestrator/parallel-orchestrator.cjs +0 -903
- package/orchestrator/planner/plan-builder.cjs +0 -125
- package/orchestrator/recovery.cjs +0 -188
- package/orchestrator/session-close-journal-worker.cjs +0 -5
- package/orchestrator/session-close-journal.cjs +0 -1014
- package/orchestrator/session-runner.cjs +0 -1153
- package/orchestrator/worktree-manager.cjs +0 -282
- package/settings.json +0 -7
- package/skills/cc-collab/SKILL.md +0 -42
- package/skills/code-review-expert/SKILL.md +0 -43
- package/skills/code-review-expert/references/correctness-reliability.md +0 -30
- package/skills/code-review-expert/references/design-boundaries.md +0 -26
- package/skills/code-review-expert/references/removal-plan.md +0 -24
- package/skills/codex-collab/SKILL.md +0 -49
- package/skills/codex-collab/evals/evals.json +0 -35
- package/skills/integration-rules/SKILL.md +0 -32
- package/skills/security-rules/SKILL.md +0 -38
- package/skills/testing-rules/SKILL.md +0 -32
- package/skills/ultra-doctor/SKILL.md +0 -41
- package/skills/ultra-plan/references/semantic-preflight.md +0 -20
- package/skills/ultra-verify/SKILL.md +0 -46
- package/skills/ultra-verify/evals/evals.json +0 -46
- package/skills/ultra-verify/references/cross-verify-modes.md +0 -28
- package/skills/ultra-verify/references/evidence-status.md +0 -21
- package/skills/ultra-verify/scripts/verify_wait.py +0 -94
- package/spec/cli-protocol.md +0 -102
- package/spec/mcp-tools.yaml +0 -312
- package/spec/schemas/baseline-reconciliation.v1.schema.json +0 -73
- package/spec/schemas/change-delta.v1.schema.json +0 -153
- package/spec/schemas/command-manifest.schema.json +0 -29
- package/spec/schemas/context-file.v4.5.schema.json +0 -54
- package/spec/schemas/delivery-report.v1.schema.json +0 -66
- package/spec/schemas/documentation-reconciliation.v1.schema.json +0 -106
- package/spec/schemas/mcp-tools.schema.json +0 -136
- package/spec/schemas/skill-manifest.schema.json +0 -24
- package/spec/schemas/state-db.sql +0 -641
- package/spec/schemas/task-ledger.v2.schema.json +0 -296
- package/spec/schemas/tasks.v4.5.schema.json +0 -96
- package/spec/schemas/test-report.v1.schema.json +0 -225
- package/ultra-tools/cli.cjs +0 -129
- package/ultra-tools/commands/db.cjs +0 -226
- package/ultra-tools/commands/legacy-memory.cjs +0 -159
- package/ultra-tools/commands/migrate.cjs +0 -1386
- package/ultra-tools/commands/session.cjs +0 -213
- package/ultra-tools/commands/status.cjs +0 -559
- package/ultra-tools/commands/system.cjs +0 -498
- package/ultra-tools/commands/task.cjs +0 -105
package/adapters/codex.js
CHANGED
|
@@ -1,21 +1,5 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Codex adapter.
|
|
5
|
-
*
|
|
6
|
-
* Codex-native mapping:
|
|
7
|
-
* commands/*.md -> explicit plugin skills ($ultra-builder-pro:<name>)
|
|
8
|
-
* skills/** -> ~/plugins/ultra-builder-pro/skills/**
|
|
9
|
-
* agents/*.md -> ~/.codex/agents/*.toml
|
|
10
|
-
* hooks/*.py -> plugin hooks with a Codex wire adapter
|
|
11
|
-
* mcp-server -> plugin .mcp.json (project cwd owns .ultra/.runtime/state.db)
|
|
12
|
-
* plugin marketplace -> ~/.agents/plugins/marketplace.json
|
|
13
|
-
*
|
|
14
|
-
* Deprecated ~/.codex/prompts and ~/.codex/skills projection is intentionally
|
|
15
|
-
* not used. The plugin remains the single distribution boundary for skills,
|
|
16
|
-
* hooks, and MCP; custom agents use Codex's standalone TOML surface.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
3
|
const fs = require('node:fs');
|
|
20
4
|
const os = require('node:os');
|
|
21
5
|
const path = require('node:path');
|
|
@@ -23,43 +7,35 @@ const crypto = require('node:crypto');
|
|
|
23
7
|
const { spawnSync } = require('node:child_process');
|
|
24
8
|
|
|
25
9
|
const {
|
|
10
|
+
copyHooks,
|
|
11
|
+
copySkills,
|
|
12
|
+
inspectPlugin,
|
|
13
|
+
markManaged,
|
|
14
|
+
publishManagedTrees,
|
|
15
|
+
removeManaged,
|
|
16
|
+
writePluginProvenance,
|
|
17
|
+
} = require('./_shared/plugin-core.cjs');
|
|
18
|
+
const {
|
|
19
|
+
captureAbsent,
|
|
26
20
|
ensureDir,
|
|
27
21
|
isManaged,
|
|
22
|
+
listRelative,
|
|
23
|
+
managedMetadata,
|
|
24
|
+
pruneCreatedEmpty,
|
|
28
25
|
removeTree,
|
|
29
26
|
writeAtomic,
|
|
30
27
|
} = require('./_shared/file-ops.cjs');
|
|
31
|
-
const {
|
|
32
|
-
PLUGIN_NAME,
|
|
33
|
-
MANAGED_MARKER,
|
|
34
|
-
applyNativeDoctor,
|
|
35
|
-
buildPlugin,
|
|
36
|
-
installAgents,
|
|
37
|
-
} = require('./_shared/codex-assets.cjs');
|
|
38
|
-
const provenance = require('./_shared/provenance.cjs');
|
|
39
28
|
|
|
40
|
-
const
|
|
41
|
-
const SOURCE_TAG = 'ubp';
|
|
42
|
-
const MARKER_BEGIN = '# >>> ultra-builder-pro managed block — do not edit by hand';
|
|
43
|
-
const MARKER_END = '# <<< ultra-builder-pro managed block';
|
|
44
|
-
const RUNTIME_MANIFEST_DIR = 'ultra-builder-pro';
|
|
45
|
-
const RUNTIME_MANIFEST_FILE = 'install-manifest.json';
|
|
46
|
-
const PROVENANCE_FILE = 'provenance.json';
|
|
47
|
-
const HOOK_ADAPTER_RELATIVE = path.join('hooks', 'adapters', 'codex.py');
|
|
29
|
+
const PLUGIN_NAME = 'ultra-builder-pro';
|
|
48
30
|
|
|
49
31
|
function resolveTarget(ctx = {}) {
|
|
50
32
|
if (ctx.configDir) return path.resolve(ctx.configDir);
|
|
51
|
-
if (ctx.scope === 'global')
|
|
52
|
-
return process.env.CODEX_HOME || path.join(ctx.homeDir || os.homedir(), '.codex');
|
|
53
|
-
}
|
|
33
|
+
if (ctx.scope === 'global') return process.env.CODEX_HOME || path.join(ctx.homeDir || os.homedir(), '.codex');
|
|
54
34
|
return path.join(ctx.cwd || process.cwd(), '.codex');
|
|
55
35
|
}
|
|
56
36
|
|
|
57
37
|
function resolveHomeDir(ctx = {}) {
|
|
58
|
-
return path.resolve(ctx.homeDir || os.homedir());
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function resolveRepoRoot(ctx = {}) {
|
|
62
|
-
return ctx.repoRoot || path.resolve(__dirname, '..');
|
|
38
|
+
return path.resolve(ctx.configDir || ctx.homeDir || os.homedir());
|
|
63
39
|
}
|
|
64
40
|
|
|
65
41
|
function resolvePluginRoot(ctx = {}) {
|
|
@@ -70,896 +46,387 @@ function resolveMarketplaceFile(ctx = {}) {
|
|
|
70
46
|
return path.join(resolveHomeDir(ctx), '.agents', 'plugins', 'marketplace.json');
|
|
71
47
|
}
|
|
72
48
|
|
|
73
|
-
function
|
|
74
|
-
return path.
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function stripManagedBlock(text) {
|
|
78
|
-
const begin = text.indexOf(MARKER_BEGIN);
|
|
79
|
-
if (begin === -1) return text;
|
|
80
|
-
const end = text.indexOf(MARKER_END, begin);
|
|
81
|
-
if (end === -1) return text;
|
|
82
|
-
const endLine = text.indexOf('\n', end);
|
|
83
|
-
const after = endLine === -1 ? text.length : endLine + 1;
|
|
84
|
-
const leading = text.slice(0, begin).replace(/\n+$/, '\n');
|
|
85
|
-
return leading + text.slice(after);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function hasManagedBlock(text) {
|
|
89
|
-
return text.includes(MARKER_BEGIN);
|
|
49
|
+
function resolveRepoRoot(ctx = {}) {
|
|
50
|
+
return ctx.repoRoot || path.resolve(__dirname, '..');
|
|
90
51
|
}
|
|
91
52
|
|
|
92
|
-
function
|
|
93
|
-
|
|
94
|
-
let configUpdated = false;
|
|
95
|
-
if (fs.existsSync(configFile)) {
|
|
96
|
-
const existing = fs.readFileSync(configFile, 'utf8');
|
|
97
|
-
if (hasManagedBlock(existing)) {
|
|
98
|
-
const stripped = stripManagedBlock(existing);
|
|
99
|
-
if (stripped.trim()) writeAtomic(configFile, stripped);
|
|
100
|
-
else fs.unlinkSync(configFile);
|
|
101
|
-
configUpdated = true;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const removed = [];
|
|
106
|
-
for (const sub of ['skills', 'prompts']) {
|
|
107
|
-
const dir = path.join(configDir, sub);
|
|
108
|
-
if (fs.existsSync(dir) && isManaged(dir)) {
|
|
109
|
-
removeTree(dir);
|
|
110
|
-
removed.push(sub);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
return { configUpdated, removed };
|
|
53
|
+
function currentCleanupRoots(ctx = {}) {
|
|
54
|
+
return new Set([resolveHomeDir(ctx), resolveTarget(ctx)].map((root) => path.resolve(root)));
|
|
114
55
|
}
|
|
115
56
|
|
|
116
|
-
function
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
const payload = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
125
|
-
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
126
|
-
throw new Error(`${file} must contain a JSON object`);
|
|
127
|
-
}
|
|
128
|
-
if (typeof payload.name !== 'string' || !payload.name.trim()) {
|
|
129
|
-
throw new Error(`${file} must contain a non-empty marketplace name`);
|
|
130
|
-
}
|
|
131
|
-
if (!payload.interface || typeof payload.interface !== 'object' || Array.isArray(payload.interface)) {
|
|
132
|
-
payload.interface = { displayName: 'Personal' };
|
|
133
|
-
}
|
|
134
|
-
if (!Array.isArray(payload.plugins)) {
|
|
135
|
-
throw new Error(`${file} field plugins must be an array`);
|
|
57
|
+
function installCleanupRoots(ctx, pluginRoot) {
|
|
58
|
+
const previous = managedMetadata(pluginRoot)?.cleanup_roots;
|
|
59
|
+
const allowed = currentCleanupRoots(ctx);
|
|
60
|
+
if (previous !== undefined) {
|
|
61
|
+
if (!Array.isArray(previous) || previous.some((entry) => (
|
|
62
|
+
!entry || !allowed.has(path.resolve(entry.root || '')) || !Array.isArray(entry.absent)
|
|
63
|
+
))) throw new Error(`invalid Codex cleanup ownership in ${pluginRoot}`);
|
|
64
|
+
return previous;
|
|
136
65
|
}
|
|
137
|
-
return payload;
|
|
138
|
-
}
|
|
139
66
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
category: 'Developer Tools',
|
|
67
|
+
const groups = new Map();
|
|
68
|
+
const add = (root, candidates) => {
|
|
69
|
+
root = path.resolve(root);
|
|
70
|
+
if (!groups.has(root)) groups.set(root, new Set());
|
|
71
|
+
for (const candidate of candidates) groups.get(root).add(candidate);
|
|
146
72
|
};
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if (next.length === payload.plugins.length) {
|
|
169
|
-
return { file, updated: false, name: payload.name };
|
|
170
|
-
}
|
|
171
|
-
payload.plugins = next;
|
|
172
|
-
writeAtomic(file, JSON.stringify(payload, null, 2) + '\n');
|
|
173
|
-
return { file, updated: true, name: payload.name };
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function shouldRunPluginCli(ctx = {}) {
|
|
177
|
-
if (typeof ctx.runPluginCli === 'boolean') return ctx.runPluginCli;
|
|
178
|
-
return ctx.scope === 'global' && !ctx.configDir;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function shouldRunHostCli(ctx = {}) {
|
|
182
|
-
if (typeof ctx.runHostCli === 'boolean') return ctx.runHostCli;
|
|
183
|
-
return shouldRunPluginCli(ctx);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function runCodexCli(args, ctx = {}) {
|
|
187
|
-
const result = spawnSync(ctx.codexBin || 'codex', args, {
|
|
188
|
-
encoding: 'utf8',
|
|
189
|
-
timeout: ctx.hostCliTimeoutMs || 30000,
|
|
190
|
-
killSignal: 'SIGKILL',
|
|
191
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
192
|
-
env: {
|
|
193
|
-
...process.env,
|
|
194
|
-
HOME: resolveHomeDir(ctx),
|
|
195
|
-
CODEX_HOME: resolveTarget(ctx),
|
|
196
|
-
},
|
|
197
|
-
});
|
|
198
|
-
if (result.error) {
|
|
199
|
-
if (result.error.code === 'ETIMEDOUT') {
|
|
200
|
-
const error = new Error(`codex ${args.join(' ')} timed out`);
|
|
201
|
-
error.code = 'HOST_CLI_TIMEOUT';
|
|
202
|
-
throw error;
|
|
73
|
+
add(resolveHomeDir(ctx), [
|
|
74
|
+
'.', 'plugins', '.agents', path.join('.agents', 'plugins'),
|
|
75
|
+
path.join('.agents', 'plugins', 'marketplace.json'),
|
|
76
|
+
]);
|
|
77
|
+
add(resolveTarget(ctx), [
|
|
78
|
+
'.', 'config.toml', 'plugins', path.join('plugins', 'cache'),
|
|
79
|
+
path.join('plugins', 'cache', 'personal'), 'tmp', path.join('tmp', 'arg0'),
|
|
80
|
+
]);
|
|
81
|
+
return [...groups].map(([root, candidates]) => ({
|
|
82
|
+
root,
|
|
83
|
+
absent: captureAbsent(root, [...candidates]),
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function pruneInstallCleanup(ctx, cleanupRoots) {
|
|
88
|
+
const allowed = currentCleanupRoots(ctx);
|
|
89
|
+
const removed = [];
|
|
90
|
+
for (const entry of cleanupRoots || []) {
|
|
91
|
+
const root = path.resolve(entry.root || '');
|
|
92
|
+
if (!allowed.has(root) || !Array.isArray(entry.absent)) {
|
|
93
|
+
throw new Error('refusing Codex cleanup outside the current config and sidecar roots');
|
|
203
94
|
}
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
if (result.status !== 0) {
|
|
207
|
-
const detail = (result.stderr || result.stdout || '').trim();
|
|
208
|
-
throw new Error(`codex ${args.join(' ')} failed${detail ? `: ${detail}` : ''}`);
|
|
95
|
+
removed.push(...pruneCreatedEmpty(root, entry.absent).map((item) => `${root}:${item}`));
|
|
209
96
|
}
|
|
210
|
-
return
|
|
97
|
+
return removed;
|
|
211
98
|
}
|
|
212
99
|
|
|
213
|
-
function
|
|
214
|
-
|
|
215
|
-
const args = action === 'add'
|
|
216
|
-
? ['plugin', 'add', selector, '--json']
|
|
217
|
-
: ['plugin', 'remove', selector, '--json'];
|
|
218
|
-
return { selector, stdout: runCodexCli(args, ctx).trim() };
|
|
100
|
+
function shellQuote(value) {
|
|
101
|
+
return `'${String(value).replaceAll("'", "'\"'\"'")}'`;
|
|
219
102
|
}
|
|
220
103
|
|
|
221
|
-
function
|
|
222
|
-
const
|
|
223
|
-
const payload = JSON.parse(runCodexCli(['plugin', 'list', '--json'], ctx));
|
|
224
|
-
if (!payload || !Array.isArray(payload.installed)) {
|
|
225
|
-
throw new Error('codex plugin list --json did not return an installed array');
|
|
226
|
-
}
|
|
227
|
-
const entry = payload.installed.find((plugin) => (
|
|
228
|
-
plugin?.pluginId === selector
|
|
229
|
-
|| (plugin?.name === PLUGIN_NAME && plugin?.marketplaceName === marketplaceName)
|
|
230
|
-
)) || null;
|
|
104
|
+
function hookCommand(pluginRoot, name) {
|
|
105
|
+
const adapter = path.join(pluginRoot, 'hooks', 'adapters', 'codex.py');
|
|
231
106
|
return {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
enabled: !!entry && entry.enabled === true,
|
|
236
|
-
entry,
|
|
237
|
-
config: captureFiles([path.join(resolveTarget(ctx), 'config.toml')])[0],
|
|
107
|
+
type: 'command',
|
|
108
|
+
command: `python3 ${shellQuote(adapter)} ${shellQuote(name)}`,
|
|
109
|
+
timeout: 10,
|
|
238
110
|
};
|
|
239
111
|
}
|
|
240
112
|
|
|
241
|
-
function
|
|
242
|
-
const action = snapshot.installed ? 'add' : 'remove';
|
|
243
|
-
let actionError = null;
|
|
244
|
-
let result = null;
|
|
245
|
-
try {
|
|
246
|
-
result = runPluginCli(action, snapshot.marketplaceName, ctx);
|
|
247
|
-
} catch (error) {
|
|
248
|
-
actionError = error;
|
|
249
|
-
}
|
|
250
|
-
let configError = null;
|
|
251
|
-
try {
|
|
252
|
-
restoreFiles([snapshot.config]);
|
|
253
|
-
} catch (error) {
|
|
254
|
-
configError = error;
|
|
255
|
-
}
|
|
256
|
-
if (actionError || configError) {
|
|
257
|
-
throw new AggregateError(
|
|
258
|
-
[actionError, configError].filter(Boolean),
|
|
259
|
-
`Codex ${action} registration rollback failed`,
|
|
260
|
-
);
|
|
261
|
-
}
|
|
113
|
+
function buildHooksManifest(pluginRoot = '${CODEX_PLUGIN_ROOT}') {
|
|
262
114
|
return {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
115
|
+
hooks: {
|
|
116
|
+
SessionStart: [
|
|
117
|
+
{ matcher: 'startup|resume|clear', hooks: [hookCommand(pluginRoot, 'session_context.py')] },
|
|
118
|
+
{ matcher: 'compact', hooks: [hookCommand(pluginRoot, 'compact_context.py')] },
|
|
119
|
+
],
|
|
120
|
+
PreToolUse: [
|
|
121
|
+
{ matcher: 'Write|Edit|Grep|apply_patch', hooks: [hookCommand(pluginRoot, 'mid_workflow_recall.py')] },
|
|
122
|
+
{ matcher: 'Bash', hooks: [hookCommand(pluginRoot, 'block_dangerous_commands.py')] },
|
|
123
|
+
],
|
|
124
|
+
PostToolUse: [
|
|
125
|
+
{ matcher: 'Write|Edit|apply_patch', hooks: [hookCommand(pluginRoot, 'post_edit_guard.py')] },
|
|
126
|
+
],
|
|
127
|
+
PreCompact: [{ matcher: 'manual|auto', hooks: [hookCommand(pluginRoot, 'compact_context.py')] }],
|
|
128
|
+
PostCompact: [{ hooks: [hookCommand(pluginRoot, 'compact_context.py')] }],
|
|
267
129
|
},
|
|
268
130
|
};
|
|
269
131
|
}
|
|
270
132
|
|
|
271
|
-
function
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
const marketplaceName = runtimeManifest?.marketplace?.name || 'personal';
|
|
277
|
-
const pluginId = `${PLUGIN_NAME}@${marketplaceName}`;
|
|
278
|
-
const plugin = payload.installed.find((entry) => (
|
|
279
|
-
entry?.pluginId === pluginId
|
|
280
|
-
|| (entry?.name === PLUGIN_NAME && entry?.marketplaceName === marketplaceName)
|
|
281
|
-
));
|
|
282
|
-
const pluginOk = !!plugin
|
|
283
|
-
&& plugin.installed === true
|
|
284
|
-
&& plugin.enabled === true
|
|
285
|
-
&& (!(expectedPluginVersion || runtimeManifest?.plugin?.version)
|
|
286
|
-
|| plugin.version === (expectedPluginVersion || runtimeManifest.plugin.version));
|
|
287
|
-
const mcpPayload = JSON.parse(runCodexCli(['mcp', 'list', '--json'], ctx));
|
|
288
|
-
if (!Array.isArray(mcpPayload)) {
|
|
289
|
-
throw new Error('codex mcp list --json did not return an array');
|
|
290
|
-
}
|
|
291
|
-
const expected = JSON.parse(
|
|
292
|
-
fs.readFileSync(path.join(resolvePluginRoot(ctx), '.mcp.json'), 'utf8'),
|
|
293
|
-
).mcpServers?.[MCP_SERVER_NAME];
|
|
294
|
-
const mcp = mcpPayload.find((entry) => entry?.name === MCP_SERVER_NAME);
|
|
295
|
-
const mcpOk = !!mcp
|
|
296
|
-
&& mcp.enabled === true
|
|
297
|
-
&& mcp.disabled_reason === null
|
|
298
|
-
&& mcp.transport?.type === 'stdio'
|
|
299
|
-
&& mcp.transport.command === expected?.command
|
|
300
|
-
&& JSON.stringify(mcp.transport.args) === JSON.stringify(expected?.args);
|
|
301
|
-
return { pluginId, plugin, pluginOk, mcp, mcpOk };
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function pluginCacheRoot(configDir, marketplaceName) {
|
|
305
|
-
if (typeof marketplaceName !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(marketplaceName)) {
|
|
306
|
-
throw new Error(`invalid Codex marketplace name: ${marketplaceName}`);
|
|
133
|
+
function contentHash(root, version) {
|
|
134
|
+
const hash = crypto.createHash('sha256').update(version);
|
|
135
|
+
for (const relative of listRelative(root).sort()) {
|
|
136
|
+
if (relative.startsWith('.codex-plugin') || relative === '.ubp-managed') continue;
|
|
137
|
+
hash.update(relative).update(fs.readFileSync(path.join(root, relative)));
|
|
307
138
|
}
|
|
308
|
-
return
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
function normalizeHookCacheVersions(versions) {
|
|
312
|
-
const normalized = new Set();
|
|
313
|
-
for (const version of versions) {
|
|
314
|
-
if (typeof version !== 'string'
|
|
315
|
-
|| !/^[A-Za-z0-9][A-Za-z0-9.+_-]*$/.test(version)) {
|
|
316
|
-
throw new Error(`invalid Codex plugin cache version: ${String(version)}`);
|
|
317
|
-
}
|
|
318
|
-
normalized.add(version);
|
|
319
|
-
}
|
|
320
|
-
return [...normalized].sort();
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
function listHookCacheVersionsOnDisk(configDir, marketplaceName) {
|
|
324
|
-
const root = pluginCacheRoot(configDir, marketplaceName);
|
|
325
|
-
if (!fs.existsSync(root)) return [];
|
|
326
|
-
return normalizeHookCacheVersions(
|
|
327
|
-
fs.readdirSync(root, { withFileTypes: true })
|
|
328
|
-
.filter((entry) => entry.isDirectory() || entry.isSymbolicLink())
|
|
329
|
-
.map((entry) => entry.name),
|
|
330
|
-
);
|
|
139
|
+
return hash.digest('hex').slice(0, 12);
|
|
331
140
|
}
|
|
332
141
|
|
|
333
|
-
function
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
}
|
|
344
|
-
versions.push(...manifest.hook_cache_versions);
|
|
345
|
-
}
|
|
142
|
+
function buildStaging(repoRoot, staging, publishedRoot) {
|
|
143
|
+
const skillRoot = path.join(staging, 'skills');
|
|
144
|
+
const hookRoot = path.join(staging, 'hooks');
|
|
145
|
+
const skills = copySkills({ runtime: 'codex', repoRoot, skillRoot });
|
|
146
|
+
copyHooks({
|
|
147
|
+
runtime: 'codex', repoRoot, hookRoot,
|
|
148
|
+
hooksManifest: buildHooksManifest(publishedRoot),
|
|
149
|
+
});
|
|
150
|
+
if (fs.existsSync(path.join(repoRoot, 'LICENSE'))) {
|
|
151
|
+
fs.copyFileSync(path.join(repoRoot, 'LICENSE'), path.join(staging, 'LICENSE'));
|
|
346
152
|
}
|
|
347
|
-
|
|
153
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'));
|
|
154
|
+
const version = `${pkg.version}+codex.${contentHash(staging, pkg.version)}`;
|
|
155
|
+
writeAtomic(path.join(staging, '.codex-plugin', 'plugin.json'), `${JSON.stringify({
|
|
156
|
+
name: PLUGIN_NAME,
|
|
157
|
+
version,
|
|
158
|
+
description: 'File-first Ultra Builder Pro workflows for Codex.',
|
|
159
|
+
author: { name: pkg.author },
|
|
160
|
+
homepage: pkg.homepage,
|
|
161
|
+
repository: pkg.repository?.url || pkg.repository,
|
|
162
|
+
license: pkg.license,
|
|
163
|
+
keywords: ['codex', 'skills', 'hooks', 'ultra-builder-pro'],
|
|
164
|
+
skills: './skills/',
|
|
165
|
+
interface: {
|
|
166
|
+
displayName: 'Ultra Builder Pro',
|
|
167
|
+
shortDescription: 'File-first project workflows and evidence.',
|
|
168
|
+
longDescription: 'Resume product and engineering work across sessions and coding-agent hosts through portable Skills, repository files, and Git.',
|
|
169
|
+
developerName: pkg.author,
|
|
170
|
+
category: 'Developer Tools',
|
|
171
|
+
capabilities: ['Interactive', 'Write'],
|
|
172
|
+
websiteURL: pkg.homepage,
|
|
173
|
+
defaultPrompt: [
|
|
174
|
+
'Start a new project with $ultra-builder-pro:ultra-init.',
|
|
175
|
+
'Reconcile this change with $ultra-builder-pro:ultra-change.',
|
|
176
|
+
'Show workflow status with $ultra-builder-pro:ultra-status.',
|
|
177
|
+
],
|
|
178
|
+
},
|
|
179
|
+
}, null, 2)}\n`);
|
|
180
|
+
markManaged(staging, { adapter: 'codex', plugin: PLUGIN_NAME, version });
|
|
181
|
+
writePluginProvenance({
|
|
182
|
+
adapter: 'codex', repoRoot, stagingRoot: staging, publishedRoot,
|
|
183
|
+
contracts: {
|
|
184
|
+
plugin_manifest: { root: 'plugin', path: '.codex-plugin/plugin.json' },
|
|
185
|
+
hooks_manifest: { root: 'plugin', path: 'hooks/hooks.json' },
|
|
186
|
+
hook_adapter: { root: 'plugin', path: 'hooks/adapters/codex.py' },
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
return { skills, version };
|
|
348
190
|
}
|
|
349
191
|
|
|
350
|
-
function
|
|
351
|
-
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|| manifest.name !== PLUGIN_NAME) {
|
|
356
|
-
throw new Error(`invalid Codex plugin manifest: ${file}`);
|
|
192
|
+
function loadMarketplace(file) {
|
|
193
|
+
if (!fs.existsSync(file)) return { name: 'personal', interface: { displayName: 'Personal' }, plugins: [] };
|
|
194
|
+
const value = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
195
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || !Array.isArray(value.plugins)) {
|
|
196
|
+
throw new Error(`invalid Codex marketplace: ${file}`);
|
|
357
197
|
}
|
|
358
|
-
|
|
359
|
-
return { file, contents, manifest };
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function codexVersionParts(version) {
|
|
363
|
-
if (typeof version !== 'string') return null;
|
|
364
|
-
const match = /^(.+)\+codex\.([A-Za-z0-9][A-Za-z0-9._-]*)$/.exec(version);
|
|
365
|
-
return match ? { base: match[1], cachebuster: match[2] } : null;
|
|
198
|
+
return value;
|
|
366
199
|
}
|
|
367
200
|
|
|
368
|
-
function
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
marketplaceName,
|
|
375
|
-
}) {
|
|
376
|
-
const previousVersion = runtimeManifest?.plugin?.version;
|
|
377
|
-
const currentVersion = sourceManifest?.manifest?.version;
|
|
378
|
-
const previous = codexVersionParts(previousVersion);
|
|
379
|
-
const current = codexVersionParts(currentVersion);
|
|
380
|
-
if (!previous || !current || previous.base !== current.base
|
|
381
|
-
|| previousVersion === currentVersion) {
|
|
382
|
-
return false;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
const mismatch = report.issues.filter((issue) => (
|
|
386
|
-
issue.code === 'ASSET_HASH_MISMATCH'
|
|
387
|
-
&& issue.root === 'plugin'
|
|
388
|
-
&& issue.path === path.join('.codex-plugin', 'plugin.json')
|
|
389
|
-
));
|
|
390
|
-
if (mismatch.length !== 1
|
|
391
|
-
|| !host?.pluginOk
|
|
392
|
-
|| host.plugin?.version !== currentVersion) {
|
|
393
|
-
report.checks.cachebuster_binding = { status: 'fail' };
|
|
394
|
-
return false;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
const originalManifest = {
|
|
398
|
-
...sourceManifest.manifest,
|
|
399
|
-
version: previousVersion,
|
|
400
|
-
};
|
|
401
|
-
const originalHash = crypto.createHash('sha256')
|
|
402
|
-
.update(`${JSON.stringify(originalManifest, null, 2)}\n`)
|
|
403
|
-
.digest('hex');
|
|
404
|
-
const cacheManifestFile = path.join(
|
|
405
|
-
pluginCacheRoot(configDir, marketplaceName),
|
|
406
|
-
currentVersion,
|
|
407
|
-
'.codex-plugin',
|
|
408
|
-
'plugin.json',
|
|
409
|
-
);
|
|
410
|
-
let cacheMatchesSource = false;
|
|
411
|
-
try {
|
|
412
|
-
cacheMatchesSource = crypto.createHash('sha256')
|
|
413
|
-
.update(fs.readFileSync(cacheManifestFile))
|
|
414
|
-
.digest('hex')
|
|
415
|
-
=== crypto.createHash('sha256')
|
|
416
|
-
.update(sourceManifest.contents)
|
|
417
|
-
.digest('hex');
|
|
418
|
-
} catch (error) {
|
|
419
|
-
if (error.code !== 'ENOENT') throw error;
|
|
420
|
-
}
|
|
421
|
-
if (originalHash !== mismatch[0].expected || !cacheMatchesSource) {
|
|
422
|
-
report.checks.cachebuster_binding = { status: 'fail' };
|
|
423
|
-
return false;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
report.issues = report.issues.filter((issue) => issue !== mismatch[0]);
|
|
427
|
-
report.checks.assets.failed -= 1;
|
|
428
|
-
if (report.checks.assets.failed === 0) report.checks.assets.status = 'pass';
|
|
429
|
-
report.checks.cachebuster_binding = {
|
|
430
|
-
status: 'pass',
|
|
431
|
-
previous_version: previousVersion,
|
|
432
|
-
current_version: currentVersion,
|
|
433
|
-
cache_manifest: cacheManifestFile,
|
|
201
|
+
function marketplaceEntry() {
|
|
202
|
+
return {
|
|
203
|
+
name: PLUGIN_NAME,
|
|
204
|
+
source: { source: 'local', path: `./plugins/${PLUGIN_NAME}` },
|
|
205
|
+
policy: { installation: 'AVAILABLE', authentication: 'ON_INSTALL' },
|
|
206
|
+
category: 'Developer Tools',
|
|
434
207
|
};
|
|
435
|
-
return true;
|
|
436
208
|
}
|
|
437
209
|
|
|
438
|
-
function
|
|
439
|
-
const
|
|
440
|
-
|
|
441
|
-
|
|
210
|
+
function writeMarketplace(file) {
|
|
211
|
+
const value = loadMarketplace(file);
|
|
212
|
+
const next = value.plugins.filter((entry) => entry?.name !== PLUGIN_NAME);
|
|
213
|
+
next.push(marketplaceEntry());
|
|
214
|
+
value.plugins = next;
|
|
215
|
+
writeAtomic(file, `${JSON.stringify(value, null, 2)}\n`);
|
|
216
|
+
return value.name;
|
|
442
217
|
}
|
|
443
218
|
|
|
444
|
-
function
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
);
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
function hookForwarder(target) {
|
|
453
|
-
return `#!/usr/bin/env python3
|
|
454
|
-
"""Forward a live Codex task from a retired plugin cache to the current adapter."""
|
|
455
|
-
|
|
456
|
-
import runpy
|
|
457
|
-
|
|
458
|
-
runpy.run_path(${JSON.stringify(path.resolve(target))}, run_name="__main__")
|
|
459
|
-
`;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
function prepareHookAdapterPath(file) {
|
|
463
|
-
const versionRoot = path.dirname(path.dirname(path.dirname(file)));
|
|
464
|
-
try {
|
|
465
|
-
const stat = fs.lstatSync(versionRoot);
|
|
466
|
-
if (stat.isSymbolicLink()) {
|
|
467
|
-
fs.unlinkSync(versionRoot);
|
|
468
|
-
} else if (!stat.isDirectory()) {
|
|
469
|
-
throw new Error(`Codex plugin cache version is not a directory: ${versionRoot}`);
|
|
470
|
-
}
|
|
471
|
-
} catch (error) {
|
|
472
|
-
if (error.code !== 'ENOENT') throw error;
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
function restoreCachedHookAdapters(previousAdapters, target) {
|
|
477
|
-
const resolvedTarget = path.resolve(target);
|
|
478
|
-
if (!fs.existsSync(resolvedTarget)) {
|
|
479
|
-
throw new Error(`current Codex hook adapter is missing: ${resolvedTarget}`);
|
|
480
|
-
}
|
|
481
|
-
const restored = [];
|
|
482
|
-
for (const previous of previousAdapters) {
|
|
483
|
-
if (path.resolve(previous) === resolvedTarget || fs.existsSync(previous)) continue;
|
|
484
|
-
prepareHookAdapterPath(previous);
|
|
485
|
-
writeAtomic(previous, hookForwarder(resolvedTarget));
|
|
486
|
-
restored.push(previous);
|
|
487
|
-
}
|
|
488
|
-
return { target: resolvedTarget, restored };
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
function currentHookAdapter(configDir, marketplaceName, plugin) {
|
|
492
|
-
const cached = path.join(pluginCacheRoot(configDir, marketplaceName), plugin.version, HOOK_ADAPTER_RELATIVE);
|
|
493
|
-
if (fs.existsSync(cached)) return cached;
|
|
494
|
-
return path.join(plugin.root, HOOK_ADAPTER_RELATIVE);
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
function writeRuntimeManifest(configDir, data) {
|
|
498
|
-
const file = runtimeManifestFile(configDir);
|
|
499
|
-
writeAtomic(file, JSON.stringify({
|
|
500
|
-
source: SOURCE_TAG,
|
|
501
|
-
adapter: 'codex',
|
|
502
|
-
plugin: data.plugin,
|
|
503
|
-
marketplace: data.marketplace,
|
|
504
|
-
agents: data.agents,
|
|
505
|
-
hook_cache_versions: data.hookCacheVersions,
|
|
506
|
-
}, null, 2) + '\n');
|
|
507
|
-
return file;
|
|
219
|
+
function runCodex(args, ctx) {
|
|
220
|
+
const result = spawnSync(ctx.codexBin || 'codex', args, {
|
|
221
|
+
encoding: 'utf8', timeout: ctx.hostCliTimeoutMs || 30000,
|
|
222
|
+
env: { ...process.env, HOME: resolveHomeDir(ctx), CODEX_HOME: resolveTarget(ctx) },
|
|
223
|
+
});
|
|
224
|
+
if (result.error) throw result.error;
|
|
225
|
+
if (result.status !== 0) throw new Error((result.stderr || result.stdout || '').trim() || `codex ${args.join(' ')} failed`);
|
|
226
|
+
return result.stdout;
|
|
508
227
|
}
|
|
509
228
|
|
|
510
|
-
function
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
const stat = fs.statSync(file);
|
|
514
|
-
return { file, exists: true, contents: fs.readFileSync(file), mode: stat.mode & 0o7777 };
|
|
515
|
-
} catch (error) {
|
|
516
|
-
if (error.code === 'ENOENT') return { file, exists: false };
|
|
517
|
-
throw error;
|
|
518
|
-
}
|
|
519
|
-
});
|
|
229
|
+
function shouldRunPluginCli(ctx = {}) {
|
|
230
|
+
if (typeof ctx.runPluginCli === 'boolean') return ctx.runPluginCli;
|
|
231
|
+
return ctx.scope === 'global';
|
|
520
232
|
}
|
|
521
233
|
|
|
522
|
-
function
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
fs.unlinkSync(snapshot.file);
|
|
527
|
-
} catch (error) {
|
|
528
|
-
if (error.code !== 'ENOENT') throw error;
|
|
529
|
-
}
|
|
530
|
-
continue;
|
|
531
|
-
}
|
|
532
|
-
writeAtomic(snapshot.file, snapshot.contents);
|
|
533
|
-
fs.chmodSync(snapshot.file, snapshot.mode);
|
|
234
|
+
function installedPlugin(ctx) {
|
|
235
|
+
const value = JSON.parse(runCodex(['plugin', 'list', '--json'], ctx));
|
|
236
|
+
if (!Array.isArray(value.installed)) {
|
|
237
|
+
throw new Error('codex plugin list --json must include an installed array');
|
|
534
238
|
}
|
|
239
|
+
return value.installed.find((entry) => entry?.name === PLUGIN_NAME) || null;
|
|
535
240
|
}
|
|
536
241
|
|
|
537
|
-
function
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
if (fs.existsSync(target)) {
|
|
543
|
-
const existing = fs.readFileSync(target, 'utf8');
|
|
544
|
-
if (!existing.startsWith(`# ${MANAGED_MARKER}`)) {
|
|
545
|
-
throw new Error(`refusing to overwrite unmanaged Codex agent: ${target}`);
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
for (const file of staged.installed) {
|
|
550
|
-
writeAtomic(
|
|
551
|
-
path.join(targetRoot, file),
|
|
552
|
-
fs.readFileSync(path.join(staged.root, file)),
|
|
553
|
-
);
|
|
242
|
+
function restoreMarketplace(file, previous) {
|
|
243
|
+
if (previous !== null) {
|
|
244
|
+
writeAtomic(file, previous);
|
|
245
|
+
} else if (fs.existsSync(file)) {
|
|
246
|
+
removeMarketplace(file, { deleteIfEmpty: true });
|
|
554
247
|
}
|
|
555
|
-
return { root: targetRoot, installed: staged.installed };
|
|
556
248
|
}
|
|
557
249
|
|
|
558
250
|
function install(ctx = {}) {
|
|
559
|
-
const configDir = resolveTarget(ctx);
|
|
560
251
|
const repoRoot = resolveRepoRoot(ctx);
|
|
561
252
|
const pluginRoot = resolvePluginRoot(ctx);
|
|
562
253
|
const marketplaceFile = resolveMarketplaceFile(ctx);
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
const previousManifest = readRuntimeManifest(configDir);
|
|
254
|
+
const previousMetadata = managedMetadata(pluginRoot) || {};
|
|
255
|
+
const cleanupRoots = installCleanupRoots(ctx, pluginRoot);
|
|
566
256
|
if (fs.existsSync(pluginRoot) && !isManaged(pluginRoot)) {
|
|
567
|
-
throw new Error(`refusing to replace unmanaged plugin
|
|
568
|
-
}
|
|
257
|
+
throw new Error(`refusing to replace unmanaged Codex plugin: ${pluginRoot}`);
|
|
258
|
+
}
|
|
259
|
+
ensureDir(resolveTarget(ctx));
|
|
260
|
+
const runNative = shouldRunPluginCli(ctx);
|
|
261
|
+
const previousRegistration = runNative ? installedPlugin(ctx) : null;
|
|
262
|
+
const previousMarketplace = fs.existsSync(marketplaceFile)
|
|
263
|
+
? fs.readFileSync(marketplaceFile, 'utf8')
|
|
264
|
+
: null;
|
|
265
|
+
const marketplaceCreated = previousMetadata.marketplace_created === true
|
|
266
|
+
|| (previousMetadata.marketplace_created === undefined && previousMarketplace === null);
|
|
267
|
+
const previousMarketplaceName = fs.existsSync(marketplaceFile)
|
|
268
|
+
? loadMarketplace(marketplaceFile).name
|
|
269
|
+
: 'personal';
|
|
569
270
|
ensureDir(path.dirname(pluginRoot));
|
|
570
|
-
const
|
|
571
|
-
const
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
let
|
|
576
|
-
let marketplace = null;
|
|
577
|
-
let previousVersions = [];
|
|
578
|
-
let previousAdapters = [];
|
|
579
|
-
let snapshots = [];
|
|
580
|
-
let movedPrevious = false;
|
|
271
|
+
const staging = fs.mkdtempSync(path.join(path.dirname(pluginRoot), `.${PLUGIN_NAME}-staging-`));
|
|
272
|
+
const recovery = fs.existsSync(pluginRoot)
|
|
273
|
+
? `${pluginRoot}.ubp-recovery-${crypto.randomUUID()}`
|
|
274
|
+
: null;
|
|
275
|
+
if (recovery) fs.cpSync(pluginRoot, recovery, { recursive: true });
|
|
276
|
+
let marketplaceName;
|
|
581
277
|
let published = false;
|
|
582
|
-
let
|
|
583
|
-
let
|
|
584
|
-
let registrationAttempted = false;
|
|
585
|
-
let hookCompatibility = null;
|
|
586
|
-
let legacy = { configUpdated: false, removed: [] };
|
|
278
|
+
let marketplaceWritten = false;
|
|
279
|
+
let nativeAttempted = false;
|
|
587
280
|
try {
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
entry: marketplaceEntry(),
|
|
595
|
-
};
|
|
596
|
-
previousVersions = listKnownHookCacheVersions(
|
|
597
|
-
configDir,
|
|
598
|
-
marketplace.name,
|
|
599
|
-
previousManifest,
|
|
600
|
-
);
|
|
601
|
-
previousAdapters = hookAdaptersForVersions(
|
|
602
|
-
configDir,
|
|
603
|
-
marketplace.name,
|
|
604
|
-
previousVersions,
|
|
605
|
-
);
|
|
606
|
-
snapshots = captureFiles([
|
|
607
|
-
marketplaceFile,
|
|
608
|
-
runtimeManifestFile(configDir),
|
|
609
|
-
path.join(configDir, RUNTIME_MANIFEST_DIR, PROVENANCE_FILE),
|
|
610
|
-
...stagedAgents.installed.map((file) => path.join(configDir, 'agents', file)),
|
|
611
|
-
...previousAdapters,
|
|
612
|
-
]);
|
|
613
|
-
|
|
614
|
-
if (fs.existsSync(pluginRoot)) {
|
|
615
|
-
fs.renameSync(pluginRoot, backupRoot);
|
|
616
|
-
movedPrevious = true;
|
|
617
|
-
}
|
|
618
|
-
fs.renameSync(stagingRoot, pluginRoot);
|
|
281
|
+
const copied = buildStaging(repoRoot, staging, pluginRoot);
|
|
282
|
+
markManaged(staging, {
|
|
283
|
+
cleanup_roots: cleanupRoots,
|
|
284
|
+
marketplace_created: marketplaceCreated,
|
|
285
|
+
});
|
|
286
|
+
publishManagedTrees([{ source: staging, target: pluginRoot, label: 'Codex plugin' }]);
|
|
619
287
|
published = true;
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
registrationAttempted = true;
|
|
626
|
-
registration = runPluginCli('add', marketplace.name, ctx);
|
|
627
|
-
if (typeof ctx.afterPluginRegistration === 'function') {
|
|
628
|
-
ctx.afterPluginRegistration({
|
|
629
|
-
action: 'add',
|
|
630
|
-
before: registrationSnapshot,
|
|
631
|
-
registration,
|
|
632
|
-
});
|
|
633
|
-
}
|
|
634
|
-
hookCompatibility = restoreCachedHookAdapters(
|
|
635
|
-
previousAdapters,
|
|
636
|
-
currentHookAdapter(configDir, marketplace.name, plugin),
|
|
637
|
-
);
|
|
288
|
+
marketplaceName = writeMarketplace(marketplaceFile);
|
|
289
|
+
marketplaceWritten = true;
|
|
290
|
+
if (runNative) {
|
|
291
|
+
nativeAttempted = true;
|
|
292
|
+
runCodex(['plugin', 'add', `${PLUGIN_NAME}@${marketplaceName}`, '--json'], ctx);
|
|
638
293
|
}
|
|
639
|
-
|
|
640
|
-
...previousVersions,
|
|
641
|
-
plugin.version,
|
|
642
|
-
]);
|
|
643
|
-
const manifestFile = writeRuntimeManifest(configDir, {
|
|
644
|
-
plugin: { root: plugin.root, version: plugin.version },
|
|
645
|
-
marketplace: { file: marketplace.file, name: marketplace.name },
|
|
646
|
-
agents: agents.installed,
|
|
647
|
-
hookCacheVersions,
|
|
648
|
-
});
|
|
649
|
-
const source = provenance.packageSource(repoRoot);
|
|
650
|
-
const provenanceFile = path.join(configDir, RUNTIME_MANIFEST_DIR, PROVENANCE_FILE);
|
|
651
|
-
const provenanceManifest = provenance.writeProvenance({
|
|
652
|
-
file: provenanceFile,
|
|
653
|
-
adapter: 'codex',
|
|
654
|
-
...source,
|
|
655
|
-
roots: { plugin: pluginRoot, config: configDir },
|
|
656
|
-
assets: [
|
|
657
|
-
...provenance.assetRefsForTree('plugin', pluginRoot, {
|
|
658
|
-
exclude: ['.ubp-managed'],
|
|
659
|
-
}),
|
|
660
|
-
...agents.installed.map((file) => ({ root: 'config', path: path.join('agents', file) })),
|
|
661
|
-
{ root: 'config', path: path.join(RUNTIME_MANIFEST_DIR, RUNTIME_MANIFEST_FILE) },
|
|
662
|
-
],
|
|
663
|
-
contracts: {
|
|
664
|
-
plugin_manifest: { root: 'plugin', path: '.codex-plugin/plugin.json' },
|
|
665
|
-
mcp_registration: { root: 'plugin', path: '.mcp.json' },
|
|
666
|
-
mcp_launcher: { root: 'plugin', path: 'runtime/launch.cjs' },
|
|
667
|
-
native_runtime: { root: 'plugin', path: 'runtime/native-runtime.json' },
|
|
668
|
-
context_envelope_helper: { root: 'plugin', path: 'runtime/hook-context.cjs' },
|
|
669
|
-
hook_event_helper: { root: 'plugin', path: 'runtime/hook-event.cjs' },
|
|
670
|
-
hooks_manifest: { root: 'plugin', path: 'hooks/hooks.json' },
|
|
671
|
-
hook_adapter: { root: 'plugin', path: HOOK_ADAPTER_RELATIVE },
|
|
672
|
-
runtime_manifest: { root: 'config', path: path.join(RUNTIME_MANIFEST_DIR, RUNTIME_MANIFEST_FILE) },
|
|
673
|
-
},
|
|
674
|
-
});
|
|
675
|
-
provenanceManifest.file = provenanceFile;
|
|
676
|
-
legacy = cleanupLegacyConfig(configDir);
|
|
677
|
-
if (movedPrevious) removeTree(backupRoot);
|
|
678
|
-
removeTree(agentStagingConfig);
|
|
294
|
+
if (recovery) removeTree(recovery);
|
|
679
295
|
return {
|
|
680
|
-
target:
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
legacy,
|
|
687
|
-
manifestFile,
|
|
688
|
-
provenance: provenanceManifest,
|
|
689
|
-
config: { updated: legacy.configUpdated },
|
|
296
|
+
target: resolveTarget(ctx),
|
|
297
|
+
pluginRoot,
|
|
298
|
+
skillRoot: path.join(pluginRoot, 'skills'),
|
|
299
|
+
hookRoot: path.join(pluginRoot, 'hooks'),
|
|
300
|
+
marketplaceFile,
|
|
301
|
+
copied,
|
|
690
302
|
};
|
|
691
303
|
} catch (error) {
|
|
692
|
-
|
|
693
|
-
else if (fs.existsSync(stagingRoot)) removeTree(stagingRoot);
|
|
694
|
-
if (movedPrevious && fs.existsSync(backupRoot)) fs.renameSync(backupRoot, pluginRoot);
|
|
695
|
-
let rollbackError = null;
|
|
304
|
+
const rollback = [];
|
|
696
305
|
try {
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
removeTree(
|
|
306
|
+
if (published) {
|
|
307
|
+
if (fs.existsSync(pluginRoot)) removeTree(pluginRoot);
|
|
308
|
+
if (recovery && fs.existsSync(recovery)) fs.renameSync(recovery, pluginRoot);
|
|
309
|
+
} else if (recovery && fs.existsSync(recovery)) {
|
|
310
|
+
removeTree(recovery);
|
|
702
311
|
}
|
|
703
|
-
|
|
704
|
-
|
|
312
|
+
} catch (rollbackError) {
|
|
313
|
+
rollback.push(rollbackError);
|
|
314
|
+
}
|
|
315
|
+
try {
|
|
316
|
+
if (marketplaceWritten) restoreMarketplace(marketplaceFile, previousMarketplace);
|
|
317
|
+
} catch (rollbackError) {
|
|
318
|
+
rollback.push(rollbackError);
|
|
319
|
+
}
|
|
320
|
+
if (nativeAttempted) {
|
|
321
|
+
try {
|
|
322
|
+
const currentRegistration = installedPlugin(ctx);
|
|
323
|
+
if (previousRegistration && !currentRegistration) {
|
|
324
|
+
runCodex([
|
|
325
|
+
'plugin', 'add', `${PLUGIN_NAME}@${previousMarketplaceName}`, '--json',
|
|
326
|
+
], ctx);
|
|
327
|
+
} else if (!previousRegistration && currentRegistration) {
|
|
328
|
+
runCodex([
|
|
329
|
+
'plugin', 'remove', `${PLUGIN_NAME}@${marketplaceName}`, '--json',
|
|
330
|
+
], ctx);
|
|
331
|
+
}
|
|
332
|
+
} catch (rollbackError) {
|
|
333
|
+
rollback.push(rollbackError);
|
|
705
334
|
}
|
|
706
|
-
restoreFiles(snapshots);
|
|
707
|
-
} catch (caught) {
|
|
708
|
-
rollbackError = caught;
|
|
709
|
-
} finally {
|
|
710
|
-
if (fs.existsSync(agentStagingConfig)) removeTree(agentStagingConfig);
|
|
711
335
|
}
|
|
712
|
-
if (
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
);
|
|
336
|
+
if (fs.existsSync(staging)) removeTree(staging);
|
|
337
|
+
if (recovery && fs.existsSync(recovery)) removeTree(recovery);
|
|
338
|
+
try { pruneInstallCleanup(ctx, cleanupRoots); } catch (cleanupError) { rollback.push(cleanupError); }
|
|
339
|
+
if (rollback.length) {
|
|
340
|
+
throw new AggregateError([error, ...rollback], 'Codex install and rollback failed');
|
|
717
341
|
}
|
|
718
342
|
throw error;
|
|
719
343
|
}
|
|
720
344
|
}
|
|
721
345
|
|
|
722
346
|
function doctor(ctx = {}) {
|
|
723
|
-
const configDir = resolveTarget(ctx);
|
|
724
|
-
const pluginRoot = resolvePluginRoot(ctx);
|
|
725
347
|
const repoRoot = resolveRepoRoot(ctx);
|
|
726
|
-
const
|
|
727
|
-
const
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
348
|
+
const pluginRoot = resolvePluginRoot(ctx);
|
|
349
|
+
const report = inspectPlugin({
|
|
350
|
+
adapter: 'codex', repoRoot, pluginRoot,
|
|
351
|
+
skillRoot: path.join(pluginRoot, 'skills'),
|
|
352
|
+
hookRoot: path.join(pluginRoot, 'hooks'),
|
|
353
|
+
manifestFile: path.join(pluginRoot, '.codex-plugin', 'plugin.json'),
|
|
732
354
|
});
|
|
733
|
-
|
|
734
|
-
let runtimeManifest = null;
|
|
735
|
-
try {
|
|
736
|
-
runtimeManifest = readRuntimeManifest(configDir);
|
|
737
|
-
} catch (error) {
|
|
738
|
-
report.issues.push({ code: 'RUNTIME_MANIFEST_INVALID', message: error.message });
|
|
739
|
-
}
|
|
740
|
-
const runtimeManifestOk = runtimeManifest?.source === SOURCE_TAG
|
|
741
|
-
&& runtimeManifest.adapter === 'codex'
|
|
742
|
-
&& runtimeManifest.plugin?.root === pluginRoot
|
|
743
|
-
&& typeof runtimeManifest.plugin?.version === 'string';
|
|
744
|
-
if (!runtimeManifestOk && !report.issues.some((entry) => entry.code === 'RUNTIME_MANIFEST_INVALID')) {
|
|
745
|
-
report.issues.push({ code: 'RUNTIME_MANIFEST_INVALID', path: runtimeManifestFile(configDir) });
|
|
746
|
-
}
|
|
747
|
-
report.checks.runtime_manifest = { status: runtimeManifestOk ? 'pass' : 'fail' };
|
|
748
|
-
|
|
749
355
|
let marketplaceOk = false;
|
|
750
356
|
try {
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
} catch (error) {
|
|
756
|
-
report.issues.push({ code: 'MARKETPLACE_REGISTRATION_INVALID', message: error.message });
|
|
757
|
-
}
|
|
758
|
-
if (!marketplaceOk && !report.issues.some((entry) => entry.code === 'MARKETPLACE_REGISTRATION_INVALID')) {
|
|
759
|
-
report.issues.push({ code: 'MARKETPLACE_REGISTRATION_INVALID', path: marketplaceFile });
|
|
760
|
-
}
|
|
357
|
+
marketplaceOk = loadMarketplace(resolveMarketplaceFile(ctx)).plugins.some((entry) => (
|
|
358
|
+
entry?.name === PLUGIN_NAME && entry.source?.path === `./plugins/${PLUGIN_NAME}`
|
|
359
|
+
));
|
|
360
|
+
} catch {}
|
|
761
361
|
report.checks.marketplace = { status: marketplaceOk ? 'pass' : 'fail' };
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
sourceManifest = readPluginManifest(pluginRoot);
|
|
766
|
-
} catch (error) {
|
|
767
|
-
report.issues.push({ code: 'PLUGIN_MANIFEST_INVALID', message: error.message });
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
let host = null;
|
|
771
|
-
if (shouldRunHostCli(ctx)) {
|
|
772
|
-
try {
|
|
773
|
-
host = inspectCodexHost(
|
|
774
|
-
ctx,
|
|
775
|
-
runtimeManifest,
|
|
776
|
-
sourceManifest?.manifest?.version || null,
|
|
777
|
-
);
|
|
778
|
-
report.checks.host_plugin = { status: host.pluginOk ? 'pass' : 'fail' };
|
|
779
|
-
report.checks.host_mcp = { status: host.mcpOk ? 'pass' : 'fail' };
|
|
780
|
-
if (!host.pluginOk) {
|
|
781
|
-
report.issues.push({
|
|
782
|
-
code: 'HOST_PLUGIN_NOT_DISCOVERED',
|
|
783
|
-
plugin_id: host.pluginId,
|
|
784
|
-
});
|
|
785
|
-
}
|
|
786
|
-
if (!host.mcpOk) {
|
|
787
|
-
report.issues.push({
|
|
788
|
-
code: 'HOST_MCP_NOT_DISCOVERED',
|
|
789
|
-
plugin_id: host.pluginId,
|
|
790
|
-
server: MCP_SERVER_NAME,
|
|
791
|
-
});
|
|
792
|
-
}
|
|
793
|
-
} catch (error) {
|
|
794
|
-
report.checks.host_plugin = { status: 'fail' };
|
|
795
|
-
report.checks.host_mcp = { status: 'fail' };
|
|
796
|
-
report.issues.push({
|
|
797
|
-
code: error.code === 'HOST_CLI_TIMEOUT'
|
|
798
|
-
? 'HOST_CLI_TIMEOUT'
|
|
799
|
-
: 'HOST_CLI_INSPECTION_FAILED',
|
|
800
|
-
message: error.message,
|
|
801
|
-
});
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
const marketplaceName = runtimeManifest?.marketplace?.name || 'personal';
|
|
806
|
-
if (runtimeManifestOk && sourceManifest && host) {
|
|
807
|
-
bindCachebusterProvenance({
|
|
808
|
-
report,
|
|
809
|
-
runtimeManifest,
|
|
810
|
-
sourceManifest,
|
|
811
|
-
host,
|
|
812
|
-
configDir,
|
|
813
|
-
marketplaceName,
|
|
814
|
-
});
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
let hookTargetsOk = runtimeManifestOk;
|
|
818
|
-
if (runtimeManifestOk) {
|
|
819
|
-
const cacheRoot = pluginCacheRoot(configDir, marketplaceName);
|
|
820
|
-
let requiredVersions = [];
|
|
362
|
+
if (!marketplaceOk) report.issues.push({ code: 'MARKETPLACE_REGISTRATION_INVALID' });
|
|
363
|
+
if (shouldRunPluginCli(ctx)) {
|
|
364
|
+
let discovered = false;
|
|
821
365
|
try {
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
function removeManagedAgents(configDir, manifest) {
|
|
851
|
-
const agentRoot = path.join(configDir, 'agents');
|
|
852
|
-
const requested = manifest && Array.isArray(manifest.agents)
|
|
853
|
-
? manifest.agents
|
|
854
|
-
: (fs.existsSync(agentRoot) ? fs.readdirSync(agentRoot).filter((name) => name.endsWith('.toml')) : []);
|
|
855
|
-
const removed = [];
|
|
856
|
-
for (const file of requested) {
|
|
857
|
-
const target = path.join(agentRoot, path.basename(file));
|
|
858
|
-
if (!fs.existsSync(target)) continue;
|
|
859
|
-
const contents = fs.readFileSync(target, 'utf8');
|
|
860
|
-
if (!contents.startsWith(`# ${MANAGED_MARKER}`)) continue;
|
|
861
|
-
fs.unlinkSync(target);
|
|
862
|
-
removed.push(path.basename(target));
|
|
863
|
-
}
|
|
864
|
-
return removed;
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
function readRuntimeManifest(configDir) {
|
|
868
|
-
const file = runtimeManifestFile(configDir);
|
|
869
|
-
if (!fs.existsSync(file)) return null;
|
|
870
|
-
try {
|
|
871
|
-
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
872
|
-
} catch (error) {
|
|
873
|
-
throw new Error(`invalid Codex runtime manifest ${file}: ${error.message}`, { cause: error });
|
|
366
|
+
const plugin = installedPlugin(ctx);
|
|
367
|
+
discovered = !!plugin && plugin.installed !== false && plugin.enabled === true;
|
|
368
|
+
} catch {}
|
|
369
|
+
report.checks.host_plugin = { status: discovered ? 'pass' : 'fail' };
|
|
370
|
+
if (!discovered) report.issues.push({ code: 'HOST_PLUGIN_NOT_DISCOVERED' });
|
|
371
|
+
}
|
|
372
|
+
report.checks.hook_activation = {
|
|
373
|
+
status: 'user_review_required',
|
|
374
|
+
reason: 'Codex does not trust non-managed plugin hooks merely because the plugin is installed or enabled.',
|
|
375
|
+
};
|
|
376
|
+
report.actions = [{
|
|
377
|
+
code: 'REVIEW_CODEX_HOOK_TRUST',
|
|
378
|
+
message: 'Open a new Codex session, review the current Ultra hook definition, and trust it before relying on hook acceleration.',
|
|
379
|
+
}];
|
|
380
|
+
report.status = report.issues.length === 0 ? 'healthy' : 'degraded';
|
|
381
|
+
return report;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function removeMarketplace(file, { deleteIfEmpty = false } = {}) {
|
|
385
|
+
if (!fs.existsSync(file)) return false;
|
|
386
|
+
const value = loadMarketplace(file);
|
|
387
|
+
const before = value.plugins.length;
|
|
388
|
+
value.plugins = value.plugins.filter((entry) => entry?.name !== PLUGIN_NAME);
|
|
389
|
+
if (value.plugins.length === before) return false;
|
|
390
|
+
if (deleteIfEmpty && value.plugins.length === 0 && value.name === 'personal') {
|
|
391
|
+
fs.unlinkSync(file);
|
|
392
|
+
} else {
|
|
393
|
+
writeAtomic(file, `${JSON.stringify(value, null, 2)}\n`);
|
|
874
394
|
}
|
|
395
|
+
return true;
|
|
875
396
|
}
|
|
876
397
|
|
|
877
398
|
function uninstall(ctx = {}) {
|
|
878
|
-
const configDir = resolveTarget(ctx);
|
|
879
|
-
const pluginRoot = resolvePluginRoot(ctx);
|
|
880
399
|
const marketplaceFile = resolveMarketplaceFile(ctx);
|
|
881
|
-
const
|
|
882
|
-
const
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
let pluginRemoved = false;
|
|
902
|
-
if (fs.existsSync(pluginRoot) && fs.existsSync(path.join(pluginRoot, '.ubp-managed'))) {
|
|
903
|
-
removeTree(pluginRoot);
|
|
904
|
-
pluginRemoved = true;
|
|
905
|
-
}
|
|
906
|
-
const marketplace = removeMarketplaceEntry(marketplaceFile);
|
|
907
|
-
const legacy = cleanupLegacyConfig(configDir);
|
|
908
|
-
const manifestDir = path.join(configDir, RUNTIME_MANIFEST_DIR);
|
|
909
|
-
if (fs.existsSync(manifestDir)) removeTree(manifestDir);
|
|
910
|
-
|
|
911
|
-
return {
|
|
912
|
-
target: configDir,
|
|
913
|
-
removed: { plugin: pluginRemoved, agents },
|
|
914
|
-
marketplace,
|
|
915
|
-
registration,
|
|
916
|
-
legacy,
|
|
917
|
-
config: { updated: legacy.configUpdated },
|
|
918
|
-
};
|
|
919
|
-
} catch (error) {
|
|
920
|
-
if (registrationAttempted && registrationSnapshot) {
|
|
921
|
-
try {
|
|
922
|
-
restorePluginRegistration(registrationSnapshot, ctx);
|
|
923
|
-
} catch (rollbackError) {
|
|
924
|
-
throw new AggregateError(
|
|
925
|
-
[error, rollbackError],
|
|
926
|
-
'Codex uninstall and registration rollback both failed',
|
|
927
|
-
);
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
throw error;
|
|
931
|
-
}
|
|
400
|
+
const pluginRoot = resolvePluginRoot(ctx);
|
|
401
|
+
const metadata = managedMetadata(pluginRoot) || {};
|
|
402
|
+
const cleanupRoots = metadata.cleanup_roots || [];
|
|
403
|
+
const marketplaceCreated = metadata.marketplace_created === true;
|
|
404
|
+
const marketplace = fs.existsSync(marketplaceFile) ? loadMarketplace(marketplaceFile) : { name: 'personal' };
|
|
405
|
+
if (shouldRunPluginCli(ctx)) {
|
|
406
|
+
runCodex(['plugin', 'remove', `${PLUGIN_NAME}@${marketplace.name}`, '--json'], ctx);
|
|
407
|
+
}
|
|
408
|
+
const removedPlugin = removeManaged(pluginRoot, 'Codex plugin');
|
|
409
|
+
const removedMarketplace = removeMarketplace(marketplaceFile, { deleteIfEmpty: marketplaceCreated });
|
|
410
|
+
const cleaned = pruneInstallCleanup(ctx, cleanupRoots);
|
|
411
|
+
return {
|
|
412
|
+
target: resolveTarget(ctx), pluginRoot,
|
|
413
|
+
removed: {
|
|
414
|
+
plugin: removedPlugin,
|
|
415
|
+
marketplace: removedMarketplace,
|
|
416
|
+
},
|
|
417
|
+
cleaned,
|
|
418
|
+
};
|
|
932
419
|
}
|
|
933
420
|
|
|
934
421
|
module.exports = {
|
|
935
422
|
name: 'codex',
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
MARKER_END,
|
|
939
|
-
SOURCE_TAG,
|
|
423
|
+
PLUGIN_NAME,
|
|
424
|
+
buildHooksManifest,
|
|
940
425
|
resolveTarget,
|
|
941
426
|
resolveHomeDir,
|
|
942
427
|
resolvePluginRoot,
|
|
943
428
|
resolveMarketplaceFile,
|
|
944
429
|
install,
|
|
945
|
-
uninstall,
|
|
946
430
|
doctor,
|
|
947
|
-
|
|
948
|
-
stripManagedBlock,
|
|
949
|
-
hasManagedBlock,
|
|
950
|
-
cleanupLegacyConfig,
|
|
951
|
-
loadMarketplace,
|
|
952
|
-
upsertMarketplace,
|
|
953
|
-
removeMarketplaceEntry,
|
|
954
|
-
runPluginCli,
|
|
955
|
-
runCodexCli,
|
|
956
|
-
snapshotPluginRegistration,
|
|
957
|
-
restorePluginRegistration,
|
|
958
|
-
inspectCodexHost,
|
|
959
|
-
pluginCacheRoot,
|
|
960
|
-
listKnownHookCacheVersions,
|
|
961
|
-
listCachedHookAdapters,
|
|
962
|
-
prepareHookAdapterPath,
|
|
963
|
-
restoreCachedHookAdapters,
|
|
964
|
-
},
|
|
431
|
+
uninstall,
|
|
965
432
|
};
|