nubos-pilot 0.5.2 → 0.5.4
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/bin/install.js +2 -12
- package/bin/np-tools/_commands.cjs +2 -0
- package/bin/np-tools/add-tests.cjs +4 -0
- package/bin/np-tools/add-todo.cjs +4 -0
- package/bin/np-tools/discuss-phase.cjs +5 -0
- package/bin/np-tools/discuss-phase.test.cjs +75 -18
- package/bin/np-tools/discuss-project.cjs +5 -0
- package/bin/np-tools/execute-milestone.cjs +5 -0
- package/bin/np-tools/lang-directive.cjs +64 -0
- package/bin/np-tools/lang-directive.test.cjs +88 -0
- package/bin/np-tools/new-milestone.cjs +6 -2
- package/bin/np-tools/new-project.cjs +6 -2
- package/bin/np-tools/plan-milestone.cjs +5 -0
- package/bin/np-tools/research-phase.cjs +5 -0
- package/bin/np-tools/resume-work.cjs +5 -0
- package/bin/np-tools/text-mode.cjs +56 -0
- package/bin/np-tools/text-mode.test.cjs +132 -0
- package/bin/np-tools/verify-work.cjs +5 -0
- package/lib/language.cjs +63 -0
- package/lib/language.test.cjs +99 -0
- package/lib/runtime/_readline.cjs +5 -1
- package/lib/runtime/_readline.test.cjs +2 -1
- package/lib/runtime/claude.cjs +21 -1
- package/lib/runtime/claude.test.cjs +63 -0
- package/lib/text-mode.cjs +79 -0
- package/lib/text-mode.test.cjs +139 -0
- package/np-tools.cjs +2 -0
- package/package.json +1 -1
- package/workflows/add-todo.md +10 -5
- package/workflows/discuss-phase.md +49 -17
- package/workflows/discuss-project.md +6 -0
- package/workflows/execute-phase.md +16 -2
- package/workflows/new-milestone.md +12 -0
- package/workflows/new-project.md +13 -0
- package/workflows/note.md +11 -0
- package/workflows/pause-work.md +5 -0
- package/workflows/plan-phase.md +13 -1
- package/workflows/research-phase.md +12 -0
- package/workflows/resume-work.md +12 -0
- package/workflows/scan-codebase.md +8 -0
- package/workflows/session-report.md +18 -0
- package/workflows/update-docs.md +7 -0
- package/workflows/validate-phase.md +13 -1
- package/workflows/verify-work.md +15 -1
|
@@ -0,0 +1,132 @@
|
|
|
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 path = require('node:path');
|
|
7
|
+
const os = require('node:os');
|
|
8
|
+
|
|
9
|
+
const subcmd = require('./text-mode.cjs');
|
|
10
|
+
|
|
11
|
+
function _mkSandbox() {
|
|
12
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'np-text-mode-cli-'));
|
|
13
|
+
fs.mkdirSync(path.join(dir, '.nubos-pilot'), { recursive: true });
|
|
14
|
+
return dir;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function _captureIO() {
|
|
18
|
+
const out = [];
|
|
19
|
+
const err = [];
|
|
20
|
+
return {
|
|
21
|
+
stdout: { write: (s) => { out.push(String(s)); return true; } },
|
|
22
|
+
stderr: { write: (s) => { err.push(String(s)); return true; } },
|
|
23
|
+
stdoutText: () => out.join(''),
|
|
24
|
+
stderrText: () => err.join(''),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function _clearClaudeEnv() {
|
|
29
|
+
const saved = {};
|
|
30
|
+
for (const k of ['CLAUDECODE', 'CLAUDE_CODE_ENTRYPOINT']) {
|
|
31
|
+
saved[k] = process.env[k];
|
|
32
|
+
delete process.env[k];
|
|
33
|
+
}
|
|
34
|
+
return () => {
|
|
35
|
+
for (const [k, v] of Object.entries(saved)) {
|
|
36
|
+
if (v === undefined) delete process.env[k];
|
|
37
|
+
else process.env[k] = v;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
test('text-mode CLI: default without config and without Claude env prints "false"', () => {
|
|
43
|
+
const restore = _clearClaudeEnv();
|
|
44
|
+
try {
|
|
45
|
+
const dir = _mkSandbox();
|
|
46
|
+
try {
|
|
47
|
+
const io = _captureIO();
|
|
48
|
+
const rc = subcmd.run([], { cwd: dir, stdout: io.stdout, stderr: io.stderr });
|
|
49
|
+
assert.equal(rc, 0);
|
|
50
|
+
assert.equal(io.stdoutText().trim(), 'false');
|
|
51
|
+
} finally {
|
|
52
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
53
|
+
}
|
|
54
|
+
} finally {
|
|
55
|
+
restore();
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('text-mode CLI: CLAUDECODE=1 prints "true"', () => {
|
|
60
|
+
const restore = _clearClaudeEnv();
|
|
61
|
+
try {
|
|
62
|
+
process.env.CLAUDECODE = '1';
|
|
63
|
+
const dir = _mkSandbox();
|
|
64
|
+
try {
|
|
65
|
+
const io = _captureIO();
|
|
66
|
+
const rc = subcmd.run([], { cwd: dir, stdout: io.stdout, stderr: io.stderr });
|
|
67
|
+
assert.equal(rc, 0);
|
|
68
|
+
assert.equal(io.stdoutText().trim(), 'true');
|
|
69
|
+
} finally {
|
|
70
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
71
|
+
}
|
|
72
|
+
} finally {
|
|
73
|
+
restore();
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('text-mode CLI: --json emits detail object', () => {
|
|
78
|
+
const restore = _clearClaudeEnv();
|
|
79
|
+
try {
|
|
80
|
+
process.env.CLAUDECODE = '1';
|
|
81
|
+
const dir = _mkSandbox();
|
|
82
|
+
try {
|
|
83
|
+
const io = _captureIO();
|
|
84
|
+
const rc = subcmd.run(['--json'], { cwd: dir, stdout: io.stdout, stderr: io.stderr });
|
|
85
|
+
assert.equal(rc, 0);
|
|
86
|
+
const payload = JSON.parse(io.stdoutText().trim());
|
|
87
|
+
assert.equal(payload.enabled, true);
|
|
88
|
+
assert.equal(payload.source, 'runtime');
|
|
89
|
+
} finally {
|
|
90
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
91
|
+
}
|
|
92
|
+
} finally {
|
|
93
|
+
restore();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('text-mode CLI: config workflow.text_mode=false wins over CLAUDECODE', () => {
|
|
98
|
+
const restore = _clearClaudeEnv();
|
|
99
|
+
try {
|
|
100
|
+
process.env.CLAUDECODE = '1';
|
|
101
|
+
const dir = _mkSandbox();
|
|
102
|
+
try {
|
|
103
|
+
fs.writeFileSync(
|
|
104
|
+
path.join(dir, '.nubos-pilot', 'config.json'),
|
|
105
|
+
JSON.stringify({ workflow: { text_mode: false } }),
|
|
106
|
+
);
|
|
107
|
+
const io = _captureIO();
|
|
108
|
+
const rc = subcmd.run(['--json'], { cwd: dir, stdout: io.stdout, stderr: io.stderr });
|
|
109
|
+
assert.equal(rc, 0);
|
|
110
|
+
const payload = JSON.parse(io.stdoutText().trim());
|
|
111
|
+
assert.equal(payload.enabled, false);
|
|
112
|
+
assert.equal(payload.source, 'config');
|
|
113
|
+
} finally {
|
|
114
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
115
|
+
}
|
|
116
|
+
} finally {
|
|
117
|
+
restore();
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('text-mode CLI: unknown flag exits 1 with structured error', () => {
|
|
122
|
+
const dir = _mkSandbox();
|
|
123
|
+
try {
|
|
124
|
+
const io = _captureIO();
|
|
125
|
+
const rc = subcmd.run(['--wat'], { cwd: dir, stdout: io.stdout, stderr: io.stderr });
|
|
126
|
+
assert.equal(rc, 1);
|
|
127
|
+
const payload = JSON.parse(io.stderrText().trim());
|
|
128
|
+
assert.equal(payload.code, 'text-mode-unknown-arg');
|
|
129
|
+
} finally {
|
|
130
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
131
|
+
}
|
|
132
|
+
});
|
|
@@ -19,6 +19,7 @@ const {
|
|
|
19
19
|
milestoneVerificationPath,
|
|
20
20
|
} = require('../../lib/verify.cjs');
|
|
21
21
|
const { getAgentSkills } = require('../../lib/agents.cjs');
|
|
22
|
+
const textMode = require('../../lib/text-mode.cjs');
|
|
22
23
|
|
|
23
24
|
const INLINE_THRESHOLD_BYTES = 16 * 1024;
|
|
24
25
|
const _VALID_SC_STATUSES = new Set(['Pass', 'Fail', 'Defer', 'Pending']);
|
|
@@ -90,6 +91,8 @@ function _initPayload(mNum, cwd) {
|
|
|
90
91
|
};
|
|
91
92
|
});
|
|
92
93
|
|
|
94
|
+
const tmDetail = textMode.resolveTextModeDetail(cwd);
|
|
95
|
+
|
|
93
96
|
return {
|
|
94
97
|
_workflow: 'verify-work',
|
|
95
98
|
milestone: mNum,
|
|
@@ -101,6 +104,8 @@ function _initPayload(mNum, cwd) {
|
|
|
101
104
|
verification_path: verificationPath,
|
|
102
105
|
slice_uat: sliceUat,
|
|
103
106
|
verifier_tier: 'sonnet',
|
|
107
|
+
text_mode: tmDetail.enabled,
|
|
108
|
+
text_mode_source: tmDetail.source,
|
|
104
109
|
agent_skills: { verifier: _safeSkills('np-verifier', cwd) },
|
|
105
110
|
};
|
|
106
111
|
}
|
package/lib/language.cjs
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { findProjectRoot, NubosPilotError } = require('./core.cjs');
|
|
6
|
+
|
|
7
|
+
const LANG_DIRECTIVES = {
|
|
8
|
+
de: 'Sprache: **Deutsch.** Jede nubos-pilot Slash-Command-Ausgabe, jede Frage an den User und jedes Statusupdate in allen `/np:*` Workflows ist auf Deutsch zu schreiben — inklusive Fehlermeldungen und Klärungsfragen. Nur Code, Bash-Kommandos, Tool-Outputs und Commit-Messages bleiben wie sie sind.',
|
|
9
|
+
en: 'Language: **English.** All `/np:*` slash-command output, askuser prompts and status updates respond in English.',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const DEFAULT_LANGUAGE = 'en';
|
|
13
|
+
|
|
14
|
+
function normalizeLanguage(raw) {
|
|
15
|
+
const s = String(raw || '').trim().toLowerCase();
|
|
16
|
+
return s || DEFAULT_LANGUAGE;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function buildDirective(language) {
|
|
20
|
+
const lang = normalizeLanguage(language);
|
|
21
|
+
if (LANG_DIRECTIVES[lang]) return LANG_DIRECTIVES[lang];
|
|
22
|
+
return 'Language: respond in the ISO-639 language `' + lang + '` for all `/np:*` slash-command output, askuser prompts and status updates.';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readConfigLanguage(cwd) {
|
|
26
|
+
let root;
|
|
27
|
+
try {
|
|
28
|
+
root = findProjectRoot(cwd || process.cwd());
|
|
29
|
+
} catch (err) {
|
|
30
|
+
if (err && err.code === 'not-in-project') return null;
|
|
31
|
+
throw err;
|
|
32
|
+
}
|
|
33
|
+
const p = path.join(root, '.nubos-pilot', 'config.json');
|
|
34
|
+
if (!fs.existsSync(p)) return null;
|
|
35
|
+
let parsed;
|
|
36
|
+
try {
|
|
37
|
+
parsed = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
38
|
+
} catch (err) {
|
|
39
|
+
throw new NubosPilotError('language-config-parse-error', 'config.json invalid JSON', { cause: err && err.message });
|
|
40
|
+
}
|
|
41
|
+
if (!parsed || typeof parsed !== 'object') return null;
|
|
42
|
+
const raw = parsed.response_language;
|
|
43
|
+
if (raw == null || raw === '') return null;
|
|
44
|
+
return normalizeLanguage(raw);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function resolveLanguage(cwd) {
|
|
48
|
+
return readConfigLanguage(cwd) || DEFAULT_LANGUAGE;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function resolveDirective(cwd) {
|
|
52
|
+
return buildDirective(resolveLanguage(cwd));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = {
|
|
56
|
+
LANG_DIRECTIVES,
|
|
57
|
+
DEFAULT_LANGUAGE,
|
|
58
|
+
normalizeLanguage,
|
|
59
|
+
buildDirective,
|
|
60
|
+
readConfigLanguage,
|
|
61
|
+
resolveLanguage,
|
|
62
|
+
resolveDirective,
|
|
63
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
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 path = require('node:path');
|
|
7
|
+
const os = require('node:os');
|
|
8
|
+
|
|
9
|
+
const lang = require('./language.cjs');
|
|
10
|
+
|
|
11
|
+
function _mkSandbox() {
|
|
12
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'np-lang-'));
|
|
13
|
+
fs.mkdirSync(path.join(dir, '.nubos-pilot'), { recursive: true });
|
|
14
|
+
return dir;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function _writeConfig(dir, obj) {
|
|
18
|
+
fs.writeFileSync(path.join(dir, '.nubos-pilot', 'config.json'), JSON.stringify(obj, null, 2));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
test('language: normalizeLanguage lowercases and trims', () => {
|
|
22
|
+
assert.equal(lang.normalizeLanguage('DE'), 'de');
|
|
23
|
+
assert.equal(lang.normalizeLanguage(' En '), 'en');
|
|
24
|
+
assert.equal(lang.normalizeLanguage(''), 'en');
|
|
25
|
+
assert.equal(lang.normalizeLanguage(null), 'en');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('language: buildDirective returns known de/en strings', () => {
|
|
29
|
+
const de = lang.buildDirective('de');
|
|
30
|
+
assert.match(de, /Sprache: \*\*Deutsch\.\*\*/);
|
|
31
|
+
const en = lang.buildDirective('en');
|
|
32
|
+
assert.match(en, /Language: \*\*English\.\*\*/);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('language: buildDirective falls back to ISO-639 template for unknown code', () => {
|
|
36
|
+
const fr = lang.buildDirective('fr');
|
|
37
|
+
assert.match(fr, /ISO-639 language `fr`/);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('language: readConfigLanguage returns null when no config', () => {
|
|
41
|
+
const dir = _mkSandbox();
|
|
42
|
+
try {
|
|
43
|
+
assert.equal(lang.readConfigLanguage(dir), null);
|
|
44
|
+
} finally {
|
|
45
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('language: readConfigLanguage returns normalized value from config', () => {
|
|
50
|
+
const dir = _mkSandbox();
|
|
51
|
+
try {
|
|
52
|
+
_writeConfig(dir, { response_language: 'DE' });
|
|
53
|
+
assert.equal(lang.readConfigLanguage(dir), 'de');
|
|
54
|
+
} finally {
|
|
55
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('language: readConfigLanguage returns null when response_language absent', () => {
|
|
60
|
+
const dir = _mkSandbox();
|
|
61
|
+
try {
|
|
62
|
+
_writeConfig(dir, { runtime: 'claude' });
|
|
63
|
+
assert.equal(lang.readConfigLanguage(dir), null);
|
|
64
|
+
} finally {
|
|
65
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('language: resolveLanguage defaults to en when no project root', () => {
|
|
70
|
+
const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'np-outside-'));
|
|
71
|
+
try {
|
|
72
|
+
assert.equal(lang.resolveLanguage(outside), 'en');
|
|
73
|
+
} finally {
|
|
74
|
+
fs.rmSync(outside, { recursive: true, force: true });
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('language: resolveDirective uses config language', () => {
|
|
79
|
+
const dir = _mkSandbox();
|
|
80
|
+
try {
|
|
81
|
+
_writeConfig(dir, { response_language: 'de' });
|
|
82
|
+
assert.match(lang.resolveDirective(dir), /Sprache: \*\*Deutsch\.\*\*/);
|
|
83
|
+
} finally {
|
|
84
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('language: readConfigLanguage throws on invalid JSON', () => {
|
|
89
|
+
const dir = _mkSandbox();
|
|
90
|
+
try {
|
|
91
|
+
fs.writeFileSync(path.join(dir, '.nubos-pilot', 'config.json'), '{not json');
|
|
92
|
+
assert.throws(
|
|
93
|
+
() => lang.readConfigLanguage(dir),
|
|
94
|
+
(err) => err && err.code === 'language-config-parse-error',
|
|
95
|
+
);
|
|
96
|
+
} finally {
|
|
97
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
98
|
+
}
|
|
99
|
+
});
|
|
@@ -7,6 +7,10 @@ function _setReadlineImplForTests(impl) {
|
|
|
7
7
|
_readlineImpl = impl || null;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
function _hasReadlineImplForTests() {
|
|
11
|
+
return _readlineImpl != null;
|
|
12
|
+
}
|
|
13
|
+
|
|
10
14
|
function _readOneLine() {
|
|
11
15
|
if (_readlineImpl) return Promise.resolve(_readlineImpl());
|
|
12
16
|
return new Promise((resolve, reject) => {
|
|
@@ -156,4 +160,4 @@ async function askUserReadline({ type, question, options, def }) {
|
|
|
156
160
|
return { value: _parseAnswer(type, line, options, def), source: 'readline' };
|
|
157
161
|
}
|
|
158
162
|
|
|
159
|
-
module.exports = { askUserReadline, _readOneLine, _parseAnswer, _setReadlineImplForTests };
|
|
163
|
+
module.exports = { askUserReadline, _readOneLine, _parseAnswer, _setReadlineImplForTests, _hasReadlineImplForTests };
|
|
@@ -115,9 +115,10 @@ test('RL-9: _parseAnswer unknown type throws askuser-invalid-type', () => {
|
|
|
115
115
|
);
|
|
116
116
|
});
|
|
117
117
|
|
|
118
|
-
test('RL-10: module exports
|
|
118
|
+
test('RL-10: module exports readline helpers + claude-runtime TTY probe', () => {
|
|
119
119
|
const keys = Object.keys(rl).sort();
|
|
120
120
|
assert.deepEqual(keys, [
|
|
121
|
+
'_hasReadlineImplForTests',
|
|
121
122
|
'_parseAnswer',
|
|
122
123
|
'_readOneLine',
|
|
123
124
|
'_setReadlineImplForTests',
|
package/lib/runtime/claude.cjs
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const {
|
|
2
|
+
_readOneLine,
|
|
3
|
+
_parseAnswer,
|
|
4
|
+
_hasReadlineImplForTests,
|
|
5
|
+
askUserReadline,
|
|
6
|
+
} = require('./_readline.cjs');
|
|
7
|
+
const { NubosPilotError } = require('../core.cjs');
|
|
2
8
|
|
|
3
9
|
function _emitClaudeMarkerBlock({ type, question, options, def }) {
|
|
4
10
|
const payload = {
|
|
@@ -17,6 +23,20 @@ async function askUser(spec) {
|
|
|
17
23
|
const question = spec && spec.question;
|
|
18
24
|
const options = spec && spec.options;
|
|
19
25
|
const def = spec ? spec.default : undefined;
|
|
26
|
+
const hasTTY = !!process.stdin.isTTY;
|
|
27
|
+
if (hasTTY) {
|
|
28
|
+
return askUserReadline({ type, question, options, def });
|
|
29
|
+
}
|
|
30
|
+
if (!_hasReadlineImplForTests()) {
|
|
31
|
+
if (def !== undefined && def !== null) {
|
|
32
|
+
return { value: def, source: 'default' };
|
|
33
|
+
}
|
|
34
|
+
throw new NubosPilotError(
|
|
35
|
+
'askuser-no-tty',
|
|
36
|
+
'askUser cannot prompt without TTY (Claude Code Bash has no interactive stdin). Fall back to plain-text numbered list.',
|
|
37
|
+
{ question, type },
|
|
38
|
+
);
|
|
39
|
+
}
|
|
20
40
|
const line = await _emitClaudeMarkerBlock({ type, question, options, def });
|
|
21
41
|
return { value: _parseAnswer(type, line, options, def), source: 'askUserQuestion' };
|
|
22
42
|
}
|
|
@@ -99,3 +99,66 @@ test('claude-adapter: exports runtimeNotice compatible with agents-md SC-5 check
|
|
|
99
99
|
test('claude-adapter: runtimeNotice does not contain the forbidden joined Claude-tool literal (SC-5 guard)', () => {
|
|
100
100
|
assert.ok(!/Ask-User-Question/.test(claude.runtimeNotice));
|
|
101
101
|
});
|
|
102
|
+
|
|
103
|
+
test('claude-adapter: askUser without TTY and without default throws askuser-no-tty', async () => {
|
|
104
|
+
const originalIsTTY = process.stdin.isTTY;
|
|
105
|
+
try {
|
|
106
|
+
Object.defineProperty(process.stdin, 'isTTY', { value: false, configurable: true });
|
|
107
|
+
_setReadlineImplForTests(null);
|
|
108
|
+
await assert.rejects(
|
|
109
|
+
() => claude.askUser({ type: 'select', question: 'P', options: ['A', 'B'] }),
|
|
110
|
+
(err) => err && err.code === 'askuser-no-tty',
|
|
111
|
+
);
|
|
112
|
+
} finally {
|
|
113
|
+
if (originalIsTTY === undefined) {
|
|
114
|
+
delete process.stdin.isTTY;
|
|
115
|
+
} else {
|
|
116
|
+
Object.defineProperty(process.stdin, 'isTTY', { value: originalIsTTY, configurable: true });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('claude-adapter: askUser with TTY stdin uses readline UI, no marker block', async () => {
|
|
122
|
+
const originalIsTTY = process.stdin.isTTY;
|
|
123
|
+
_setReadlineImplForTests(async () => '2');
|
|
124
|
+
const originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
125
|
+
const stderrChunks = [];
|
|
126
|
+
process.stderr.write = (chunk) => { stderrChunks.push(String(chunk)); return true; };
|
|
127
|
+
try {
|
|
128
|
+
Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true });
|
|
129
|
+
const { val, out } = await captureStdout(() =>
|
|
130
|
+
claude.askUser({ type: 'select', question: 'Q', options: ['A', 'B', 'C'] })
|
|
131
|
+
);
|
|
132
|
+
assert.ok(!/<!-- askUser v1 -->/.test(out),
|
|
133
|
+
'must not emit marker block when stdin is TTY');
|
|
134
|
+
assert.equal(val.source, 'readline');
|
|
135
|
+
assert.equal(val.value, 'B');
|
|
136
|
+
const stderrJoined = stderrChunks.join('');
|
|
137
|
+
assert.match(stderrJoined, /Q/, 'readline UI should render the question on stderr');
|
|
138
|
+
} finally {
|
|
139
|
+
process.stderr.write = originalStderrWrite;
|
|
140
|
+
_setReadlineImplForTests(null);
|
|
141
|
+
if (originalIsTTY === undefined) {
|
|
142
|
+
delete process.stdin.isTTY;
|
|
143
|
+
} else {
|
|
144
|
+
Object.defineProperty(process.stdin, 'isTTY', { value: originalIsTTY, configurable: true });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('claude-adapter: askUser without TTY but with default returns default', async () => {
|
|
150
|
+
const originalIsTTY = process.stdin.isTTY;
|
|
151
|
+
try {
|
|
152
|
+
Object.defineProperty(process.stdin, 'isTTY', { value: false, configurable: true });
|
|
153
|
+
_setReadlineImplForTests(null);
|
|
154
|
+
const res = await claude.askUser({ type: 'confirm', question: 'OK?', default: true });
|
|
155
|
+
assert.equal(res.value, true);
|
|
156
|
+
assert.equal(res.source, 'default');
|
|
157
|
+
} finally {
|
|
158
|
+
if (originalIsTTY === undefined) {
|
|
159
|
+
delete process.stdin.isTTY;
|
|
160
|
+
} else {
|
|
161
|
+
Object.defineProperty(process.stdin, 'isTTY', { value: originalIsTTY, configurable: true });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { findProjectRoot, NubosPilotError } = require('./core.cjs');
|
|
6
|
+
|
|
7
|
+
const DEFAULT_TEXT_MODE = false;
|
|
8
|
+
const CLAUDE_ENV_KEYS = ['CLAUDECODE', 'CLAUDE_CODE_ENTRYPOINT'];
|
|
9
|
+
|
|
10
|
+
function _coerceBool(raw) {
|
|
11
|
+
if (raw === true || raw === false) return raw;
|
|
12
|
+
if (raw == null) return null;
|
|
13
|
+
const s = String(raw).trim().toLowerCase();
|
|
14
|
+
if (s === 'true' || s === '1' || s === 'yes' || s === 'on') return true;
|
|
15
|
+
if (s === 'false' || s === '0' || s === 'no' || s === 'off') return false;
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function readConfigTextMode(cwd) {
|
|
20
|
+
let root;
|
|
21
|
+
try {
|
|
22
|
+
root = findProjectRoot(cwd || process.cwd());
|
|
23
|
+
} catch (err) {
|
|
24
|
+
if (err && err.code === 'not-in-project') return null;
|
|
25
|
+
throw err;
|
|
26
|
+
}
|
|
27
|
+
const p = path.join(root, '.nubos-pilot', 'config.json');
|
|
28
|
+
if (!fs.existsSync(p)) return null;
|
|
29
|
+
let parsed;
|
|
30
|
+
try {
|
|
31
|
+
parsed = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
32
|
+
} catch (err) {
|
|
33
|
+
throw new NubosPilotError('text-mode-config-parse-error', 'config.json invalid JSON', { cause: err && err.message });
|
|
34
|
+
}
|
|
35
|
+
if (!parsed || typeof parsed !== 'object') return null;
|
|
36
|
+
const workflow = parsed.workflow;
|
|
37
|
+
if (!workflow || typeof workflow !== 'object') return null;
|
|
38
|
+
if (!Object.prototype.hasOwnProperty.call(workflow, 'text_mode')) return null;
|
|
39
|
+
const coerced = _coerceBool(workflow.text_mode);
|
|
40
|
+
return coerced;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function detectRuntimeTextMode(env) {
|
|
44
|
+
const source = env || process.env;
|
|
45
|
+
for (const key of CLAUDE_ENV_KEYS) {
|
|
46
|
+
const v = source[key];
|
|
47
|
+
if (v != null && String(v) !== '' && String(v) !== '0' && String(v).toLowerCase() !== 'false') {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function resolveTextMode(cwd, env) {
|
|
55
|
+
const fromConfig = readConfigTextMode(cwd);
|
|
56
|
+
if (fromConfig !== null) return fromConfig;
|
|
57
|
+
if (detectRuntimeTextMode(env)) return true;
|
|
58
|
+
return DEFAULT_TEXT_MODE;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function resolveTextModeDetail(cwd, env) {
|
|
62
|
+
const fromConfig = readConfigTextMode(cwd);
|
|
63
|
+
if (fromConfig !== null) {
|
|
64
|
+
return { enabled: fromConfig, source: 'config' };
|
|
65
|
+
}
|
|
66
|
+
if (detectRuntimeTextMode(env)) {
|
|
67
|
+
return { enabled: true, source: 'runtime' };
|
|
68
|
+
}
|
|
69
|
+
return { enabled: DEFAULT_TEXT_MODE, source: 'default' };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = {
|
|
73
|
+
DEFAULT_TEXT_MODE,
|
|
74
|
+
CLAUDE_ENV_KEYS,
|
|
75
|
+
readConfigTextMode,
|
|
76
|
+
detectRuntimeTextMode,
|
|
77
|
+
resolveTextMode,
|
|
78
|
+
resolveTextModeDetail,
|
|
79
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
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 path = require('node:path');
|
|
7
|
+
const os = require('node:os');
|
|
8
|
+
|
|
9
|
+
const tm = require('./text-mode.cjs');
|
|
10
|
+
|
|
11
|
+
function _mkSandbox() {
|
|
12
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'np-textmode-'));
|
|
13
|
+
fs.mkdirSync(path.join(dir, '.nubos-pilot'), { recursive: true });
|
|
14
|
+
return dir;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function _writeConfig(dir, obj) {
|
|
18
|
+
fs.writeFileSync(path.join(dir, '.nubos-pilot', 'config.json'), JSON.stringify(obj, null, 2));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
test('text-mode: default without config or runtime env is false', () => {
|
|
22
|
+
const dir = _mkSandbox();
|
|
23
|
+
try {
|
|
24
|
+
assert.equal(tm.resolveTextMode(dir, {}), false);
|
|
25
|
+
const detail = tm.resolveTextModeDetail(dir, {});
|
|
26
|
+
assert.deepEqual(detail, { enabled: false, source: 'default' });
|
|
27
|
+
} finally {
|
|
28
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('text-mode: CLAUDECODE=1 in env flips default to true', () => {
|
|
33
|
+
const dir = _mkSandbox();
|
|
34
|
+
try {
|
|
35
|
+
assert.equal(tm.resolveTextMode(dir, { CLAUDECODE: '1' }), true);
|
|
36
|
+
const detail = tm.resolveTextModeDetail(dir, { CLAUDECODE: '1' });
|
|
37
|
+
assert.deepEqual(detail, { enabled: true, source: 'runtime' });
|
|
38
|
+
} finally {
|
|
39
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('text-mode: CLAUDE_CODE_ENTRYPOINT non-empty flips to true', () => {
|
|
44
|
+
const dir = _mkSandbox();
|
|
45
|
+
try {
|
|
46
|
+
assert.equal(tm.resolveTextMode(dir, { CLAUDE_CODE_ENTRYPOINT: 'cli' }), true);
|
|
47
|
+
} finally {
|
|
48
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('text-mode: env value "0" or "false" does not flip', () => {
|
|
53
|
+
const dir = _mkSandbox();
|
|
54
|
+
try {
|
|
55
|
+
assert.equal(tm.resolveTextMode(dir, { CLAUDECODE: '0' }), false);
|
|
56
|
+
assert.equal(tm.resolveTextMode(dir, { CLAUDECODE: 'false' }), false);
|
|
57
|
+
assert.equal(tm.resolveTextMode(dir, { CLAUDECODE: '' }), false);
|
|
58
|
+
} finally {
|
|
59
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('text-mode: config workflow.text_mode=true wins over absent runtime', () => {
|
|
64
|
+
const dir = _mkSandbox();
|
|
65
|
+
try {
|
|
66
|
+
_writeConfig(dir, { workflow: { text_mode: true } });
|
|
67
|
+
assert.equal(tm.resolveTextMode(dir, {}), true);
|
|
68
|
+
assert.deepEqual(tm.resolveTextModeDetail(dir, {}), { enabled: true, source: 'config' });
|
|
69
|
+
} finally {
|
|
70
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('text-mode: config workflow.text_mode=false overrides CLAUDECODE runtime', () => {
|
|
75
|
+
const dir = _mkSandbox();
|
|
76
|
+
try {
|
|
77
|
+
_writeConfig(dir, { workflow: { text_mode: false } });
|
|
78
|
+
assert.equal(tm.resolveTextMode(dir, { CLAUDECODE: '1' }), false);
|
|
79
|
+
assert.deepEqual(
|
|
80
|
+
tm.resolveTextModeDetail(dir, { CLAUDECODE: '1' }),
|
|
81
|
+
{ enabled: false, source: 'config' },
|
|
82
|
+
);
|
|
83
|
+
} finally {
|
|
84
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('text-mode: config workflow.text_mode="true" string coerced to boolean', () => {
|
|
89
|
+
const dir = _mkSandbox();
|
|
90
|
+
try {
|
|
91
|
+
_writeConfig(dir, { workflow: { text_mode: 'true' } });
|
|
92
|
+
assert.equal(tm.resolveTextMode(dir, {}), true);
|
|
93
|
+
_writeConfig(dir, { workflow: { text_mode: '0' } });
|
|
94
|
+
assert.equal(tm.resolveTextMode(dir, { CLAUDECODE: '1' }), false);
|
|
95
|
+
} finally {
|
|
96
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('text-mode: config missing workflow.text_mode falls through to runtime detection', () => {
|
|
101
|
+
const dir = _mkSandbox();
|
|
102
|
+
try {
|
|
103
|
+
_writeConfig(dir, { response_language: 'de' });
|
|
104
|
+
assert.equal(tm.resolveTextMode(dir, { CLAUDECODE: '1' }), true);
|
|
105
|
+
assert.equal(tm.resolveTextMode(dir, {}), false);
|
|
106
|
+
} finally {
|
|
107
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('text-mode: readConfigTextMode returns null outside project root', () => {
|
|
112
|
+
const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'np-outside-'));
|
|
113
|
+
try {
|
|
114
|
+
assert.equal(tm.readConfigTextMode(outside), null);
|
|
115
|
+
assert.equal(tm.resolveTextMode(outside, {}), false);
|
|
116
|
+
} finally {
|
|
117
|
+
fs.rmSync(outside, { recursive: true, force: true });
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('text-mode: readConfigTextMode throws on invalid JSON', () => {
|
|
122
|
+
const dir = _mkSandbox();
|
|
123
|
+
try {
|
|
124
|
+
fs.writeFileSync(path.join(dir, '.nubos-pilot', 'config.json'), '{not json');
|
|
125
|
+
assert.throws(
|
|
126
|
+
() => tm.readConfigTextMode(dir),
|
|
127
|
+
(err) => err && err.code === 'text-mode-config-parse-error',
|
|
128
|
+
);
|
|
129
|
+
} finally {
|
|
130
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('text-mode: detectRuntimeTextMode honors multiple env keys', () => {
|
|
135
|
+
assert.equal(tm.detectRuntimeTextMode({ CLAUDECODE: '1' }), true);
|
|
136
|
+
assert.equal(tm.detectRuntimeTextMode({ CLAUDE_CODE_ENTRYPOINT: 'cli' }), true);
|
|
137
|
+
assert.equal(tm.detectRuntimeTextMode({ OTHER: '1' }), false);
|
|
138
|
+
assert.equal(tm.detectRuntimeTextMode({}), false);
|
|
139
|
+
});
|
package/np-tools.cjs
CHANGED
|
@@ -45,6 +45,8 @@ const topLevelCommands = {
|
|
|
45
45
|
'metrics': require('./bin/np-tools/metrics.cjs'),
|
|
46
46
|
'resolve-model': require('./bin/np-tools/resolve-model.cjs'),
|
|
47
47
|
'stats': require('./bin/np-tools/stats.cjs'),
|
|
48
|
+
'lang-directive': require('./bin/np-tools/lang-directive.cjs'),
|
|
49
|
+
'text-mode': require('./bin/np-tools/text-mode.cjs'),
|
|
48
50
|
};
|
|
49
51
|
|
|
50
52
|
const THRESHOLD = 16 * 1024;
|