nubos-pilot 0.1.0 → 0.2.1

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 CHANGED
@@ -14,9 +14,37 @@ const agentsMdMod = require('../lib/install/agents-md.cjs');
14
14
  const codexTomlMod = require('../lib/install/codex-toml.cjs');
15
15
  const runtimeDetectMod = require('../lib/install/runtime-detect.cjs');
16
16
  const backupMod = require('../lib/install/backup.cjs');
17
+ const registryMod = require('../lib/install/runtimes-registry.cjs');
17
18
 
18
19
  const cyan = '\x1b[36m', green = '\x1b[32m', yellow = '\x1b[33m',
19
- red = '\x1b[31m', dim = '\x1b[2m', reset = '\x1b[0m';
20
+ red = '\x1b[31m', blue = '\x1b[38;5;33m',
21
+ dim = '\x1b[2m', bold = '\x1b[1m', reset = '\x1b[0m';
22
+
23
+ const LOGO = [
24
+ ' ███╗ ██╗██╗ ██╗██████╗ ██████╗ ███████╗',
25
+ ' ████╗ ██║██║ ██║██╔══██╗██╔═══██╗██╔════╝',
26
+ ' ██╔██╗ ██║██║ ██║██████╔╝██║ ██║███████╗',
27
+ ' ██║╚██╗██║██║ ██║██╔══██╗██║ ██║╚════██║',
28
+ ' ██║ ╚████║╚██████╔╝██████╔╝╚██████╔╝███████║',
29
+ ' ╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝',
30
+ ];
31
+
32
+ function _printBanner() {
33
+ let pkgVersion = '0.0.0';
34
+ let pkgDesc = '';
35
+ try {
36
+ const pkg = require('../package.json');
37
+ pkgVersion = String(pkg.version || '0.0.0');
38
+ pkgDesc = String(pkg.description || '');
39
+ } catch {}
40
+ process.stderr.write('\n');
41
+ for (const line of LOGO) process.stderr.write(blue + line + reset + '\n');
42
+ process.stderr.write('\n');
43
+ process.stderr.write(' ' + bold + blue + 'Nubos Pilot' + reset
44
+ + dim + ' v' + pkgVersion + reset + '\n');
45
+ if (pkgDesc) process.stderr.write(' ' + dim + pkgDesc + reset + '\n');
46
+ process.stderr.write('\n');
47
+ }
20
48
 
21
49
  const PAYLOAD_SUBPATH = path.join('.claude', 'nubos-pilot');
22
50
  const STATE_SUBPATH = '.nubos-pilot';
@@ -38,16 +66,26 @@ const MANAGED_BLOCK_INNER =
38
66
  + ' for planning and execution.\n\nRun `npx nubos-pilot doctor`'
39
67
  + ' to check install integrity.';
40
68
 
41
- const VALID_AGENTS = ['claude', 'codex', 'gemini', 'opencode'];
69
+ const VALID_AGENTS = registryMod.listRuntimeIds();
42
70
  const VALID_SCOPES = ['local', 'global'];
43
71
 
