ultra-builder-pro-cli 0.5.3 → 0.7.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.
Files changed (91) hide show
  1. package/.ultra-template/changes/active/.gitkeep +1 -0
  2. package/.ultra-template/changes/archive/.gitkeep +1 -0
  3. package/CHANGELOG.md +90 -14
  4. package/README.md +38 -32
  5. package/adapters/_shared/codex-assets.cjs +32 -151
  6. package/adapters/_shared/frontmatter.cjs +2 -2
  7. package/adapters/_shared/handbook.cjs +10 -10
  8. package/adapters/_shared/path-rewrite.cjs +0 -3
  9. package/adapters/_shared/runtime-assets.cjs +13 -4
  10. package/adapters/_shared/tests/handbook.test.cjs +4 -1
  11. package/adapters/_shared/tests/path-rewrite.test.cjs +1 -3
  12. package/adapters/_shared/tests/resolve-target.test.cjs +1 -11
  13. package/adapters/_shared/tests/runtime-assets.test.cjs +7 -6
  14. package/adapters/opencode.js +82 -26
  15. package/adapters/tests/claude.test.cjs +1 -4
  16. package/adapters/tests/codex.test.cjs +9 -8
  17. package/adapters/tests/opencode.test.cjs +17 -11
  18. package/bin/install.js +6 -28
  19. package/bin/orchestrator.js +12 -30
  20. package/commands/ultra-change.md +47 -0
  21. package/commands/ultra-deliver.md +14 -7
  22. package/commands/ultra-dev.md +6 -3
  23. package/commands/ultra-doctor.md +36 -0
  24. package/commands/ultra-init.md +4 -3
  25. package/commands/ultra-status.md +7 -5
  26. package/commands/ultra-test.md +2 -1
  27. package/docs/AGENT-CONTEXT.md +35 -28
  28. package/docs/ARCHITECTURE.md +49 -21
  29. package/docs/PLAN.zh-CN.md +67 -68
  30. package/docs/ROADMAP.md +17 -17
  31. package/docs/RUNTIME-COMPAT-MATRIX.md +63 -62
  32. package/hooks/README.md +8 -7
  33. package/hooks/active_task_context.py +22 -15
  34. package/hooks/health_check.py +136 -38
  35. package/hooks/workflow_context.py +120 -30
  36. package/mcp-server/lib/change-workflow.cjs +556 -0
  37. package/mcp-server/lib/change-workflow.test.cjs +259 -0
  38. package/mcp-server/lib/doctor.cjs +112 -0
  39. package/mcp-server/lib/doctor.test.cjs +86 -0
  40. package/mcp-server/lib/init-project.cjs +32 -6
  41. package/mcp-server/lib/pricing.cjs +0 -6
  42. package/mcp-server/lib/runtime-state.cjs +222 -0
  43. package/mcp-server/lib/runtime-state.test.cjs +92 -0
  44. package/mcp-server/lib/state-db.cjs +31 -8
  45. package/mcp-server/lib/state-ops.cjs +24 -3
  46. package/mcp-server/lib/telemetry.cjs +4 -0
  47. package/mcp-server/server.cjs +102 -10
  48. package/mcp-server/tests/db-ops.test.cjs +5 -1
  49. package/mcp-server/tests/init-project.test.cjs +45 -0
  50. package/mcp-server/tests/server-contract.test.cjs +140 -1
  51. package/mcp-server/tests/state-db.test.cjs +44 -7
  52. package/mcp-server/tests/state-ops.test.cjs +42 -0
  53. package/mcp-server/tests/telemetry.test.cjs +14 -0
  54. package/orchestrator/daemon.cjs +39 -0
  55. package/orchestrator/dispatch-rules.cjs +15 -4
  56. package/orchestrator/tests/daemon.test.cjs +63 -12
  57. package/orchestrator/tests/dispatch-rules.test.cjs +12 -10
  58. package/package.json +5 -7
  59. package/settings.json +1 -2
  60. package/skills/ultra-change/SKILL.md +140 -0
  61. package/skills/ultra-deliver/SKILL.md +81 -15
  62. package/skills/ultra-dev/SKILL.md +32 -12
  63. package/skills/ultra-doctor/SKILL.md +90 -0
  64. package/skills/ultra-init/SKILL.md +8 -6
  65. package/skills/ultra-review/SKILL.md +16 -0
  66. package/skills/ultra-status/SKILL.md +30 -7
  67. package/skills/ultra-test/SKILL.md +14 -2
  68. package/skills/ultra-verify/SKILL.md +22 -41
  69. package/skills/ultra-verify/evals/evals.json +22 -24
  70. package/skills/ultra-verify/references/confidence-system.md +15 -32
  71. package/skills/ultra-verify/references/cross-verify-modes.md +14 -59
  72. package/skills/ultra-verify/scripts/verify_wait.py +44 -81
  73. package/spec/cli-protocol.md +12 -1
  74. package/spec/mcp-tools.yaml +340 -4
  75. package/spec/schemas/mcp-tools.schema.json +1 -1
  76. package/spec/schemas/skill-manifest.schema.json +2 -2
  77. package/spec/schemas/state-db.sql +119 -2
  78. package/ultra-tools/cli.cjs +3 -0
  79. package/ultra-tools/commands/system.cjs +57 -0
  80. package/ultra-tools/system-command.test.cjs +53 -0
  81. package/adapters/_shared/md-to-toml.cjs +0 -54
  82. package/adapters/_shared/rtk-detect.cjs +0 -86
  83. package/adapters/_shared/tests/md-to-toml.test.cjs +0 -48
  84. package/adapters/_shared/tests/rtk-detect.test.cjs +0 -116
  85. package/adapters/gemini.js +0 -204
  86. package/adapters/tests/gemini.test.cjs +0 -112
  87. package/docs/LEGACY-HERMES.md +0 -1175
  88. package/orchestrator/code-graph-watcher.cjs +0 -121
  89. package/orchestrator/tests/code-graph-watcher.test.cjs +0 -183
  90. package/skills/gemini-collab/SKILL.md +0 -51
  91. package/skills/gemini-collab/evals/evals.json +0 -42
