nubos-pilot 0.1.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/agents/np-ai-researcher.md +140 -0
- package/agents/np-code-fixer.md +363 -0
- package/agents/np-code-reviewer.md +351 -0
- package/agents/np-domain-researcher.md +136 -0
- package/agents/np-eval-auditor.md +167 -0
- package/agents/np-eval-planner.md +153 -0
- package/agents/np-executor.md +72 -0
- package/agents/np-framework-selector.md +171 -0
- package/agents/np-nyquist-auditor.md +185 -0
- package/agents/np-plan-checker.md +165 -0
- package/agents/np-planner.md +199 -0
- package/agents/np-researcher.md +150 -0
- package/agents/np-security-auditor.md +206 -0
- package/agents/np-ui-auditor.md +369 -0
- package/agents/np-ui-checker.md +192 -0
- package/agents/np-ui-researcher.md +324 -0
- package/agents/np-verifier.md +79 -0
- package/bin/check-coverage.cjs +40 -0
- package/bin/check-workflows.cjs +171 -0
- package/bin/check-workflows.test.cjs +208 -0
- package/bin/install.js +500 -0
- package/bin/np-tools/_commands.cjs +70 -0
- package/bin/np-tools/add-tests.cjs +171 -0
- package/bin/np-tools/add-tests.test.cjs +122 -0
- package/bin/np-tools/add-todo.cjs +108 -0
- package/bin/np-tools/add-todo.test.cjs +112 -0
- package/bin/np-tools/agent-skills.cjs +14 -0
- package/bin/np-tools/agent-skills.test.cjs +42 -0
- package/bin/np-tools/ai-integration-phase.cjs +109 -0
- package/bin/np-tools/ai-integration-phase.test.cjs +123 -0
- package/bin/np-tools/askuser.cjs +53 -0
- package/bin/np-tools/askuser.test.cjs +49 -0
- package/bin/np-tools/autonomous.cjs +69 -0
- package/bin/np-tools/autonomous.test.cjs +74 -0
- package/bin/np-tools/checkpoint.cjs +101 -0
- package/bin/np-tools/checkpoint.test.cjs +119 -0
- package/bin/np-tools/code-review.cjs +133 -0
- package/bin/np-tools/code-review.test.cjs +96 -0
- package/bin/np-tools/commit-task.cjs +120 -0
- package/bin/np-tools/commit-task.test.cjs +160 -0
- package/bin/np-tools/commit.cjs +103 -0
- package/bin/np-tools/commit.test.cjs +93 -0
- package/bin/np-tools/config.cjs +101 -0
- package/bin/np-tools/config.test.cjs +71 -0
- package/bin/np-tools/discuss-phase-power.cjs +265 -0
- package/bin/np-tools/discuss-phase-power.test.cjs +242 -0
- package/bin/np-tools/discuss-phase.cjs +132 -0
- package/bin/np-tools/discuss-phase.test.cjs +148 -0
- package/bin/np-tools/dispatch.cjs +116 -0
- package/bin/np-tools/doctor.cjs +242 -0
- package/bin/np-tools/eval-review.cjs +116 -0
- package/bin/np-tools/eval-review.test.cjs +123 -0
- package/bin/np-tools/execute-phase.cjs +182 -0
- package/bin/np-tools/execute-phase.test.cjs +116 -0
- package/bin/np-tools/execute-plan.cjs +124 -0
- package/bin/np-tools/execute-plan.test.cjs +82 -0
- package/bin/np-tools/help.cjs +28 -0
- package/bin/np-tools/help.test.cjs +29 -0
- package/bin/np-tools/init-dispatch.test.cjs +91 -0
- package/bin/np-tools/metrics.cjs +97 -0
- package/bin/np-tools/metrics.test.cjs +188 -0
- package/bin/np-tools/new-milestone.cjs +288 -0
- package/bin/np-tools/new-milestone.test.cjs +166 -0
- package/bin/np-tools/new-project.cjs +284 -0
- package/bin/np-tools/new-project.test.cjs +165 -0
- package/bin/np-tools/next.cjs +7 -0
- package/bin/np-tools/next.test.cjs +30 -0
- package/bin/np-tools/park.cjs +48 -0
- package/bin/np-tools/park.test.cjs +50 -0
- package/bin/np-tools/pause-work.cjs +24 -0
- package/bin/np-tools/pause-work.test.cjs +74 -0
- package/bin/np-tools/phase.cjs +71 -0
- package/bin/np-tools/phase.test.cjs +81 -0
- package/bin/np-tools/plan-diff.cjs +57 -0
- package/bin/np-tools/plan-diff.test.cjs +134 -0
- package/bin/np-tools/plan-milestone-gaps.cjs +115 -0
- package/bin/np-tools/plan-milestone-gaps.test.cjs +122 -0
- package/bin/np-tools/plan-phase.cjs +350 -0
- package/bin/np-tools/plan-phase.test.cjs +263 -0
- package/bin/np-tools/progress.cjs +7 -0
- package/bin/np-tools/progress.test.cjs +44 -0
- package/bin/np-tools/queue.cjs +213 -0
- package/bin/np-tools/research-phase.cjs +144 -0
- package/bin/np-tools/research-phase.test.cjs +154 -0
- package/bin/np-tools/reset-slice.cjs +17 -0
- package/bin/np-tools/reset-slice.test.cjs +96 -0
- package/bin/np-tools/resolve-model.cjs +110 -0
- package/bin/np-tools/resolve-model.test.cjs +200 -0
- package/bin/np-tools/resume-work.cjs +76 -0
- package/bin/np-tools/resume-work.test.cjs +91 -0
- package/bin/np-tools/skip.cjs +48 -0
- package/bin/np-tools/skip.test.cjs +66 -0
- package/bin/np-tools/slug.cjs +34 -0
- package/bin/np-tools/slug.test.cjs +46 -0
- package/bin/np-tools/state.cjs +16 -0
- package/bin/np-tools/state.test.cjs +40 -0
- package/bin/np-tools/stats.cjs +151 -0
- package/bin/np-tools/stats.test.cjs +118 -0
- package/bin/np-tools/triage.cjs +128 -0
- package/bin/np-tools/ui-phase.cjs +108 -0
- package/bin/np-tools/ui-phase.test.cjs +121 -0
- package/bin/np-tools/ui-review.cjs +108 -0
- package/bin/np-tools/ui-review.test.cjs +120 -0
- package/bin/np-tools/undo-task.cjs +31 -0
- package/bin/np-tools/undo-task.test.cjs +117 -0
- package/bin/np-tools/undo.cjs +43 -0
- package/bin/np-tools/undo.test.cjs +120 -0
- package/bin/np-tools/unpark.cjs +48 -0
- package/bin/np-tools/unpark.test.cjs +50 -0
- package/bin/np-tools/verify-work.cjs +186 -0
- package/bin/np-tools/verify-work.test.cjs +97 -0
- package/docs/adr/0001-no-daemon-invariant.md +82 -0
- package/docs/adr/0002-zero-runtime-dependencies.md +90 -0
- package/docs/adr/0003-max-six-unit-types.md +85 -0
- package/docs/adr/0004-atomic-commit-per-unit.md +102 -0
- package/docs/adr/0005-three-orthogonal-file-trees.md +98 -0
- package/docs/adr/0006-yaml-dependency-amendment.md +60 -0
- package/docs/adr/README.md +27 -0
- package/docs/agent-frontmatter-schema.md +84 -0
- package/docs/phase-artifact-schemas.md +292 -0
- package/docs/phase-directory-layout.md +82 -0
- package/lib/__tests__/README.md +1 -0
- package/lib/agents.cjs +98 -0
- package/lib/agents.test.cjs +286 -0
- package/lib/askuser.cjs +36 -0
- package/lib/askuser.test.cjs +310 -0
- package/lib/checkpoint.cjs +135 -0
- package/lib/checkpoint.test.cjs +184 -0
- package/lib/core.cjs +165 -0
- package/lib/core.test.cjs +405 -0
- package/lib/fixtures/README.md +1 -0
- package/lib/fixtures/phase-tree/README.md +1 -0
- package/lib/fixtures/plans/cycle/PLAN.md +16 -0
- package/lib/fixtures/plans/cycle/tasks/T-01.md +20 -0
- package/lib/fixtures/plans/cycle/tasks/T-02.md +20 -0
- package/lib/fixtures/plans/cycle/tasks/T-03.md +20 -0
- package/lib/fixtures/plans/linear/PLAN.md +16 -0
- package/lib/fixtures/plans/linear/tasks/T-01.md +20 -0
- package/lib/fixtures/plans/linear/tasks/T-02.md +20 -0
- package/lib/fixtures/plans/linear/tasks/T-03.md +20 -0
- package/lib/fixtures/plans/parallel/PLAN.md +16 -0
- package/lib/fixtures/plans/parallel/tasks/T-01.md +20 -0
- package/lib/fixtures/plans/parallel/tasks/T-02.md +20 -0
- package/lib/fixtures/plans/parallel/tasks/T-03.md +20 -0
- package/lib/fixtures/plans/wave-conflict/PLAN.md +16 -0
- package/lib/fixtures/plans/wave-conflict/tasks/T-01.md +20 -0
- package/lib/fixtures/plans/wave-conflict/tasks/T-02.md +20 -0
- package/lib/fixtures/roadmap/ROADMAP-malformed.md +3 -0
- package/lib/fixtures/roadmap/ROADMAP-minimal.md +51 -0
- package/lib/fixtures/roadmap/roadmap-malformed.yaml +7 -0
- package/lib/fixtures/roadmap/roadmap-minimal.yaml +40 -0
- package/lib/fixtures/roadmap/roadmap-ten-phases.yaml +101 -0
- package/lib/fixtures/templates/phase-context.md +6 -0
- package/lib/fixtures/templates/plan-skeleton.md +6 -0
- package/lib/frontmatter.cjs +251 -0
- package/lib/frontmatter.test.cjs +177 -0
- package/lib/gaps.cjs +197 -0
- package/lib/gaps.test.cjs +200 -0
- package/lib/git.cjs +207 -0
- package/lib/git.test.cjs +305 -0
- package/lib/install/agents-md.cjs +77 -0
- package/lib/install/backup.cjs +70 -0
- package/lib/install/codex-toml.cjs +440 -0
- package/lib/install/managed-block.cjs +30 -0
- package/lib/install/manifest.cjs +148 -0
- package/lib/install/mcp-writer.cjs +127 -0
- package/lib/install/runtime-detect.cjs +44 -0
- package/lib/install/staging.cjs +149 -0
- package/lib/metrics-aggregate.cjs +229 -0
- package/lib/metrics-aggregate.test.cjs +192 -0
- package/lib/metrics.cjs +120 -0
- package/lib/metrics.test.cjs +182 -0
- package/lib/model-aliases.regression.test.cjs +16 -0
- package/lib/model-profiles.cjs +42 -0
- package/lib/model-profiles.test.cjs +61 -0
- package/lib/next.cjs +236 -0
- package/lib/next.test.cjs +194 -0
- package/lib/phase.cjs +95 -0
- package/lib/phase.test.cjs +189 -0
- package/lib/plan-checker-contract.test.cjs +72 -0
- package/lib/plan-diff.cjs +173 -0
- package/lib/plan-diff.test.cjs +217 -0
- package/lib/plan.cjs +85 -0
- package/lib/plan.test.cjs +263 -0
- package/lib/progress.cjs +95 -0
- package/lib/progress.test.cjs +116 -0
- package/lib/researcher-contract.test.cjs +61 -0
- package/lib/roadmap-render.cjs +206 -0
- package/lib/roadmap-render.test.cjs +121 -0
- package/lib/roadmap.cjs +416 -0
- package/lib/roadmap.test.cjs +371 -0
- package/lib/runtime/_contract.test.cjs +61 -0
- package/lib/runtime/_readline.cjs +119 -0
- package/lib/runtime/_readline.test.cjs +126 -0
- package/lib/runtime/claude.cjs +48 -0
- package/lib/runtime/claude.test.cjs +101 -0
- package/lib/runtime/codex.cjs +35 -0
- package/lib/runtime/codex.test.cjs +114 -0
- package/lib/runtime/gemini.cjs +35 -0
- package/lib/runtime/gemini.test.cjs +109 -0
- package/lib/runtime/index.cjs +49 -0
- package/lib/runtime/index.test.cjs +181 -0
- package/lib/runtime/opencode.cjs +35 -0
- package/lib/runtime/opencode.test.cjs +124 -0
- package/lib/state.cjs +205 -0
- package/lib/state.test.cjs +264 -0
- package/lib/surface-audit.test.cjs +46 -0
- package/lib/tasks.cjs +327 -0
- package/lib/tasks.test.cjs +389 -0
- package/lib/template.cjs +66 -0
- package/lib/template.test.cjs +159 -0
- package/lib/undo.cjs +179 -0
- package/lib/undo.test.cjs +261 -0
- package/lib/verify.cjs +116 -0
- package/lib/verify.test.cjs +187 -0
- package/np-tools.cjs +303 -0
- package/package.json +39 -0
- package/templates/AI-SPEC.md +90 -0
- package/templates/CONTEXT.md +32 -0
- package/templates/PLAN.md +69 -0
- package/templates/PROJECT.md +60 -0
- package/templates/REQUIREMENTS.md +38 -0
- package/templates/SECURITY.md +61 -0
- package/templates/UI-SPEC.md +64 -0
- package/templates/VALIDATION.md +76 -0
- package/templates/claude/payload/README.md +11 -0
- package/templates/opencode/opencode.json +6 -0
- package/templates/opencode/payload/AGENTS.md +9 -0
- package/workflows/add-backlog.md +212 -0
- package/workflows/add-tests.md +69 -0
- package/workflows/add-todo.md +222 -0
- package/workflows/ai-integration-phase.md +230 -0
- package/workflows/autonomous.md +94 -0
- package/workflows/cleanup.md +325 -0
- package/workflows/code-review-fix.md +435 -0
- package/workflows/code-review.md +447 -0
- package/workflows/discuss-phase-assumptions.md +269 -0
- package/workflows/discuss-phase-power.md +139 -0
- package/workflows/discuss-phase.md +386 -0
- package/workflows/dispatch.md +9 -0
- package/workflows/doctor.md +10 -0
- package/workflows/eval-review.md +243 -0
- package/workflows/execute-phase.md +142 -0
- package/workflows/execute-plan.md +82 -0
- package/workflows/help.md +8 -0
- package/workflows/new-milestone.md +166 -0
- package/workflows/new-project.md +213 -0
- package/workflows/next.md +8 -0
- package/workflows/note.md +244 -0
- package/workflows/park.md +29 -0
- package/workflows/pause-work.md +34 -0
- package/workflows/plan-milestone-gaps.md +233 -0
- package/workflows/plan-phase.md +351 -0
- package/workflows/progress.md +8 -0
- package/workflows/queue.md +9 -0
- package/workflows/research-phase.md +327 -0
- package/workflows/reset-slice.md +39 -0
- package/workflows/resume-work.md +79 -0
- package/workflows/review.md +489 -0
- package/workflows/secure-phase.md +209 -0
- package/workflows/session-report.md +243 -0
- package/workflows/skip.md +29 -0
- package/workflows/state.md +7 -0
- package/workflows/stats.md +170 -0
- package/workflows/thread.md +214 -0
- package/workflows/triage.md +9 -0
- package/workflows/ui-phase.md +246 -0
- package/workflows/ui-review.md +222 -0
- package/workflows/undo-task.md +42 -0
- package/workflows/undo.md +55 -0
- package/workflows/unpark.md +29 -0
- package/workflows/validate-phase.md +231 -0
- package/workflows/verify-work.md +83 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const { _readOneLine, _parseAnswer } = require('./_readline.cjs');
|
|
2
|
+
|
|
3
|
+
function _emitClaudeMarkerBlock({ type, question, options, def }) {
|
|
4
|
+
const payload = {
|
|
5
|
+
type,
|
|
6
|
+
question,
|
|
7
|
+
options: options || null,
|
|
8
|
+
default: def === undefined ? null : def,
|
|
9
|
+
};
|
|
10
|
+
const block = '<!-- askUser v1 -->\n<!-- ' + JSON.stringify(payload) + ' -->\n';
|
|
11
|
+
process.stdout.write(block);
|
|
12
|
+
return _readOneLine();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function askUser(spec) {
|
|
16
|
+
const type = spec && spec.type;
|
|
17
|
+
const question = spec && spec.question;
|
|
18
|
+
const options = spec && spec.options;
|
|
19
|
+
const def = spec ? spec.default : undefined;
|
|
20
|
+
const line = await _emitClaudeMarkerBlock({ type, question, options, def });
|
|
21
|
+
return { value: _parseAnswer(type, line, options, def), source: 'askUserQuestion' };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = {
|
|
25
|
+
name: 'claude',
|
|
26
|
+
detectHints: {
|
|
27
|
+
env: ['CLAUDECODE', 'CLAUDE_CODE_ENTRYPOINT'],
|
|
28
|
+
pathBinary: 'claude',
|
|
29
|
+
diskMarkers: ['.claude/'],
|
|
30
|
+
},
|
|
31
|
+
capabilities: {
|
|
32
|
+
askUserQuestion: true,
|
|
33
|
+
slashCommands: true,
|
|
34
|
+
agentsMd: 'CLAUDE.md',
|
|
35
|
+
textMode: 'off',
|
|
36
|
+
modelResolution: 'profile',
|
|
37
|
+
},
|
|
38
|
+
paths: {
|
|
39
|
+
payload: '.claude/nubos-pilot/',
|
|
40
|
+
commands: '.claude/commands/',
|
|
41
|
+
agents: '.claude/agents/',
|
|
42
|
+
agentsMd: 'CLAUDE.md',
|
|
43
|
+
},
|
|
44
|
+
runtimeNotice:
|
|
45
|
+
'> **Runtime-Hinweis:** Diese Datei wird von Claude Code konsumiert. '
|
|
46
|
+
+ 'Interaktive Prompts laufen über Claudes native Frage-Dialog (marker block auf stdout).',
|
|
47
|
+
askUser,
|
|
48
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
const { test } = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const { _setReadlineImplForTests } = require('./_readline.cjs');
|
|
4
|
+
const claude = require('./claude.cjs');
|
|
5
|
+
|
|
6
|
+
function captureStdout(fn) {
|
|
7
|
+
const chunks = [];
|
|
8
|
+
const orig = process.stdout.write.bind(process.stdout);
|
|
9
|
+
process.stdout.write = (chunk) => { chunks.push(chunk.toString()); return true; };
|
|
10
|
+
return Promise.resolve(fn()).then(
|
|
11
|
+
(val) => { process.stdout.write = orig; return { val, out: chunks.join('') }; },
|
|
12
|
+
(err) => { process.stdout.write = orig; throw err; },
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
test('claude-adapter: exports five-key contract', () => {
|
|
17
|
+
for (const k of ['name', 'detectHints', 'capabilities', 'paths', 'askUser']) {
|
|
18
|
+
assert.ok(k in claude, 'missing ' + k);
|
|
19
|
+
}
|
|
20
|
+
assert.equal(claude.name, 'claude');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('claude-adapter: capabilities match D-07 spec', () => {
|
|
24
|
+
const c = claude.capabilities;
|
|
25
|
+
assert.equal(c.askUserQuestion, true);
|
|
26
|
+
assert.equal(c.slashCommands, true);
|
|
27
|
+
assert.equal(c.agentsMd, 'CLAUDE.md');
|
|
28
|
+
assert.equal(c.textMode, 'off');
|
|
29
|
+
assert.equal(c.modelResolution, 'profile');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('claude-adapter: askUser emits askUser v1 marker block', async () => {
|
|
33
|
+
_setReadlineImplForTests(async () => 'chosen');
|
|
34
|
+
try {
|
|
35
|
+
const { val, out } = await captureStdout(() =>
|
|
36
|
+
claude.askUser({ type: 'input', question: 'Q' })
|
|
37
|
+
);
|
|
38
|
+
assert.match(out, /<!-- askUser v1 -->/);
|
|
39
|
+
assert.match(out, /<!-- \{"type":"input"/);
|
|
40
|
+
assert.equal(val.source, 'askUserQuestion');
|
|
41
|
+
assert.equal(val.value, 'chosen');
|
|
42
|
+
} finally {
|
|
43
|
+
_setReadlineImplForTests(null);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('claude-adapter: select with injected index returns option', async () => {
|
|
48
|
+
_setReadlineImplForTests(async () => '2');
|
|
49
|
+
try {
|
|
50
|
+
const { val } = await captureStdout(() =>
|
|
51
|
+
claude.askUser({ type: 'select', question: 'P', options: ['A', 'B', 'C'] })
|
|
52
|
+
);
|
|
53
|
+
assert.equal(val.value, 'B');
|
|
54
|
+
assert.equal(val.source, 'askUserQuestion');
|
|
55
|
+
} finally {
|
|
56
|
+
_setReadlineImplForTests(null);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('claude-adapter: confirm y/n parsing', async () => {
|
|
61
|
+
_setReadlineImplForTests(async () => 'y');
|
|
62
|
+
try {
|
|
63
|
+
const { val } = await captureStdout(() =>
|
|
64
|
+
claude.askUser({ type: 'confirm', question: 'OK' })
|
|
65
|
+
);
|
|
66
|
+
assert.equal(val.value, true);
|
|
67
|
+
} finally {
|
|
68
|
+
_setReadlineImplForTests(null);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('claude-adapter: marker JSON payload has exact key order', async () => {
|
|
73
|
+
_setReadlineImplForTests(async () => 'x');
|
|
74
|
+
try {
|
|
75
|
+
const { out } = await captureStdout(() =>
|
|
76
|
+
claude.askUser({ type: 'input', question: 'Q', options: ['a'], default: 'd' })
|
|
77
|
+
);
|
|
78
|
+
assert.match(out, /"type":"input","question":"Q","options":\["a"\],"default":"d"/);
|
|
79
|
+
} finally {
|
|
80
|
+
_setReadlineImplForTests(null);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('claude-adapter: does not import node:readline directly', () => {
|
|
85
|
+
const src = require('fs').readFileSync(require.resolve('./claude.cjs'), 'utf-8');
|
|
86
|
+
assert.ok(
|
|
87
|
+
!/require\(['"]node:readline['"]\)/.test(src),
|
|
88
|
+
'claude.cjs must delegate to _readline.cjs, not require node:readline',
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('claude-adapter: exports runtimeNotice compatible with agents-md SC-5 check', () => {
|
|
93
|
+
const notice = claude.runtimeNotice;
|
|
94
|
+
assert.equal(typeof notice, 'string');
|
|
95
|
+
assert.ok(notice.length > 0);
|
|
96
|
+
assert.match(notice, /readline|prompt/i, 'runtimeNotice must match /readline|prompt/i');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('claude-adapter: runtimeNotice does not contain the forbidden joined Claude-tool literal (SC-5 guard)', () => {
|
|
100
|
+
assert.ok(!/Ask-User-Question/.test(claude.runtimeNotice));
|
|
101
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const { askUserReadline } = require('./_readline.cjs');
|
|
2
|
+
|
|
3
|
+
async function askUser(spec) {
|
|
4
|
+
return askUserReadline({
|
|
5
|
+
type: spec && spec.type,
|
|
6
|
+
question: spec && spec.question,
|
|
7
|
+
options: spec && spec.options,
|
|
8
|
+
def: spec ? spec.default : undefined,
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
module.exports = {
|
|
13
|
+
name: 'codex',
|
|
14
|
+
detectHints: {
|
|
15
|
+
env: ['CODEX_HOME', 'CODEX_VERSION'],
|
|
16
|
+
pathBinary: 'codex',
|
|
17
|
+
diskMarkers: ['.codex/'],
|
|
18
|
+
},
|
|
19
|
+
capabilities: {
|
|
20
|
+
askUserQuestion: false,
|
|
21
|
+
slashCommands: false,
|
|
22
|
+
agentsMd: 'AGENTS.md',
|
|
23
|
+
textMode: 'auto',
|
|
24
|
+
modelResolution: 'profile',
|
|
25
|
+
},
|
|
26
|
+
paths: {
|
|
27
|
+
payload: null,
|
|
28
|
+
config: null,
|
|
29
|
+
agentsMd: 'AGENTS.md',
|
|
30
|
+
},
|
|
31
|
+
runtimeNotice:
|
|
32
|
+
'> **Runtime-Hinweis:** Diese Datei wird von Codex/Gemini/OpenCode konsumiert. '
|
|
33
|
+
+ 'Interaktive Prompts laufen über readline (stderr), nicht über das Claude-spezifische AskUser-Tool.',
|
|
34
|
+
askUser,
|
|
35
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
const { test } = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const { _setReadlineImplForTests } = require('./_readline.cjs');
|
|
5
|
+
const codex = require('./codex.cjs');
|
|
6
|
+
|
|
7
|
+
function captureStderr(fn) {
|
|
8
|
+
const chunks = [];
|
|
9
|
+
const orig = process.stderr.write.bind(process.stderr);
|
|
10
|
+
process.stderr.write = (chunk) => { chunks.push(chunk.toString()); return true; };
|
|
11
|
+
return Promise.resolve(fn()).then(
|
|
12
|
+
(val) => { process.stderr.write = orig; return { val, out: chunks.join('') }; },
|
|
13
|
+
(err) => { process.stderr.write = orig; throw err; },
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
test('codex-adapter: exports five-key contract', () => {
|
|
18
|
+
for (const k of ['name', 'detectHints', 'capabilities', 'paths', 'askUser']) {
|
|
19
|
+
assert.ok(k in codex, 'missing ' + k);
|
|
20
|
+
}
|
|
21
|
+
assert.equal(codex.name, 'codex');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('codex-adapter: capabilities match D-07 spec', () => {
|
|
25
|
+
const c = codex.capabilities;
|
|
26
|
+
assert.equal(c.askUserQuestion, false);
|
|
27
|
+
assert.equal(c.slashCommands, false);
|
|
28
|
+
assert.equal(c.agentsMd, 'AGENTS.md');
|
|
29
|
+
assert.equal(c.textMode, 'auto');
|
|
30
|
+
assert.equal(c.modelResolution, 'profile');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('codex-adapter: paths are null for payload/config (codex has no per-project tree)', () => {
|
|
34
|
+
assert.equal(codex.paths.payload, null);
|
|
35
|
+
assert.equal(codex.paths.config, null);
|
|
36
|
+
assert.equal(codex.paths.agentsMd, 'AGENTS.md');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('codex-adapter: readline select returns option', async () => {
|
|
40
|
+
_setReadlineImplForTests(async () => '1');
|
|
41
|
+
try {
|
|
42
|
+
const { val } = await captureStderr(() =>
|
|
43
|
+
codex.askUser({ type: 'select', question: 'P', options: ['A', 'B'] }),
|
|
44
|
+
);
|
|
45
|
+
assert.equal(val.value, 'A');
|
|
46
|
+
assert.equal(val.source, 'readline');
|
|
47
|
+
} finally {
|
|
48
|
+
_setReadlineImplForTests(null);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('codex-adapter: readline input returns line', async () => {
|
|
53
|
+
_setReadlineImplForTests(async () => 'hi');
|
|
54
|
+
try {
|
|
55
|
+
const { val } = await captureStderr(() =>
|
|
56
|
+
codex.askUser({ type: 'input', question: 'Q' }),
|
|
57
|
+
);
|
|
58
|
+
assert.equal(val.value, 'hi');
|
|
59
|
+
} finally {
|
|
60
|
+
_setReadlineImplForTests(null);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('codex-adapter: readline confirm yes/no', async () => {
|
|
65
|
+
_setReadlineImplForTests(async () => 'yes');
|
|
66
|
+
try {
|
|
67
|
+
const { val } = await captureStderr(() =>
|
|
68
|
+
codex.askUser({ type: 'confirm', question: 'OK' }),
|
|
69
|
+
);
|
|
70
|
+
assert.equal(val.value, true);
|
|
71
|
+
} finally {
|
|
72
|
+
_setReadlineImplForTests(null);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('codex-adapter: REGRESSION GUARD — does not import codex-toml or install-layer modules (D-20)', () => {
|
|
77
|
+
const src = fs.readFileSync(require.resolve('./codex.cjs'), 'utf-8');
|
|
78
|
+
assert.ok(
|
|
79
|
+
!/codex-toml/.test(src),
|
|
80
|
+
'D-20 violation: codex.cjs must not import lib/install/codex-toml.cjs — [features]-repair stays in install layer',
|
|
81
|
+
);
|
|
82
|
+
assert.ok(
|
|
83
|
+
!/require\(['"]\.\.\/install\//.test(src),
|
|
84
|
+
'codex.cjs must not reach into lib/install/',
|
|
85
|
+
);
|
|
86
|
+
assert.ok(
|
|
87
|
+
!/repairCodexFeatures/.test(src),
|
|
88
|
+
'codex.cjs must not reference repairCodexFeatures',
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('codex-adapter: does not import node:readline directly', () => {
|
|
93
|
+
const src = fs.readFileSync(require.resolve('./codex.cjs'), 'utf-8');
|
|
94
|
+
assert.ok(
|
|
95
|
+
!/require\(['"]node:readline['"]\)/.test(src),
|
|
96
|
+
'codex.cjs must delegate to _readline.cjs',
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('codex-adapter: exports runtimeNotice compatible with agents-md SC-5 check', () => {
|
|
101
|
+
const notice = codex.runtimeNotice;
|
|
102
|
+
assert.equal(typeof notice, 'string');
|
|
103
|
+
assert.ok(notice.length > 0);
|
|
104
|
+
assert.match(notice, /readline|prompt/i, 'runtimeNotice must match /readline|prompt/i');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('codex-adapter: runtimeNotice starts with the runtime-hint marker (default fallback)', () => {
|
|
108
|
+
assert.ok(codex.runtimeNotice.startsWith('> **Runtime-Hinweis:**'));
|
|
109
|
+
assert.ok(/readline/.test(codex.runtimeNotice));
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('codex-adapter: runtimeNotice does not contain the forbidden joined Claude-tool literal (SC-5 guard)', () => {
|
|
113
|
+
assert.ok(!/Ask-User-Question/.test(codex.runtimeNotice));
|
|
114
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const { askUserReadline } = require('./_readline.cjs');
|
|
2
|
+
|
|
3
|
+
async function askUser(spec) {
|
|
4
|
+
return askUserReadline({
|
|
5
|
+
type: spec && spec.type,
|
|
6
|
+
question: spec && spec.question,
|
|
7
|
+
options: spec && spec.options,
|
|
8
|
+
def: spec ? spec.default : undefined,
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
module.exports = {
|
|
13
|
+
name: 'gemini',
|
|
14
|
+
detectHints: {
|
|
15
|
+
env: ['GEMINI_CLI', 'GEMINI_VERSION'],
|
|
16
|
+
pathBinary: 'gemini',
|
|
17
|
+
diskMarkers: ['.gemini/'],
|
|
18
|
+
},
|
|
19
|
+
capabilities: {
|
|
20
|
+
askUserQuestion: false,
|
|
21
|
+
slashCommands: false,
|
|
22
|
+
agentsMd: 'GEMINI.md',
|
|
23
|
+
textMode: 'auto',
|
|
24
|
+
modelResolution: 'profile',
|
|
25
|
+
},
|
|
26
|
+
paths: {
|
|
27
|
+
payload: null,
|
|
28
|
+
config: null,
|
|
29
|
+
agentsMd: 'GEMINI.md',
|
|
30
|
+
},
|
|
31
|
+
runtimeNotice:
|
|
32
|
+
'> **Runtime-Hinweis:** Diese Datei (GEMINI.md) wird von der Gemini CLI konsumiert. '
|
|
33
|
+
+ 'Interaktive Prompts laufen über readline (stderr).',
|
|
34
|
+
askUser,
|
|
35
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
const { test } = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const { _setReadlineImplForTests } = require('./_readline.cjs');
|
|
5
|
+
const gemini = require('./gemini.cjs');
|
|
6
|
+
|
|
7
|
+
function captureStderr(fn) {
|
|
8
|
+
const chunks = [];
|
|
9
|
+
const orig = process.stderr.write.bind(process.stderr);
|
|
10
|
+
process.stderr.write = (chunk) => { chunks.push(chunk.toString()); return true; };
|
|
11
|
+
return Promise.resolve(fn()).then(
|
|
12
|
+
(val) => { process.stderr.write = orig; return { val, out: chunks.join('') }; },
|
|
13
|
+
(err) => { process.stderr.write = orig; throw err; },
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
test('gemini-adapter: exports five-key contract', () => {
|
|
18
|
+
for (const k of ['name', 'detectHints', 'capabilities', 'paths', 'askUser']) {
|
|
19
|
+
assert.ok(k in gemini, 'missing ' + k);
|
|
20
|
+
}
|
|
21
|
+
assert.equal(gemini.name, 'gemini');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('gemini-adapter: capabilities match D-07 + D-17', () => {
|
|
25
|
+
const c = gemini.capabilities;
|
|
26
|
+
assert.equal(c.askUserQuestion, false);
|
|
27
|
+
assert.equal(c.slashCommands, false);
|
|
28
|
+
assert.equal(
|
|
29
|
+
c.agentsMd,
|
|
30
|
+
'GEMINI.md',
|
|
31
|
+
'D-17: Gemini reads GEMINI.md as default context file',
|
|
32
|
+
);
|
|
33
|
+
assert.equal(c.textMode, 'auto');
|
|
34
|
+
assert.equal(c.modelResolution, 'profile');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('gemini-adapter: paths.agentsMd is root-level GEMINI.md', () => {
|
|
38
|
+
assert.equal(
|
|
39
|
+
gemini.paths.agentsMd,
|
|
40
|
+
'GEMINI.md',
|
|
41
|
+
'D-17: GEMINI.md at project root, not in .gemini/ subdir',
|
|
42
|
+
);
|
|
43
|
+
assert.equal(gemini.paths.payload, null);
|
|
44
|
+
assert.equal(gemini.paths.config, null);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('gemini-adapter: readline select parses 1-based index', async () => {
|
|
48
|
+
_setReadlineImplForTests(async () => '2');
|
|
49
|
+
try {
|
|
50
|
+
const { val } = await captureStderr(() =>
|
|
51
|
+
gemini.askUser({ type: 'select', question: 'P', options: ['A', 'B'] }),
|
|
52
|
+
);
|
|
53
|
+
assert.equal(val.value, 'B');
|
|
54
|
+
assert.equal(val.source, 'readline');
|
|
55
|
+
} finally {
|
|
56
|
+
_setReadlineImplForTests(null);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('gemini-adapter: readline input', async () => {
|
|
61
|
+
_setReadlineImplForTests(async () => 'ok');
|
|
62
|
+
try {
|
|
63
|
+
const { val } = await captureStderr(() =>
|
|
64
|
+
gemini.askUser({ type: 'input', question: 'Q' }),
|
|
65
|
+
);
|
|
66
|
+
assert.equal(val.value, 'ok');
|
|
67
|
+
} finally {
|
|
68
|
+
_setReadlineImplForTests(null);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('gemini-adapter: readline confirm', async () => {
|
|
73
|
+
_setReadlineImplForTests(async () => 'y');
|
|
74
|
+
try {
|
|
75
|
+
const { val } = await captureStderr(() =>
|
|
76
|
+
gemini.askUser({ type: 'confirm', question: 'OK' }),
|
|
77
|
+
);
|
|
78
|
+
assert.equal(val.value, true);
|
|
79
|
+
} finally {
|
|
80
|
+
_setReadlineImplForTests(null);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('gemini-adapter: single responsibility — no install-layer imports, no direct readline', () => {
|
|
85
|
+
const src = fs.readFileSync(require.resolve('./gemini.cjs'), 'utf-8');
|
|
86
|
+
assert.ok(
|
|
87
|
+
!/require\(['"]\.\.\/install\//.test(src),
|
|
88
|
+
'gemini.cjs must not reach into lib/install/',
|
|
89
|
+
);
|
|
90
|
+
assert.ok(
|
|
91
|
+
!/require\(['"]node:readline['"]\)/.test(src),
|
|
92
|
+
'gemini.cjs must delegate to _readline.cjs',
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('gemini-adapter: exports runtimeNotice compatible with agents-md SC-5 check', () => {
|
|
97
|
+
const notice = gemini.runtimeNotice;
|
|
98
|
+
assert.equal(typeof notice, 'string');
|
|
99
|
+
assert.ok(notice.length > 0);
|
|
100
|
+
assert.match(notice, /readline|prompt/i, 'runtimeNotice must match /readline|prompt/i');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('gemini-adapter: runtimeNotice references GEMINI.md', () => {
|
|
104
|
+
assert.ok(gemini.runtimeNotice.includes('GEMINI.md'));
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('gemini-adapter: runtimeNotice does not contain the forbidden joined Claude-tool literal (SC-5 guard)', () => {
|
|
108
|
+
assert.ok(!/Ask-User-Question/.test(gemini.runtimeNotice));
|
|
109
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const { NubosPilotError } = require('../core.cjs');
|
|
4
|
+
const { getRuntime: _askuserGetRuntime } = require('../askuser.cjs');
|
|
5
|
+
|
|
6
|
+
const KNOWN_RUNTIMES = ['claude', 'opencode', 'codex', 'gemini'];
|
|
7
|
+
|
|
8
|
+
function listRuntimes() {
|
|
9
|
+
return KNOWN_RUNTIMES.slice();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function getAdapter(name) {
|
|
13
|
+
if (!KNOWN_RUNTIMES.includes(name)) {
|
|
14
|
+
throw new NubosPilotError(
|
|
15
|
+
'runtime-unknown',
|
|
16
|
+
'Unknown runtime: ' + name,
|
|
17
|
+
{ name, known: KNOWN_RUNTIMES.slice() },
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
return require('./' + name + '.cjs');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function detect(opts) {
|
|
24
|
+
const cwd = (opts && opts.cwd) || process.cwd();
|
|
25
|
+
|
|
26
|
+
const configPath = path.join(cwd, '.nubos-pilot', 'config.json');
|
|
27
|
+
if (fs.existsSync(configPath)) {
|
|
28
|
+
try {
|
|
29
|
+
const cfg = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
30
|
+
if (cfg && cfg.runtime && KNOWN_RUNTIMES.includes(cfg.runtime)) {
|
|
31
|
+
return { runtime: cfg.runtime, source: cfg.runtime_source || 'config' };
|
|
32
|
+
}
|
|
33
|
+
} catch { }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const live = _askuserGetRuntime();
|
|
37
|
+
if (KNOWN_RUNTIMES.includes(live)) {
|
|
38
|
+
return { runtime: live, source: 'env' };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { runtime: 'codex', source: 'default' };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getCurrent() {
|
|
45
|
+
const { runtime } = detect();
|
|
46
|
+
return getAdapter(runtime);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { listRuntimes, getAdapter, getCurrent, detect };
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const os = require('node:os');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const test = require('node:test');
|
|
5
|
+
const assert = require('node:assert/strict');
|
|
6
|
+
|
|
7
|
+
const rt = require('./index.cjs');
|
|
8
|
+
|
|
9
|
+
const RUNTIME_ENV_KEYS = [
|
|
10
|
+
'NUBOS_RUNTIME',
|
|
11
|
+
'CLAUDECODE',
|
|
12
|
+
'CLAUDE_CODE_ENTRYPOINT',
|
|
13
|
+
'CODEX_HOME',
|
|
14
|
+
'CODEX_VERSION',
|
|
15
|
+
'GEMINI_CLI',
|
|
16
|
+
'GEMINI_VERSION',
|
|
17
|
+
'OPENCODE',
|
|
18
|
+
'OPENCODE_VERSION',
|
|
19
|
+
'NUBOS_PILOT_REDETECT_RUNTIME',
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
function snapshotEnv() {
|
|
23
|
+
const snap = {};
|
|
24
|
+
for (const k of RUNTIME_ENV_KEYS) snap[k] = process.env[k];
|
|
25
|
+
return snap;
|
|
26
|
+
}
|
|
27
|
+
function restoreEnv(snap) {
|
|
28
|
+
for (const k of RUNTIME_ENV_KEYS) {
|
|
29
|
+
if (snap[k] === undefined) delete process.env[k];
|
|
30
|
+
else process.env[k] = snap[k];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function clearAllRuntimeEnv() {
|
|
34
|
+
for (const k of RUNTIME_ENV_KEYS) delete process.env[k];
|
|
35
|
+
}
|
|
36
|
+
function forceRedetect() {
|
|
37
|
+
process.env.NUBOS_PILOT_REDETECT_RUNTIME = '1';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function mkTmp(scope) {
|
|
41
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'np-runtime-' + scope + '-'));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function writeConfig(cwd, json) {
|
|
45
|
+
const dir = path.join(cwd, '.nubos-pilot');
|
|
46
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
47
|
+
fs.writeFileSync(path.join(dir, 'config.json'), JSON.stringify(json), 'utf-8');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
test('RTI-1: listRuntimes returns four known runtimes in canonical order', () => {
|
|
51
|
+
assert.deepEqual(rt.listRuntimes(), ['claude', 'opencode', 'codex', 'gemini']);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('RTI-2: listRuntimes returns defensive copy — mutation does not leak', () => {
|
|
55
|
+
const a = rt.listRuntimes();
|
|
56
|
+
a.push('pwned');
|
|
57
|
+
const b = rt.listRuntimes();
|
|
58
|
+
assert.deepEqual(b, ['claude', 'opencode', 'codex', 'gemini']);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('RTI-3: getAdapter("nonexistent") throws NubosPilotError with code runtime-unknown', () => {
|
|
62
|
+
assert.throws(
|
|
63
|
+
() => rt.getAdapter('nonexistent'),
|
|
64
|
+
(err) => err && err.name === 'NubosPilotError' && err.code === 'runtime-unknown',
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('RTI-4: getAdapter unknown-runtime error details.known lists KNOWN_RUNTIMES', () => {
|
|
69
|
+
try {
|
|
70
|
+
rt.getAdapter('mystery');
|
|
71
|
+
assert.fail('should throw');
|
|
72
|
+
} catch (err) {
|
|
73
|
+
assert.equal(err.code, 'runtime-unknown');
|
|
74
|
+
assert.deepEqual(err.details.known, ['claude', 'opencode', 'codex', 'gemini']);
|
|
75
|
+
assert.equal(err.details.name, 'mystery');
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('RTI-5: detect honors .nubos-pilot/config.json runtime + runtime_source', () => {
|
|
80
|
+
const snap = snapshotEnv();
|
|
81
|
+
const tmp = mkTmp('cfg-honored');
|
|
82
|
+
try {
|
|
83
|
+
clearAllRuntimeEnv();
|
|
84
|
+
forceRedetect();
|
|
85
|
+
writeConfig(tmp, { runtime: 'codex', runtime_source: 'asked' });
|
|
86
|
+
const d = rt.detect({ cwd: tmp });
|
|
87
|
+
assert.equal(d.runtime, 'codex');
|
|
88
|
+
assert.equal(d.source, 'asked');
|
|
89
|
+
} finally {
|
|
90
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
91
|
+
restoreEnv(snap);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('RTI-6: detect config without runtime_source defaults source to "config"', () => {
|
|
96
|
+
const snap = snapshotEnv();
|
|
97
|
+
const tmp = mkTmp('cfg-no-src');
|
|
98
|
+
try {
|
|
99
|
+
clearAllRuntimeEnv();
|
|
100
|
+
forceRedetect();
|
|
101
|
+
writeConfig(tmp, { runtime: 'gemini' });
|
|
102
|
+
const d = rt.detect({ cwd: tmp });
|
|
103
|
+
assert.equal(d.runtime, 'gemini');
|
|
104
|
+
assert.equal(d.source, 'config');
|
|
105
|
+
} finally {
|
|
106
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
107
|
+
restoreEnv(snap);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('RTI-7: detect falls through when config.runtime is unknown', () => {
|
|
112
|
+
const snap = snapshotEnv();
|
|
113
|
+
const tmp = mkTmp('cfg-unknown');
|
|
114
|
+
try {
|
|
115
|
+
clearAllRuntimeEnv();
|
|
116
|
+
forceRedetect();
|
|
117
|
+
writeConfig(tmp, { runtime: 'mystery' });
|
|
118
|
+
|
|
119
|
+
const d = rt.detect({ cwd: tmp });
|
|
120
|
+
assert.equal(d.runtime, 'codex');
|
|
121
|
+
assert.equal(d.source, 'default');
|
|
122
|
+
} finally {
|
|
123
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
124
|
+
restoreEnv(snap);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('RTI-8: detect with no config + CLAUDECODE=1 returns {runtime:claude, source:env}', () => {
|
|
129
|
+
const snap = snapshotEnv();
|
|
130
|
+
const tmp = mkTmp('env-claude');
|
|
131
|
+
try {
|
|
132
|
+
clearAllRuntimeEnv();
|
|
133
|
+
process.env.CLAUDECODE = '1';
|
|
134
|
+
forceRedetect();
|
|
135
|
+
const d = rt.detect({ cwd: tmp });
|
|
136
|
+
assert.equal(d.runtime, 'claude');
|
|
137
|
+
assert.equal(d.source, 'env');
|
|
138
|
+
} finally {
|
|
139
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
140
|
+
restoreEnv(snap);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('RTI-9: detect with no config + no env returns {runtime:codex, source:default}', () => {
|
|
145
|
+
const snap = snapshotEnv();
|
|
146
|
+
const tmp = mkTmp('default');
|
|
147
|
+
try {
|
|
148
|
+
clearAllRuntimeEnv();
|
|
149
|
+
forceRedetect();
|
|
150
|
+
const d = rt.detect({ cwd: tmp });
|
|
151
|
+
assert.equal(d.runtime, 'codex');
|
|
152
|
+
assert.equal(d.source, 'default');
|
|
153
|
+
} finally {
|
|
154
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
155
|
+
restoreEnv(snap);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('RTI-10: detect tolerates malformed config.json (silent fall-through to env/default)', () => {
|
|
160
|
+
const snap = snapshotEnv();
|
|
161
|
+
const tmp = mkTmp('bad-json');
|
|
162
|
+
try {
|
|
163
|
+
clearAllRuntimeEnv();
|
|
164
|
+
forceRedetect();
|
|
165
|
+
const dir = path.join(tmp, '.nubos-pilot');
|
|
166
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
167
|
+
fs.writeFileSync(path.join(dir, 'config.json'), '{ not json', 'utf-8');
|
|
168
|
+
const d = rt.detect({ cwd: tmp });
|
|
169
|
+
assert.equal(d.runtime, 'codex');
|
|
170
|
+
assert.equal(d.source, 'default');
|
|
171
|
+
} finally {
|
|
172
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
173
|
+
restoreEnv(snap);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('RTI-11: module exports exactly listRuntimes, getAdapter, getCurrent, detect', () => {
|
|
178
|
+
const keys = Object.keys(rt).sort();
|
|
179
|
+
assert.deepEqual(keys, ['detect', 'getAdapter', 'getCurrent', 'listRuntimes']);
|
|
180
|
+
});
|
|
181
|
+
|