72
+ function _parseAgentsFlag(value) {
73
+ return String(value || '')
74
+ .split(/[,\s]+/)
75
+ .map((s) => s.trim())
76
+ .filter(Boolean);
77
+ }
78
+
44
79
  function parseInstallFlags(args) {
45
- const flags = { agent: null, scope: null, mcp: false, yes: false };
80
+ const flags = { agent: null, agents: null, scope: null, mcp: false, yes: false };
46
81
  const rest = [];
47
82
  for (let i = 0; i < args.length; i++) {
48
83
  const a = args[i];
49
84
  if (a === '--agent' || a === '-a') { flags.agent = args[++i] || null; continue; }
50
85
  if (a.startsWith('--agent=')) { flags.agent = a.slice('--agent='.length); continue; }
86
+ if (a === '--agents') { flags.agents = _parseAgentsFlag(args[++i]); continue; }
87
+ if (a.startsWith('--agents=')) { flags.agents = _parseAgentsFlag(a.slice('--agents='.length)); continue; }
88
+ if (a === '--all') { flags.agents = VALID_AGENTS.slice(); continue; }
51
89
  if (a === '--scope' || a === '-s') { flags.scope = args[++i] || null; continue; }
52
90
  if (a.startsWith('--scope=')) { flags.scope = a.slice('--scope='.length); continue; }
53
91
  if (a === '--mcp') { flags.mcp = true; continue; }
@@ -59,6 +97,21 @@ function parseInstallFlags(args) {
59
97
  '--agent must be one of: ' + VALID_AGENTS.join(', '),
60
98
  { flag: '--agent', got: flags.agent });
61
99
  }
100
+ if (flags.agents !== null) {
101
+ for (const a of flags.agents) {
102
+ if (!VALID_AGENTS.includes(a)) {
103
+ throw new NubosPilotError('invalid-flag',
104
+ '--agents values must be one of: ' + VALID_AGENTS.join(', '),
105
+ { flag: '--agents', got: a });
106
+ }
107
+ }
108
+ if (flags.agents.length === 0) {
109
+ throw new NubosPilotError('invalid-flag',
110
+ '--agents requires at least one value',
111
+ { flag: '--agents' });
112
+ }
113
+ if (!flags.agent) flags.agent = flags.agents[0];
114
+ }
62
115
  if (flags.scope !== null && !VALID_SCOPES.includes(flags.scope)) {
63
116
  throw new NubosPilotError('invalid-flag',
64
117
  '--scope must be one of: ' + VALID_SCOPES.join(', '),
@@ -123,10 +176,35 @@ function _copyTree(src, dst) {
123
176
  }
124
177
  }
125
178
 
179
+ function _runtimeSelectLabels() {
180
+ return registryMod.RUNTIMES.map((r) => {
181
+ const home = registryMod.runtimeGlobalDir(r).replace(process.env.HOME || '', '~');
182
+ return r.label + ' (' + home + ')';
183
+ });
184
+ }
185
+
126
186
  async function _runInitQuestions(detectedRuntime, askUser, flags) {
127
187
  const f = flags || {};
128
- const runtime = f.agent || (await askUser({ type: 'select', question: 'Welche Runtime nutzt du?',
129
- options: VALID_AGENTS, default: detectedRuntime || 'claude' })).value;
188
+ let runtimes;
189
+ if (f.agents && f.agents.length) {
190
+ runtimes = f.agents.slice();
191
+ } else if (f.agent) {
192
+ runtimes = [f.agent];
193
+ } else {
194
+ const labels = _runtimeSelectLabels();
195
+ const detectedIdx = Math.max(0, VALID_AGENTS.indexOf(detectedRuntime || 'claude'));
196
+ const picked = (await askUser({ type: 'multiselect',
197
+ question: yellow + 'Which runtime(s) would you like to install for?' + reset,
198
+ options: labels, default: [labels[detectedIdx]] })).value;
199
+ runtimes = Array.isArray(picked) && picked.length && typeof picked[0] === 'string'
200
+ && picked[0].includes('(')
201
+ ? picked.map((label) => {
202
+ const idx = labels.indexOf(label);
203
+ return VALID_AGENTS[idx];
204
+ })
205
+ : (Array.isArray(picked) ? picked : [picked]);
206
+ }
207
+ const runtime = runtimes[0];
130
208
  const scope = f.scope || (await askUser({ type: 'select', question: 'Installation scope?',
131
209
  options: VALID_SCOPES, default: 'local' })).value;
132
210
  const model_profile = (await askUser({ type: 'select', question: 'Model-Profile?',
@@ -141,7 +219,7 @@ async function _runInitQuestions(detectedRuntime, askUser, flags) {
141
219
  const plan_checker = (await askUser({ type: 'confirm', question: 'Enable plan_checker?', default: true })).value;
142
220
  const verifier = (await askUser({ type: 'confirm', question: 'Enable verifier?', default: true })).value;
143
221
  const response_language = (await askUser({ type: 'input', question: 'Response language (ISO-639 code)?', default: 'en' })).value;
144
- return { runtime, scope, mcp: !!f.mcp, model_profile, commit_docs, branching_strategy, phase_branch_template,
222
+ return { runtime, runtimes, scope, mcp: !!f.mcp, model_profile, commit_docs, branching_strategy, phase_branch_template,
145
223
  milestone_branch_template, parallelization, research, plan_checker, verifier, response_language };
146
224
  }
147
225
 
@@ -157,7 +235,9 @@ function _repairCodexConfig() {
157
235
  return true;
158
236
  }
159
237
 
160
- function _rewriteManagedMarkdown(projectRoot) {
238
+ const LEGACY_AGENTS = new Set(['claude', 'codex', 'gemini', 'opencode']);
239
+
240
+ function _rewriteManagedMarkdown(projectRoot, runtimes) {
161
241
  const claudePath = path.join(projectRoot, 'CLAUDE.md');
162
242
  const agentsPath = path.join(projectRoot, 'AGENTS.md');
163
243
  const geminiPath = path.join(projectRoot, 'GEMINI.md');
@@ -174,10 +254,12 @@ function _rewriteManagedMarkdown(projectRoot) {
174
254
  } else if (claudeExists) {
175
255
  agentsBase = agentsMdMod.generateAgentsMd(fs.readFileSync(claudePath, 'utf-8'), 'codex');
176
256
  } else {
177
- return;
257
+ agentsBase = null;
258
+ }
259
+ if (agentsBase !== null) {
260
+ const agentsNext = managedBlockMod.rewriteBlock(agentsBase, MANAGED_BLOCK_INNER);
261
+ atomicWriteFileSync(agentsPath, agentsNext);
178
262
  }
179
- const agentsNext = managedBlockMod.rewriteBlock(agentsBase, MANAGED_BLOCK_INNER);
180
- atomicWriteFileSync(agentsPath, agentsNext);
181
263
 
182
264
  let geminiBase;
183
265
  if (fs.existsSync(geminiPath)) {
@@ -185,10 +267,30 @@ function _rewriteManagedMarkdown(projectRoot) {
185
267
  } else if (claudeExists) {
186
268
  geminiBase = agentsMdMod.generateAgentsMd(fs.readFileSync(claudePath, 'utf-8'), 'gemini');
187
269
  } else {
188
- return;
270
+ geminiBase = null;
271
+ }
272
+ if (geminiBase !== null) {
273
+ const geminiNext = managedBlockMod.rewriteBlock(geminiBase, MANAGED_BLOCK_INNER);
274
+ atomicWriteFileSync(geminiPath, geminiNext);
275
+ }
276
+
277
+ const extras = (runtimes || []).filter((id) => !LEGACY_AGENTS.has(id));
278
+ for (const id of extras) {
279
+ const meta = registryMod.getRuntimeMeta(id);
280
+ if (!meta) continue;
281
+ const targetPath = registryMod.runtimeAgentsPath(meta, 'local', projectRoot);
282
+ let base;
283
+ if (fs.existsSync(targetPath)) {
284
+ base = fs.readFileSync(targetPath, 'utf-8');
285
+ } else if (claudeExists) {
286
+ base = agentsMdMod.generateAgentsMd(fs.readFileSync(claudePath, 'utf-8'), 'codex');
287
+ } else {
288
+ base = '';
289
+ }
290
+ const next = managedBlockMod.rewriteBlock(base, MANAGED_BLOCK_INNER);
291
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
292
+ atomicWriteFileSync(targetPath, next);
189
293
  }
190
- const geminiNext = managedBlockMod.rewriteBlock(geminiBase, MANAGED_BLOCK_INNER);
191
- atomicWriteFileSync(geminiPath, geminiNext);
192
294
  }
193
295
 
194
296
  async function runInstall(opts) {
@@ -208,6 +310,7 @@ async function runInstall(opts) {
208
310
 
209
311
  async function _runInstallLocked(ctx) {
210
312
  const { projectRoot, mode, dryRun, askUser, sourceDir, stateDir, flags } = ctx;
313
+ _printBanner();
211
314
  console.error(cyan + '→ nubos-pilot install (mode=' + mode + ')' + reset);
212
315
 
213
316
  const preliminaryScope = (flags && flags.scope) || _readExistingScope(projectRoot) || 'local';
@@ -335,7 +438,8 @@ async function _runInstallLocked(ctx) {
335
438
  }
336
439
  }
337
440
 
338
- _rewriteManagedMarkdown(projectRoot);
441
+ const selectedRuntimes = (initConfig && initConfig.runtimes) || (initConfig ? [initConfig.runtime] : []);
442
+ _rewriteManagedMarkdown(projectRoot, selectedRuntimes);
339
443
 
340
444
  if (initConfig && initConfig.mcp && !dryRun) {
341
445
  try {
@@ -413,8 +517,26 @@ function _runUninstallLocked(projectRoot) {
413
517
 
414
518
  try { fs.rmdirSync(payloadDir); } catch { }
415
519
 
416
- for (const name of ['CLAUDE.md', 'AGENTS.md', 'GEMINI.md']) {
417
- const p = path.join(projectRoot, name);
520
+ const cfgPath = path.join(_stateDirFor(projectRoot), 'config.json');
521
+ let installedRuntimes = [];
522
+ try {
523
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
524
+ installedRuntimes = cfg.runtimes || (cfg.runtime ? [cfg.runtime] : []);
525
+ } catch {}
526
+
527
+ const legacyFiles = ['CLAUDE.md', 'AGENTS.md', 'GEMINI.md'];
528
+ const extraFiles = [];
529
+ for (const id of installedRuntimes) {
530
+ if (LEGACY_AGENTS.has(id)) continue;
531
+ const meta = registryMod.getRuntimeMeta(id);
532
+ if (!meta) continue;
533
+ extraFiles.push(registryMod.runtimeAgentsPath(meta, 'local', projectRoot));
534
+ }
535
+
536
+ const toStrip = legacyFiles
537
+ .map((n) => path.join(projectRoot, n))
538
+ .concat(extraFiles);
539
+ for (const p of toStrip) {
418
540
  if (!fs.existsSync(p)) continue;
419
541
  const stripped = managedBlockMod.stripBlock(fs.readFileSync(p, 'utf-8'));
420
542
  if (!stripped || !stripped.trim()) {
@@ -0,0 +1,197 @@
1
+ 'use strict';
2
+
3
+ const os = require('node:os');
4
+ const path = require('node:path');
5
+
6
+ const RUNTIMES = [
7
+ {
8
+ id: 'claude',
9
+ label: 'Claude Code',
10
+ localDir: '.claude',
11
+ globalDir: ['.claude'],
12
+ envConfigDir: 'CLAUDE_CONFIG_DIR',
13
+ agentsMd: 'CLAUDE.md',
14
+ agentsMdScope: 'project',
15
+ payloadSubdir: 'nubos-pilot',
16
+ },
17
+ {
18
+ id: 'antigravity',
19
+ label: 'Antigravity',
20
+ localDir: '.agent',
21
+ globalDir: ['.gemini', 'antigravity'],
22
+ envConfigDir: 'ANTIGRAVITY_CONFIG_DIR',
23
+ agentsMd: 'AGENTS.md',
24
+ agentsMdScope: 'project',
25
+ payloadSubdir: 'nubos-pilot',
26
+ },
27
+ {
28
+ id: 'augment',
29
+ label: 'Augment',
30
+ localDir: '.augment',
31
+ globalDir: ['.augment'],
32
+ envConfigDir: 'AUGMENT_CONFIG_DIR',
33
+ agentsMd: 'AGENTS.md',
34
+ agentsMdScope: 'project',
35
+ payloadSubdir: 'nubos-pilot',
36
+ },
37
+ {
38
+ id: 'cline',
39
+ label: 'Cline',
40
+ localDir: '.',
41
+ globalDir: ['.cline'],
42
+ envConfigDir: 'CLINE_CONFIG_DIR',
43
+ agentsMd: '.clinerules',
44
+ agentsMdScope: 'project',
45
+ payloadSubdir: '.clinerules-nubos-pilot',
46
+ },
47
+ {
48
+ id: 'codebuddy',
49
+ label: 'CodeBuddy',
50
+ localDir: '.codebuddy',
51
+ globalDir: ['.codebuddy'],
52
+ envConfigDir: 'CODEBUDDY_CONFIG_DIR',
53
+ agentsMd: 'AGENTS.md',
54
+ agentsMdScope: 'project',
55
+ payloadSubdir: 'nubos-pilot',
56
+ },
57
+ {
58
+ id: 'codex',
59
+ label: 'Codex',
60
+ localDir: '.codex',
61
+ globalDir: ['.codex'],
62
+ envConfigDir: 'CODEX_HOME',
63
+ agentsMd: 'AGENTS.md',
64
+ agentsMdScope: 'project',
65
+ payloadSubdir: 'nubos-pilot',
66
+ },
67
+ {
68
+ id: 'copilot',
69
+ label: 'Copilot',
70
+ localDir: '.github',
71
+ globalDir: ['.copilot'],
72
+ envConfigDir: 'COPILOT_CONFIG_DIR',
73
+ agentsMd: 'copilot-instructions.md',
74
+ agentsMdScope: 'dir',
75
+ payloadSubdir: 'nubos-pilot',
76
+ },
77
+ {
78
+ id: 'cursor',
79
+ label: 'Cursor',
80
+ localDir: '.cursor',
81
+ globalDir: ['.cursor'],
82
+ envConfigDir: 'CURSOR_CONFIG_DIR',
83
+ agentsMd: 'rules/nubos-pilot.mdc',
84
+ agentsMdScope: 'dir',
85
+ payloadSubdir: 'nubos-pilot',
86
+ },
87
+ {
88
+ id: 'gemini',
89
+ label: 'Gemini',
90
+ localDir: '.gemini',
91
+ globalDir: ['.gemini'],
92
+ envConfigDir: 'GEMINI_CONFIG_DIR',
93
+ agentsMd: 'GEMINI.md',
94
+ agentsMdScope: 'project',
95
+ payloadSubdir: 'nubos-pilot',
96
+ },
97
+ {
98
+ id: 'kilo',
99
+ label: 'Kilo',
100
+ localDir: '.kilo',
101
+ globalDir: ['.config', 'kilo'],
102
+ envConfigDir: 'KILO_CONFIG_DIR',
103
+ agentsMd: 'AGENTS.md',
104
+ agentsMdScope: 'project',
105
+ payloadSubdir: 'nubos-pilot',
106
+ },
107
+ {
108
+ id: 'opencode',
109
+ label: 'OpenCode',
110
+ localDir: '.opencode',
111
+ globalDir: ['.config', 'opencode'],
112
+ envConfigDir: 'OPENCODE_CONFIG_DIR',
113
+ agentsMd: 'AGENTS.md',
114
+ agentsMdScope: 'dir',
115
+ payloadSubdir: 'nubos-pilot',
116
+ },
117
+ {
118
+ id: 'qwen',
119
+ label: 'Qwen Code',
120
+ localDir: '.qwen',
121
+ globalDir: ['.qwen'],
122
+ envConfigDir: 'QWEN_CONFIG_DIR',
123
+ agentsMd: 'AGENTS.md',
124
+ agentsMdScope: 'project',
125
+ payloadSubdir: 'nubos-pilot',
126
+ },
127
+ {
128
+ id: 'trae',
129
+ label: 'Trae',
130
+ localDir: '.trae',
131
+ globalDir: ['.trae'],
132
+ envConfigDir: 'TRAE_CONFIG_DIR',
133
+ agentsMd: 'AGENTS.md',
134
+ agentsMdScope: 'project',
135
+ payloadSubdir: 'nubos-pilot',
136
+ },
137
+ {
138
+ id: 'windsurf',
139
+ label: 'Windsurf',
140
+ localDir: '.windsurf',
141
+ globalDir: ['.codeium', 'windsurf'],
142
+ envConfigDir: 'WINDSURF_CONFIG_DIR',
143
+ agentsMd: '.windsurfrules',
144
+ agentsMdScope: 'project',
145
+ payloadSubdir: '.windsurf-nubos-pilot',
146
+ },
147
+ ];
148
+
149
+ const RUNTIME_INDEX = new Map(RUNTIMES.map((r) => [r.id, r]));
150
+
151
+ function listRuntimeIds() {
152
+ return RUNTIMES.map((r) => r.id);
153
+ }
154
+
155
+ function getRuntimeMeta(id) {
156
+ return RUNTIME_INDEX.get(id) || null;
157
+ }
158
+
159
+ function runtimeGlobalDir(meta) {
160
+ if (meta.envConfigDir && process.env[meta.envConfigDir]) {
161
+ const v = process.env[meta.envConfigDir];
162
+ return v.startsWith('~') ? path.join(os.homedir(), v.slice(1)) : v;
163
+ }
164
+ return path.join(os.homedir(), ...(meta.globalDir || [meta.localDir]));
165
+ }
166
+
167
+ function runtimeLocalDir(meta, projectRoot) {
168
+ return path.join(projectRoot, meta.localDir || '.');
169
+ }
170
+
171
+ function runtimeConfigDir(meta, scope, projectRoot) {
172
+ return scope === 'global'
173
+ ? runtimeGlobalDir(meta)
174
+ : runtimeLocalDir(meta, projectRoot);
175
+ }
176
+
177
+ function runtimePayloadDir(meta, scope, projectRoot) {
178
+ return path.join(runtimeConfigDir(meta, scope, projectRoot), meta.payloadSubdir);
179
+ }
180
+
181
+ function runtimeAgentsPath(meta, scope, projectRoot) {
182
+ if (meta.agentsMdScope === 'dir') {
183
+ return path.join(runtimeConfigDir(meta, scope, projectRoot), meta.agentsMd);
184
+ }
185
+ return path.join(projectRoot, meta.agentsMd);
186
+ }
187
+
188
+ module.exports = {
189
+ RUNTIMES,
190
+ listRuntimeIds,
191
+ getRuntimeMeta,
192
+ runtimeGlobalDir,
193
+ runtimeLocalDir,
194
+ runtimeConfigDir,
195
+ runtimePayloadDir,
196
+ runtimeAgentsPath,
197
+ };
@@ -6,7 +6,11 @@ const REQUIRED_KEYS = ['name', 'detectHints', 'capabilities', 'paths', 'askUser'
6
6
  const CAP_KEYS = ['askUserQuestion', 'slashCommands', 'agentsMd', 'textMode', 'modelResolution'];
7
7
  const VALID_TEXT_MODE = ['auto', 'force', 'off'];
8
8
  const VALID_MODEL_RES = ['explicit', 'inherit', 'profile'];
9
- const VALID_AGENTS_MD = [null, 'AGENTS.md', 'GEMINI.md', 'CLAUDE.md'];
9
+ const VALID_AGENTS_MD = [
10
+ null, 'AGENTS.md', 'GEMINI.md', 'CLAUDE.md',
11
+ '.clinerules', '.windsurfrules',
12
+ 'rules/nubos-pilot.mdc', 'copilot-instructions.md',
13
+ ];
10
14
 
11
15
  for (const name of listRuntimes()) {
12
16
  test('RT-contract(' + name + '): exports all required keys', () => {
@@ -51,7 +51,7 @@ function _parseAnswer(type, rawLine, options, def) {
51
51
  }
52
52
  if (type === 'multiselect') {
53
53
  if (line === '' && def != null) return def;
54
- const parts = line.split(',').map((s) => s.trim()).filter(Boolean);
54
+ const parts = line.split(/[\s,]+/).map((s) => s.trim()).filter(Boolean);
55
55
  const picks = [];
56
56
  for (const p of parts) {
57
57
  const n = Number(p);
@@ -88,6 +88,36 @@ function _parseAnswer(type, rawLine, options, def) {
88
88
  );
89
89
  }
90
90
 
91
+ const NUBOS_BLUE = '\x1b[38;5;33m';
92
+ const ANSI_RESET = '\x1b[0m';
93
+
94
+ function _defaultDisplay(type, options, def) {
95
+ if (def == null) {
96
+ if (type === 'confirm') return '[y/n]';
97
+ return '';
98
+ }
99
+ if (type === 'confirm') {
100
+ if (def === true) return '[Y/n]';
101
+ if (def === false) return '[y/N]';
102
+ return '[y/n]';
103
+ }
104
+ if (type === 'select') {
105
+ if (options) {
106
+ const idx = options.indexOf(def);
107
+ if (idx >= 0) return '[' + (idx + 1) + ']';
108
+ }
109
+ return '[' + String(def) + ']';
110
+ }
111
+ if (type === 'multiselect') {
112
+ if (Array.isArray(def) && options) {
113
+ const idxs = def.map((v) => options.indexOf(v));
114
+ if (idxs.every((i) => i >= 0)) return '[' + idxs.map((i) => i + 1).join(',') + ']';
115
+ }
116
+ return '[' + (Array.isArray(def) ? def.join(',') : String(def)) + ']';
117
+ }
118
+ return '[' + String(def) + ']';
119
+ }
120
+
91
121
  async function askUserReadline({ type, question, options, def }) {
92
122
  const hasTTY = !!process.stdin.isTTY;
93
123
  if (!hasTTY && !_readlineImpl) {
@@ -98,20 +128,25 @@ async function askUserReadline({ type, question, options, def }) {
98
128
  { question },
99
129
  );
100
130
  }
101
- process.stderr.write(question + '\n');
131
+ process.stderr.write('\n');
132
+ process.stderr.write(' ' + question + '\n');
133
+ process.stderr.write('\n');
102
134
  if (type === 'select' || type === 'multiselect') {
103
135
  if (options) {
104
136
  for (let i = 0; i < options.length; i++) {
105
- process.stderr.write(' ' + (i + 1) + ') ' + String(options[i]) + '\n');
137
+ process.stderr.write(
138
+ ' ' + NUBOS_BLUE + (i + 1) + ')' + ANSI_RESET + ' ' + String(options[i]) + '\n',
139
+ );
106
140
  }
107
141
  }
108
- if (type === 'multiselect') process.stderr.write('(comma-separated indices) ');
109
- else process.stderr.write('> ');
110
- } else if (type === 'confirm') {
111
- process.stderr.write('[y/n] ');
112
- } else {
113
- process.stderr.write('> ');
142
+ process.stderr.write('\n');
143
+ if (type === 'multiselect') {
144
+ process.stderr.write(' Select multiple: 1,2,6 or 1 2 6\n');
145
+ process.stderr.write('\n');
146
+ }
114
147
  }
148
+ const marker = _defaultDisplay(type, options, def);
149
+ process.stderr.write(' Choice' + (marker ? ' ' + marker : '') + ': ');
115
150
  const line = await _readOneLine();
116
151
  return { value: _parseAnswer(type, line, options, def), source: 'readline' };
117
152
  }
@@ -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: 'antigravity',
14
+ detectHints: {
15
+ env: ['ANTIGRAVITY_CONFIG_DIR'],
16
+ pathBinary: 'antigravity',
17
+ diskMarkers: ['.agent/', '.gemini/antigravity/'],
18
+ },
19
+ capabilities: {
20
+ askUserQuestion: false,
21
+ slashCommands: false,
22
+ agentsMd: 'AGENTS.md',
23
+ textMode: 'auto',
24
+ modelResolution: 'inherit',
25
+ },
26
+ paths: {
27
+ payload: '.agent/nubos-pilot/',
28
+ config: null,
29
+ agentsMd: 'AGENTS.md',
30
+ },
31
+ runtimeNotice:
32
+ '> **Runtime-Hinweis:** Diese Datei wird von Antigravity konsumiert. '
33
+ + 'Interaktive Prompts laufen über readline (stderr).',
34
+ askUser,
35
+ };
@@ -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: 'augment',
14
+ detectHints: {
15
+ env: ['AUGMENT_CONFIG_DIR'],
16
+ pathBinary: 'augment',
17
+ diskMarkers: ['.augment/'],
18
+ },
19
+ capabilities: {
20
+ askUserQuestion: false,
21
+ slashCommands: false,
22
+ agentsMd: 'AGENTS.md',
23
+ textMode: 'auto',
24
+ modelResolution: 'inherit',
25
+ },
26
+ paths: {
27
+ payload: '.augment/nubos-pilot/',
28
+ config: null,
29
+ agentsMd: 'AGENTS.md',
30
+ },
31
+ runtimeNotice:
32
+ '> **Runtime-Hinweis:** Diese Datei wird von Augment konsumiert. '
33
+ + 'Interaktive Prompts laufen über readline (stderr).',
34
+ askUser,
35
+ };
@@ -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: 'cline',
14
+ detectHints: {
15
+ env: ['CLINE_CONFIG_DIR'],
16
+ pathBinary: 'cline',
17
+ diskMarkers: ['.clinerules'],
18
+ },
19
+ capabilities: {
20
+ askUserQuestion: false,
21
+ slashCommands: false,
22
+ agentsMd: '.clinerules',
23
+ textMode: 'auto',
24
+ modelResolution: 'inherit',
25
+ },
26
+ paths: {
27
+ payload: '.clinerules-nubos-pilot/',
28
+ config: null,
29
+ agentsMd: '.clinerules',
30
+ },
31
+ runtimeNotice:
32
+ '> **Runtime-Hinweis:** Diese Datei (.clinerules) wird von Cline konsumiert. '
33
+ + 'Interaktive Prompts laufen über readline (stderr).',
34
+ askUser,
35
+ };
@@ -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: 'codebuddy',
14
+ detectHints: {
15
+ env: ['CODEBUDDY_CONFIG_DIR'],
16
+ pathBinary: 'codebuddy',
17
+ diskMarkers: ['.codebuddy/'],
18
+ },
19
+ capabilities: {
20
+ askUserQuestion: false,
21
+ slashCommands: false,
22
+ agentsMd: 'AGENTS.md',
23
+ textMode: 'auto',
24
+ modelResolution: 'inherit',
25
+ },
26
+ paths: {
27
+ payload: '.codebuddy/nubos-pilot/',
28
+ config: null,
29
+ agentsMd: 'AGENTS.md',
30
+ },
31
+ runtimeNotice:
32
+ '> **Runtime-Hinweis:** Diese Datei wird von CodeBuddy konsumiert. '
33
+ + 'Interaktive Prompts laufen über readline (stderr).',
34
+ askUser,
35
+ };
@@ -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: 'copilot',
14
+ detectHints: {
15
+ env: ['COPILOT_CONFIG_DIR'],
16
+ pathBinary: 'copilot',
17
+ diskMarkers: ['.github/copilot-instructions.md', '.copilot/'],
18
+ },
19
+ capabilities: {
20
+ askUserQuestion: false,
21
+ slashCommands: false,
22
+ agentsMd: 'copilot-instructions.md',
23
+ textMode: 'auto',
24
+ modelResolution: 'inherit',
25
+ },
26
+ paths: {
27
+ payload: '.github/nubos-pilot/',
28
+ config: null,
29
+ agentsMd: '.github/copilot-instructions.md',
30
+ },
31
+ runtimeNotice:
32
+ '> **Runtime-Hinweis:** Diese Datei (.github/copilot-instructions.md) wird von GitHub Copilot konsumiert. '
33
+ + 'Interaktive Prompts laufen über readline (stderr).',
34
+ askUser,
35
+ };
@@ -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: 'cursor',
14
+ detectHints: {
15
+ env: ['CURSOR_CONFIG_DIR'],
16
+ pathBinary: 'cursor',
17
+ diskMarkers: ['.cursor/', '.cursorrules'],
18
+ },
19
+ capabilities: {
20
+ askUserQuestion: false,
21
+ slashCommands: false,
22
+ agentsMd: 'rules/nubos-pilot.mdc',
23
+ textMode: 'auto',
24
+ modelResolution: 'inherit',
25
+ },
26
+ paths: {
27
+ payload: '.cursor/nubos-pilot/',
28
+ config: null,
29
+ agentsMd: '.cursor/rules/nubos-pilot.mdc',
30
+ },
31
+ runtimeNotice:
32
+ '> **Runtime-Hinweis:** Diese Datei (.cursor/rules/nubos-pilot.mdc) wird von Cursor konsumiert. '
33
+ + 'Interaktive Prompts laufen über readline (stderr).',
34
+ askUser,
35
+ };
@@ -3,7 +3,11 @@ const path = require('node:path');
3
3
  const { NubosPilotError } = require('../core.cjs');
4
4
  const { getRuntime: _askuserGetRuntime } = require('../askuser.cjs');
5
5
 
6
- const KNOWN_RUNTIMES = ['claude', 'opencode', 'codex', 'gemini'];
6
+ const KNOWN_RUNTIMES = [
7
+ 'claude', 'antigravity', 'augment', 'cline', 'codebuddy',
8
+ 'codex', 'copilot', 'cursor', 'gemini', 'kilo',
9
+ 'opencode', 'qwen', 'trae', 'windsurf',
10
+ ];
7
11
 
8
12
  function listRuntimes() {
9
13
  return KNOWN_RUNTIMES.slice();
@@ -47,15 +47,21 @@ function writeConfig(cwd, json) {
47
47
  fs.writeFileSync(path.join(dir, 'config.json'), JSON.stringify(json), 'utf-8');
48
48
  }
49
49
 
50
- test('RTI-1: listRuntimes returns four known runtimes in canonical order', () => {
51
- assert.deepEqual(rt.listRuntimes(), ['claude', 'opencode', 'codex', 'gemini']);
50
+ const EXPECTED_RUNTIMES = [
51
+ 'claude', 'antigravity', 'augment', 'cline', 'codebuddy',
52
+ 'codex', 'copilot', 'cursor', 'gemini', 'kilo',
53
+ 'opencode', 'qwen', 'trae', 'windsurf',
54
+ ];
55
+
56
+ test('RTI-1: listRuntimes returns 14 known runtimes in canonical order', () => {
57
+ assert.deepEqual(rt.listRuntimes(), EXPECTED_RUNTIMES);
52
58
  });
53
59
 
54
60
  test('RTI-2: listRuntimes returns defensive copy — mutation does not leak', () => {
55
61
  const a = rt.listRuntimes();
56
62
  a.push('pwned');
57
63
  const b = rt.listRuntimes();
58
- assert.deepEqual(b, ['claude', 'opencode', 'codex', 'gemini']);
64
+ assert.deepEqual(b, EXPECTED_RUNTIMES);
59
65
  });
60
66
 
61
67
  test('RTI-3: getAdapter("nonexistent") throws NubosPilotError with code runtime-unknown', () => {
@@ -71,7 +77,7 @@ test('RTI-4: getAdapter unknown-runtime error details.known lists KNOWN_RUNTIMES
71
77
  assert.fail('should throw');
72
78
  } catch (err) {
73
79
  assert.equal(err.code, 'runtime-unknown');
74
- assert.deepEqual(err.details.known, ['claude', 'opencode', 'codex', 'gemini']);
80
+ assert.deepEqual(err.details.known, EXPECTED_RUNTIMES);
75
81
  assert.equal(err.details.name, 'mystery');
76
82
  }
77
83
  });
@@ -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: 'kilo',
14
+ detectHints: {
15
+ env: ['KILO_CONFIG_DIR'],
16
+ pathBinary: 'kilo',
17
+ diskMarkers: ['.kilo/', '.config/kilo/'],
18
+ },
19
+ capabilities: {
20
+ askUserQuestion: false,
21
+ slashCommands: false,
22
+ agentsMd: 'AGENTS.md',
23
+ textMode: 'auto',
24
+ modelResolution: 'inherit',
25
+ },
26
+ paths: {
27
+ payload: '.kilo/nubos-pilot/',
28
+ config: null,
29
+ agentsMd: 'AGENTS.md',
30
+ },
31
+ runtimeNotice:
32
+ '> **Runtime-Hinweis:** Diese Datei wird von Kilo konsumiert. '
33
+ + 'Interaktive Prompts laufen über readline (stderr).',
34
+ askUser,
35
+ };
@@ -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: 'qwen',
14
+ detectHints: {
15
+ env: ['QWEN_CONFIG_DIR'],
16
+ pathBinary: 'qwen',
17
+ diskMarkers: ['.qwen/'],
18
+ },
19
+ capabilities: {
20
+ askUserQuestion: false,
21
+ slashCommands: false,
22
+ agentsMd: 'AGENTS.md',
23
+ textMode: 'auto',
24
+ modelResolution: 'inherit',
25
+ },
26
+ paths: {
27
+ payload: '.qwen/nubos-pilot/',
28
+ config: null,
29
+ agentsMd: 'AGENTS.md',
30
+ },
31
+ runtimeNotice:
32
+ '> **Runtime-Hinweis:** Diese Datei wird von Qwen Code konsumiert. '
33
+ + 'Interaktive Prompts laufen über readline (stderr).',
34
+ askUser,
35
+ };
@@ -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: 'trae',
14
+ detectHints: {
15
+ env: ['TRAE_CONFIG_DIR'],
16
+ pathBinary: 'trae',
17
+ diskMarkers: ['.trae/'],
18
+ },
19
+ capabilities: {
20
+ askUserQuestion: false,
21
+ slashCommands: false,
22
+ agentsMd: 'AGENTS.md',
23
+ textMode: 'auto',
24
+ modelResolution: 'inherit',
25
+ },
26
+ paths: {
27
+ payload: '.trae/nubos-pilot/',
28
+ config: null,
29
+ agentsMd: 'AGENTS.md',
30
+ },
31
+ runtimeNotice:
32
+ '> **Runtime-Hinweis:** Diese Datei wird von Trae konsumiert. '
33
+ + 'Interaktive Prompts laufen über readline (stderr).',
34
+ askUser,
35
+ };
@@ -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: 'windsurf',
14
+ detectHints: {
15
+ env: ['WINDSURF_CONFIG_DIR'],
16
+ pathBinary: 'windsurf',
17
+ diskMarkers: ['.windsurf/', '.windsurfrules', '.codeium/windsurf/'],
18
+ },
19
+ capabilities: {
20
+ askUserQuestion: false,
21
+ slashCommands: false,
22
+ agentsMd: '.windsurfrules',
23
+ textMode: 'auto',
24
+ modelResolution: 'inherit',
25
+ },
26
+ paths: {
27
+ payload: '.windsurf-nubos-pilot/',
28
+ config: null,
29
+ agentsMd: '.windsurfrules',
30
+ },
31
+ runtimeNotice:
32
+ '> **Runtime-Hinweis:** Diese Datei (.windsurfrules) wird von Windsurf konsumiert. '
33
+ + 'Interaktive Prompts laufen über readline (stderr).',
34
+ askUser,
35
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nubos-pilot",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "AI-driven planning and execution tool for code projects",
5
5
  "homepage": "https://github.com/Nubos-AI/nubos-pilot",
6
6
  "repository": {