@@ -0,0 +1,57 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ const { openStateDb, closeStateDb } = require('../../mcp-server/lib/state-db.cjs');
7
+ const doctor = require('../../mcp-server/lib/doctor.cjs');
8
+
9
+ const USAGE = `ultra-tools system doctor [--repair]\n`;
10
+
11
+ function emit(envelope) {
12
+ process.stdout.write(`${JSON.stringify(envelope)}\n`);
13
+ }
14
+
15
+ async function dispatchDoctor(args) {
16
+ const unknown = args.filter((arg) => !['--repair', '-h', '--help'].includes(arg));
17
+ if (args.includes('-h') || args.includes('--help')) {
18
+ process.stdout.write(USAGE);
19
+ return 0;
20
+ }
21
+ if (unknown.length > 0) {
22
+ emit({ ok: false, error: { code: 'VALIDATION_ERROR', message: `unknown system doctor flag: ${unknown[0]}` } });
23
+ return 1;
24
+ }
25
+ const rootDir = process.cwd();
26
+ const dbPath = path.join(rootDir, '.ultra', 'state.db');
27
+ if (!fs.existsSync(dbPath)) {
28
+ emit({ ok: false, error: { code: 'STATE_DB_MISSING', message: `.ultra/state.db not found under ${rootDir}` } });
29
+ return 2;
30
+ }
31
+ const db = openStateDb(dbPath);
32
+ try {
33
+ const data = await doctor.runDoctor(db, { rootDir, repair: args.includes('--repair') });
34
+ emit({ ok: true, data });
35
+ return data.status === 'healthy' ? 0 : 2;
36
+ } catch (error) {
37
+ emit({ ok: false, error: { code: error.code || 'STATE_DB_ERROR', message: error.message } });
38
+ return 2;
39
+ } finally {
40
+ closeStateDb(db);
41
+ }
42
+ }
43
+
44
+ async function dispatch(args) {
45
+ const [verb, ...rest] = args;
46
+ if (!verb || verb === '-h' || verb === '--help') {
47
+ process.stdout.write(USAGE);
48
+ return 0;
49
+ }
50
+ if (verb !== 'doctor') {
51
+ emit({ ok: false, error: { code: 'UNKNOWN_VERB', message: `unknown system verb: ${verb}` } });
52
+ return 1;
53
+ }
54
+ return dispatchDoctor(rest);
55
+ }
56
+
57
+ module.exports = { USAGE, dispatch, dispatchDoctor };
@@ -0,0 +1,53 @@
1
+ 'use strict';
2
+
3
+ const { test } = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const fs = require('node:fs');
6
+ const os = require('node:os');
7
+ const path = require('node:path');
8
+ const { spawnSync } = require('node:child_process');
9
+
10
+ const { initStateDb, closeStateDb } = require('../mcp-server/lib/state-db.cjs');
11
+
12
+ const CLI = path.join(__dirname, 'cli.cjs');
13
+
14
+ function fixture() {
15
+ const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-system-cli-'));
16
+ const { db } = initStateDb(path.join(rootDir, '.ultra', 'state.db'));
17
+ closeStateDb(db);
18
+ return rootDir;
19
+ }
20
+
21
+ function invoke(rootDir, args) {
22
+ return spawnSync(process.execPath, [CLI, 'system', ...args], {
23
+ cwd: rootDir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
24
+ });
25
+ }
26
+
27
+ test('system doctor provides a read-only JSON health envelope', () => {
28
+ const rootDir = fixture();
29
+ try {
30
+ const result = invoke(rootDir, ['doctor']);
31
+ assert.equal(result.status, 0, result.stderr);
32
+ const envelope = JSON.parse(result.stdout.trim().split('\n').at(-1));
33
+ assert.equal(envelope.ok, true);
34
+ assert.equal(envelope.data.status, 'healthy');
35
+ assert.equal(envelope.data.repair_performed, false);
36
+ } finally {
37
+ fs.rmSync(rootDir, { recursive: true, force: true });
38
+ }
39
+ });
40
+
41
+ test('system doctor --repair creates a backup before recovery', () => {
42
+ const rootDir = fixture();
43
+ try {
44
+ const result = invoke(rootDir, ['doctor', '--repair']);
45
+ assert.equal(result.status, 0, result.stderr);
46
+ const envelope = JSON.parse(result.stdout.trim().split('\n').at(-1));
47
+ assert.equal(envelope.ok, true);
48
+ assert.equal(envelope.data.repair_performed, true);
49
+ assert.ok(fs.existsSync(envelope.data.backup_path));
50
+ } finally {
51
+ fs.rmSync(rootDir, { recursive: true, force: true });
52
+ }
53
+ });
@@ -1,54 +0,0 @@
1
- 'use strict';
2
-
3
- // Convert a thin-shell command .md file to a Gemini custom-command TOML.
4
- // Gemini commands live in `~/.gemini/commands/<name>.toml` with shape:
5
- //
6
- // description = "…"
7
- // prompt = """
8
- // <command body>
9
- // """
10
- //
11
- // Only the description (from frontmatter) and the body text cross over.
12
- // Frontmatter keys like `workflow-ref` are inlined into the prompt so the
13
- // agent still sees the skill reference.
14
-
15
- const { parse: parseFrontmatter } = require('./frontmatter.cjs');
16
-
17
- function escapeTomlLiteral(str) {
18
- // Gemini accepts """multi-line""" literals. Escape any triple-quote to be safe.
19
- return String(str).replace(/"""/g, '\\"\\"\\"');
20
- }
21
-
22
- function buildPromptSection(fm, body) {
23
- const lines = [];
24
- if (fm && fm['workflow-ref']) {
25
- lines.push(`[ultra-builder-pro] workflow reference: ${fm['workflow-ref']}`);
26
- }
27
- if (fm && Array.isArray(fm.mcp_tools_required) && fm.mcp_tools_required.length > 0) {
28
- lines.push(`[ultra-builder-pro] MCP tools required: ${fm.mcp_tools_required.join(', ')}`);
29
- }
30
- if (fm && fm.cli_fallback) {
31
- lines.push(`[ultra-builder-pro] CLI fallback: ultra-tools ${fm.cli_fallback}`);
32
- }
33
- if (lines.length > 0) lines.push('');
34
- lines.push(body.trimEnd());
35
- return lines.join('\n');
36
- }
37
-
38
- function mdCommandToToml(mdText) {
39
- if (typeof mdText !== 'string') throw new TypeError('mdCommandToToml: expects string');
40
- const { fm, body } = parseFrontmatter(mdText);
41
- const description = (fm && fm.description) || 'Ultra Builder Pro command';
42
- const prompt = buildPromptSection(fm, body);
43
-
44
- const tomlLines = [
45
- `description = ${JSON.stringify(description)}`,
46
- 'prompt = """',
47
- escapeTomlLiteral(prompt),
48
- '"""',
49
- '',
50
- ];
51
- return tomlLines.join('\n');
52
- }
53
-
54
- module.exports = { mdCommandToToml };
@@ -1,86 +0,0 @@
1
- 'use strict';
2
-
3
- // Phase 6.1 — RTK soft dependency.
4
- //
5
- // RTK (Rust Token Killer) is an external CLI proxy the user installs
6
- // separately. We never hard-require it: installation continues even when
7
- // rtk is missing, we just print an install hint. This mirrors PLAN Phase
8
- // 6.1: "无 rtk → 提示安装 + 跳过(不硬依赖)".
9
- //
10
- // Tests inject a PATH override so we can exercise the happy/missing paths
11
- // without touching the real rtk binary on the machine.
12
-
13
- const { execFileSync } = require('node:child_process');
14
-
15
- const INSTALL_HINT =
16
- 'rtk not found. RTK is a token-saving proxy that rewrites Bash/git/npm\n' +
17
- ' commands transparently. Install: https://github.com/rcrocky/rtk\n' +
18
- ' (or run `cargo install rtk`). Continuing without RTK.';
19
-
20
- function envWithPath(pathOverride) {
21
- if (!pathOverride) return process.env;
22
- // Isolate rtk lookup to pathOverride + minimal system dirs (so shebangs
23
- // like `/usr/bin/env bash` still resolve). Tests use this to cut off the
24
- // real rtk binary on the developer's machine.
25
- return { ...process.env, PATH: `${pathOverride}:/usr/bin:/bin` };
26
- }
27
-
28
- function detectRtk({ pathOverride = null, timeoutMs = 3000 } = {}) {
29
- try {
30
- const stdout = execFileSync('rtk', ['--version'], {
31
- env: envWithPath(pathOverride),
32
- stdio: ['ignore', 'pipe', 'pipe'],
33
- timeout: timeoutMs,
34
- });
35
- return { available: true, version: stdout.toString().trim() };
36
- } catch (err) {
37
- return {
38
- available: false,
39
- reason: err && err.code === 'ENOENT' ? 'binary not on PATH' : (err && err.message) || 'unknown',
40
- };
41
- }
42
- }
43
-
44
- function initRtk({ pathOverride = null, scope = 'global', timeoutMs = 10000 } = {}) {
45
- const args = ['init'];
46
- if (scope === 'global') args.push('-g');
47
- try {
48
- const stdout = execFileSync('rtk', args, {
49
- env: envWithPath(pathOverride),
50
- stdio: ['ignore', 'pipe', 'pipe'],
51
- timeout: timeoutMs,
52
- });
53
- return { ok: true, stdout: stdout.toString() };
54
- } catch (err) {
55
- return { ok: false, error: (err && err.message) || 'unknown' };
56
- }
57
- }
58
-
59
- // High-level entry: detect + optionally init, never throws.
60
- // Caller passes {skip:true} to honor --skip-rtk flag.
61
- function installHook({ skip = false, pathOverride = null, scope = 'global' } = {}) {
62
- if (skip) return { skipped: true };
63
- const detection = detectRtk({ pathOverride });
64
- if (!detection.available) {
65
- return {
66
- available: false,
67
- initialized: false,
68
- reason: detection.reason,
69
- hint: INSTALL_HINT,
70
- };
71
- }
72
- const initResult = initRtk({ pathOverride, scope });
73
- return {
74
- available: true,
75
- initialized: initResult.ok,
76
- version: detection.version,
77
- init_error: initResult.ok ? undefined : initResult.error,
78
- };
79
- }
80
-
81
- module.exports = {
82
- detectRtk,
83
- initRtk,
84
- installHook,
85
- INSTALL_HINT,
86
- };
@@ -1,48 +0,0 @@
1
- 'use strict';
2
-
3
- const { test } = require('node:test');
4
- const assert = require('node:assert/strict');
5
-
6
- const { mdCommandToToml } = require('../md-to-toml.cjs');
7
-
8
- test('mdCommandToToml extracts description from frontmatter', () => {
9
- const md = [
10
- '---',
11
- 'description: Initialize project',
12
- 'workflow-ref: "@skills/ultra-init/SKILL.md"',
13
- '---',
14
- '',
15
- '# /ultra-init',
16
- 'body content here',
17
- ].join('\n');
18
-
19
- const toml = mdCommandToToml(md);
20
- assert.match(toml, /description = "Initialize project"/);
21
- assert.match(toml, /prompt = """/);
22
- assert.match(toml, /body content here/);
23
- assert.match(toml, /workflow reference: @skills\/ultra-init\/SKILL\.md/);
24
- });
25
-
26
- test('mdCommandToToml includes mcp_tools_required and cli_fallback when present', () => {
27
- const md = [
28
- '---',
29
- 'description: plan tasks',
30
- 'mcp_tools_required:',
31
- ' - task.create',
32
- ' - ask.question',
33
- 'cli_fallback: "task create"',
34
- '---',
35
- 'body',
36
- ].join('\n');
37
-
38
- const toml = mdCommandToToml(md);
39
- assert.match(toml, /MCP tools required: task\.create, ask\.question/);
40
- assert.match(toml, /CLI fallback: ultra-tools task create/);
41
- });
42
-
43
- test('mdCommandToToml defaults description when frontmatter absent', () => {
44
- const md = '# plain markdown, no frontmatter';
45
- const toml = mdCommandToToml(md);
46
- assert.match(toml, /description = "Ultra Builder Pro command"/);
47
- assert.match(toml, /plain markdown, no frontmatter/);
48
- });
@@ -1,116 +0,0 @@
1
- 'use strict';
2
-
3
- // Phase 6.1 — RTK soft dependency:
4
- // • detectRtk(opts): locates the `rtk` binary via PATH; returns false
5
- // (never throws) when missing or on any exec error.
6
- // • initRtk(opts): runs `rtk init -g`; only call after detectRtk() true.
7
-
8
- const { test } = require('node:test');
9
- const assert = require('node:assert/strict');
10
- const fs = require('node:fs');
11
- const os = require('node:os');
12
- const path = require('node:path');
13
-
14
- const rtk = require('../rtk-detect.cjs');
15
-
16
- function mkEmptyBinDir() {
17
- return fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-rtk-empty-bin-'));
18
- }
19
-
20
- function mkFakeBinDir({ succeed = true } = {}) {
21
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-rtk-fake-bin-'));
22
- const script = succeed
23
- ? '#!/usr/bin/env bash\nif [ "$1" = "--version" ]; then echo "rtk 1.2.3"; exit 0; fi\nif [ "$1" = "init" ]; then echo "init ok"; exit 0; fi\nexit 0\n'
24
- : '#!/usr/bin/env bash\nexit 42\n';
25
- const target = path.join(dir, 'rtk');
26
- fs.writeFileSync(target, script);
27
- fs.chmodSync(target, 0o755);
28
- return dir;
29
- }
30
-
31
- test('detectRtk returns true when rtk --version succeeds', () => {
32
- const binDir = mkFakeBinDir({ succeed: true });
33
- try {
34
- const result = rtk.detectRtk({ pathOverride: binDir });
35
- assert.equal(result.available, true);
36
- assert.match(result.version, /rtk/i);
37
- } finally {
38
- fs.rmSync(binDir, { recursive: true, force: true });
39
- }
40
- });
41
-
42
- test('detectRtk returns false when rtk missing from PATH (no throw)', () => {
43
- const binDir = mkEmptyBinDir();
44
- try {
45
- const result = rtk.detectRtk({ pathOverride: binDir });
46
- assert.equal(result.available, false);
47
- assert.ok(result.reason);
48
- } finally {
49
- fs.rmSync(binDir, { recursive: true, force: true });
50
- }
51
- });
52
-
53
- test('detectRtk returns false when rtk exits non-zero (no throw)', () => {
54
- const binDir = mkFakeBinDir({ succeed: false });
55
- try {
56
- const result = rtk.detectRtk({ pathOverride: binDir });
57
- assert.equal(result.available, false);
58
- } finally {
59
- fs.rmSync(binDir, { recursive: true, force: true });
60
- }
61
- });
62
-
63
- test('initRtk runs rtk init -g and returns success', () => {
64
- const binDir = mkFakeBinDir({ succeed: true });
65
- try {
66
- const result = rtk.initRtk({ pathOverride: binDir, scope: 'global' });
67
- assert.equal(result.ok, true);
68
- assert.match(result.stdout, /init ok/);
69
- } finally {
70
- fs.rmSync(binDir, { recursive: true, force: true });
71
- }
72
- });
73
-
74
- test('initRtk reports failure when binary is missing (does not throw)', () => {
75
- const binDir = mkEmptyBinDir();
76
- try {
77
- const result = rtk.initRtk({ pathOverride: binDir, scope: 'global' });
78
- assert.equal(result.ok, false);
79
- assert.ok(result.error);
80
- } finally {
81
- fs.rmSync(binDir, { recursive: true, force: true });
82
- }
83
- });
84
-
85
- test('installHook: skip=true short-circuits with a skipped=true record', () => {
86
- const binDir = mkEmptyBinDir();
87
- try {
88
- const result = rtk.installHook({ skip: true, pathOverride: binDir });
89
- assert.equal(result.skipped, true);
90
- assert.equal(result.available, undefined);
91
- } finally {
92
- fs.rmSync(binDir, { recursive: true, force: true });
93
- }
94
- });
95
-
96
- test('installHook: missing rtk → returns {available:false} with install hint', () => {
97
- const binDir = mkEmptyBinDir();
98
- try {
99
- const result = rtk.installHook({ skip: false, pathOverride: binDir });
100
- assert.equal(result.available, false);
101
- assert.ok(result.hint, 'should surface an install hint');
102
- } finally {
103
- fs.rmSync(binDir, { recursive: true, force: true });
104
- }
105
- });
106
-
107
- test('installHook: present rtk → runs init and returns {available:true, initialized:true}', () => {
108
- const binDir = mkFakeBinDir({ succeed: true });
109
- try {
110
- const result = rtk.installHook({ skip: false, pathOverride: binDir, scope: 'global' });
111
- assert.equal(result.available, true);
112
- assert.equal(result.initialized, true);
113
- } finally {
114
- fs.rmSync(binDir, { recursive: true, force: true });
115
- }
116
- });
@@ -1,204 +0,0 @@
1
- 'use strict';
2
-
3
- /**
4
- * Gemini CLI adapter — Phase 4.5 installer (extension-packaged).
5
- *
6
- * Target config dirs:
7
- * global: $GEMINI_CONFIG_DIR → ~/.gemini
8
- * local: <cwd>/.gemini
9
- *
10
- * Layout — everything is packaged as a Gemini extension under
11
- * <target>/extensions/ultra-builder-pro/:
12
- *
13
- * gemini-extension.json → extension manifest (mcpServers declared here)
14
- * commands/*.toml → generated from commands/*.md via md-to-toml
15
- * skills/** → Ultra-owned allowlist (Gemini reads these as prompt includes)
16
- * GEMINI.md → canonical agent context (docs/AGENT-CONTEXT.md copy)
17
- * hooks/ → NOT installed; Gemini has no hook surface.
18
- * prompt-guard downgrade is described inside GEMINI.md.
19
- *
20
- * Scope boundaries (documented):
21
- * - Gemini has no hook event loop; runtime-enforced guards degrade to
22
- * prompt-level guidance (hooks/adapters/gemini.py explains).
23
- * - Subagent registration requires the upstream spec to settle — Phase 4.5
24
- * spike captures whatever shape lands.
25
- */
26
-
27
- const fs = require('node:fs');
28
- const os = require('node:os');
29
- const path = require('node:path');
30
-
31
- const {
32
- copyTree,
33
- writeAtomic,
34
- ensureDir,
35
- removeTree,
36
- } = require('./_shared/file-ops.cjs');
37
- const { mdCommandToToml } = require('./_shared/md-to-toml.cjs');
38
- const { buildMcpRuntime } = require('./_shared/codex-assets.cjs');
39
- const { skillsForRuntime } = require('./_shared/runtime-assets.cjs');
40
-
41
- const EXTENSION_NAME = 'ultra-builder-pro';
42
- const MCP_SERVER_NAME = 'ultra-builder-pro';
43
- const SOURCE_TAG = 'ubp';
44
-
45
- function resolveTarget(ctx) {
46
- if (ctx.configDir) return ctx.configDir;
47
- if (ctx.scope === 'global') {
48
- return process.env.GEMINI_CONFIG_DIR || path.join(ctx.homeDir || os.homedir(), '.gemini');
49
- }
50
- return path.join(ctx.cwd || process.cwd(), '.gemini');
51
- }
52
-
53
- function resolveExtensionRoot(ctx) {
54
- return path.join(resolveTarget(ctx), 'extensions', EXTENSION_NAME);
55
- }
56
-
57
- function resolveRepoRoot(ctx) {
58
- return ctx.repoRoot || path.resolve(__dirname, '..');
59
- }
60
-
61
- function loadPkgVersion(repoRoot) {
62
- try {
63
- return JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8')).version;
64
- } catch (err) {
65
- // Fall back to 0.0.0 but surface the reason — silent swallow hid a
66
- // real "package.json moved" regression in a Phase 4 code review.
67
- process.stderr.write(`gemini adapter: loadPkgVersion fell back to 0.0.0 (${err.message})\n`);
68
- return '0.0.0';
69
- }
70
- }
71
-
72
- function buildManifest(repoRoot, runtime) {
73
- return {
74
- name: EXTENSION_NAME,
75
- version: loadPkgVersion(repoRoot),
76
- description: 'Ultra Builder Pro — skill/MCP/CLI agent engineering system (Gemini packaging).',
77
- // Sibling identification block — outside env so it cannot leak into the
78
- // MCP server's child process (P2 #9). Uninstall reads manifest._ubp.source.
79
- _ubp: { source: SOURCE_TAG },
80
- mcpServers: {
81
- [MCP_SERVER_NAME]: {
82
- command: process.execPath,
83
- args: [runtime.launcher],
84
- _ubp: { source: SOURCE_TAG },
85
- },
86
- },
87
- contextFileName: 'GEMINI.md',
88
- };
89
- }
90
-
91
- function ownsExtension(extensionRoot) {
92
- const manifestFile = path.join(extensionRoot, 'gemini-extension.json');
93
- if (!fs.existsSync(manifestFile)) return false;
94
- try {
95
- const manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf8'));
96
- return manifest?._ubp?.source === SOURCE_TAG;
97
- } catch (_error) {
98
- return false;
99
- }
100
- }
101
-
102
- function install(ctx) {
103
- const extensionRoot = resolveExtensionRoot(ctx);
104
- const repoRoot = resolveRepoRoot(ctx);
105
- if (fs.existsSync(extensionRoot)) {
106
- const entries = fs.readdirSync(extensionRoot);
107
- if (entries.length > 0 && !ownsExtension(extensionRoot)) {
108
- throw new Error(`refusing to replace unmanaged Gemini extension: ${extensionRoot}`);
109
- }
110
- if (entries.length > 0) {
111
- for (const managed of ['skills', 'commands', 'runtime', 'spec', 'templates']) {
112
- removeTree(path.join(extensionRoot, managed));
113
- }
114
- for (const managed of ['GEMINI.md', 'gemini-extension.json']) {
115
- const file = path.join(extensionRoot, managed);
116
- if (fs.existsSync(file)) fs.unlinkSync(file);
117
- }
118
- }
119
- }
120
- ensureDir(extensionRoot);
121
-
122
- const report = { target: extensionRoot, copied: {}, config: { updated: false } };
123
-
124
- // 1. Skills — copy only the canonical Ultra-owned allowlist.
125
- const skillsSrc = path.join(repoRoot, 'skills');
126
- report.copied.skills = [];
127
- for (const name of skillsForRuntime('gemini')) {
128
- const source = path.join(skillsSrc, name);
129
- if (!fs.existsSync(path.join(source, 'SKILL.md'))) {
130
- throw new Error(`missing allowlisted Gemini skill: ${name}`);
131
- }
132
- const files = copyTree(source, path.join(extensionRoot, 'skills', name));
133
- report.copied.skills.push(...files.map((file) => path.join(name, file)));
134
- }
135
-
136
- // 2. Commands — md → toml
137
- const commandsSrc = path.join(repoRoot, 'commands');
138
- if (fs.existsSync(commandsSrc)) {
139
- const dstCommands = path.join(extensionRoot, 'commands');
140
- ensureDir(dstCommands);
141
- const tomlFiles = [];
142
- for (const f of fs.readdirSync(commandsSrc)) {
143
- if (!f.endsWith('.md')) continue;
144
- const md = fs.readFileSync(path.join(commandsSrc, f), 'utf8');
145
- const tomlName = f.replace(/\.md$/, '.toml');
146
- writeAtomic(path.join(dstCommands, tomlName), mdCommandToToml(md));
147
- tomlFiles.push(tomlName);
148
- }
149
- report.copied.commands = tomlFiles;
150
- }
151
-
152
- // 3. GEMINI.md — copy docs/AGENT-CONTEXT.md into the extension context file
153
- const agentContext = path.join(repoRoot, 'docs', 'AGENT-CONTEXT.md');
154
- if (fs.existsSync(agentContext)) {
155
- const dst = path.join(extensionRoot, 'GEMINI.md');
156
- fs.copyFileSync(agentContext, dst);
157
- report.copied.context = 'GEMINI.md';
158
- }
159
-
160
- // 4. Manifest
161
- const runtime = buildMcpRuntime(repoRoot, extensionRoot, { runtime: 'gemini' });
162
- const manifest = buildManifest(repoRoot, runtime);
163
- writeAtomic(
164
- path.join(extensionRoot, 'gemini-extension.json'),
165
- JSON.stringify(manifest, null, 2) + '\n',
166
- );
167
- report.config.updated = true;
168
- return report;
169
- }
170
-
171
- function uninstall(ctx) {
172
- const extensionRoot = resolveExtensionRoot(ctx);
173
- const report = { target: extensionRoot, removed: {}, config: { updated: false } };
174
- if (fs.existsSync(extensionRoot)) {
175
- // Manifest carries the _ubp sibling block — if tampered with, bail loud.
176
- const manifestFile = path.join(extensionRoot, 'gemini-extension.json');
177
- if (fs.existsSync(manifestFile)) {
178
- try {
179
- const manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf8'));
180
- if (!manifest._ubp || manifest._ubp.source !== SOURCE_TAG) {
181
- throw new Error(`refusing to uninstall: ${manifestFile} was not generated by this adapter`);
182
- }
183
- } catch (err) {
184
- if (err.message.startsWith('refusing')) throw err;
185
- // malformed manifest: proceed — we still own the directory name
186
- }
187
- }
188
- removeTree(extensionRoot);
189
- report.removed.extension = true;
190
- report.config.updated = true;
191
- }
192
- return report;
193
- }
194
-
195
- module.exports = {
196
- name: 'gemini',
197
- EXTENSION_NAME,
198
- MCP_SERVER_NAME,
199
- SOURCE_TAG,
200
- resolveTarget,
201
- resolveExtensionRoot,
202
- install,
203
- uninstall,
204
- };