nubos-pilot 0.2.1 → 0.3.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/bin/install.js CHANGED
@@ -15,6 +15,7 @@ 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
17
  const registryMod = require('../lib/install/runtimes-registry.cjs');
18
+ const runtimeAssetsMod = require('../lib/install/runtime-assets.cjs');
18
19
 
19
20
  const cyan = '\x1b[36m', green = '\x1b[32m', yellow = '\x1b[33m',
20
21
  red = '\x1b[31m', blue = '\x1b[38;5;33m',
@@ -53,6 +54,8 @@ const OPENCODE_SUBPATH = path.join('.opencode', 'nubos-pilot');
53
54
  const OPENCODE_MANIFEST_PREFIX = '.opencode/nubos-pilot/';
54
55
  const SOURCE_OPENCODE_DIR = path.join(__dirname, '..', 'templates', 'opencode', 'payload');
55
56
  const OPENCODE_JSON_TEMPLATE = path.join(__dirname, '..', 'templates', 'opencode', 'opencode.json');
57
+ const SOURCE_WORKFLOWS_DIR = path.join(__dirname, '..', 'workflows');
58
+ const SOURCE_AGENTS_DIR = path.join(__dirname, '..', 'agents');
56
59
 
57
60
  function _autoAskUser(spec) {
58
61
  return Promise.resolve({
@@ -149,6 +152,17 @@ function _readExistingScope(projectRoot) {
149
152
  } catch { return null; }
150
153
  }
151
154
 
155
+ function _readExistingRuntimes(projectRoot) {
156
+ const cfgPath = path.join(_stateDirFor(projectRoot), 'config.json');
157
+ if (!fs.existsSync(cfgPath)) return null;
158
+ try {
159
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
160
+ if (Array.isArray(cfg.runtimes) && cfg.runtimes.length) return cfg.runtimes.slice();
161
+ if (cfg.runtime) return [cfg.runtime];
162
+ return null;
163
+ } catch { return null; }
164
+ }
165
+
152
166
  function detectMode(projectRoot, scope) {
153
167
  const s = scope || _readExistingScope(projectRoot) || 'local';
154
168
  const payloadDir = _payloadDirFor(projectRoot, s);
@@ -194,7 +208,7 @@ async function _runInitQuestions(detectedRuntime, askUser, flags) {
194
208
  const labels = _runtimeSelectLabels();
195
209
  const detectedIdx = Math.max(0, VALID_AGENTS.indexOf(detectedRuntime || 'claude'));
196
210
  const picked = (await askUser({ type: 'multiselect',
197
- question: yellow + 'Which runtime(s) would you like to install for?' + reset,
211
+ question: 'Which runtime(s) would you like to install for?',
198
212
  options: labels, default: [labels[detectedIdx]] })).value;
199
213
  runtimes = Array.isArray(picked) && picked.length && typeof picked[0] === 'string'
200
214
  && picked[0].includes('(')
@@ -344,13 +358,31 @@ async function _runInstallLocked(ctx) {
344
358
  try { pkgVersion = String(require('../package.json').version || '0.0.0'); } catch {}
345
359
  const newManifest = manifestMod.buildManifest(tmp, pkgVersion);
346
360
 
361
+ const selectedRuntimesEarly = (initConfig && initConfig.runtimes)
362
+ || (initConfig ? [initConfig.runtime] : null)
363
+ || _readExistingRuntimes(projectRoot)
364
+ || [];
365
+ const opencodeSelected = selectedRuntimesEarly.includes('opencode');
366
+
367
+ const assetPlans = runtimeAssetsMod.planRuntimeAssets({
368
+ selectedRuntimes: selectedRuntimesEarly,
369
+ scope: resolvedScope,
370
+ projectRoot,
371
+ workflowsDir: SOURCE_WORKFLOWS_DIR,
372
+ agentsDir: SOURCE_AGENTS_DIR,
373
+ });
374
+ const assetEntries = runtimeAssetsMod.manifestEntriesForPlans(assetPlans);
375
+ for (const k of Object.keys(assetEntries)) {
376
+ newManifest.files[k] = assetEntries[k];
377
+ }
378
+
347
379
  const opencodeTarget = _opencodePayloadDirFor(projectRoot, resolvedScope);
348
380
  const opencodeManifestPrefix = _opencodeManifestPrefix(resolvedScope);
349
381
  const opencodeTmp = path.join(stateDir, '.opencode.tmp');
350
382
  try { fs.rmSync(opencodeTmp, { recursive: true, force: true }); } catch {}
351
383
  try {
352
384
  let opencodeManifest = null;
353
- if (fs.existsSync(SOURCE_OPENCODE_DIR)) {
385
+ if (opencodeSelected && fs.existsSync(SOURCE_OPENCODE_DIR)) {
354
386
  _copyTree(SOURCE_OPENCODE_DIR, opencodeTmp);
355
387
  opencodeManifest = manifestMod.buildManifest(opencodeTmp, pkgVersion);
356
388
  for (const rel of Object.keys(opencodeManifest.files)) {
@@ -388,7 +420,7 @@ async function _runInstallLocked(ctx) {
388
420
  wouldWrite: Object.keys(newManifest.files).length,
389
421
  wouldBackup: backupLog.length, wouldDelete: diff.stale.length,
390
422
  wouldWriteGemini: true,
391
- wouldWriteOpencodeJson: !fs.existsSync(path.join(projectRoot, 'opencode.json')),
423
+ wouldWriteOpencodeJson: opencodeSelected && !fs.existsSync(path.join(projectRoot, 'opencode.json')),
392
424
  stale: diff.stale, changed: diff.changed, added: diff.added };
393
425
  process.stdout.write(JSON.stringify(summary, null, 2) + '\n');
394
426
  try { stagingMod.cleanStaleStaging(payloadBase); } catch {}
@@ -436,11 +468,31 @@ async function _runInstallLocked(ctx) {
436
468
  try { fs.unlinkSync(relFs); } catch {}
437
469
  }
438
470
  }
471
+ } else if (!opencodeSelected && fs.existsSync(opencodeTarget)) {
472
+ try { fs.rmSync(opencodeTarget, { recursive: true, force: true }); } catch {}
473
+ const opencodeParent = path.dirname(opencodeTarget);
474
+ try { fs.rmdirSync(opencodeParent); } catch {}
475
+ const projectOpencodeJson = path.join(projectRoot, 'opencode.json');
476
+ if (fs.existsSync(projectOpencodeJson) && fs.existsSync(OPENCODE_JSON_TEMPLATE)) {
477
+ try {
478
+ const template = fs.readFileSync(OPENCODE_JSON_TEMPLATE, 'utf-8');
479
+ const existing = fs.readFileSync(projectOpencodeJson, 'utf-8');
480
+ if (existing === template) fs.unlinkSync(projectOpencodeJson);
481
+ } catch {}
482
+ }
439
483
  }
440
484
 
441
485
  const selectedRuntimes = (initConfig && initConfig.runtimes) || (initConfig ? [initConfig.runtime] : []);
442
486
  _rewriteManagedMarkdown(projectRoot, selectedRuntimes);
443
487
 
488
+ if (assetPlans.length) {
489
+ runtimeAssetsMod.writeRuntimeAssets(assetPlans);
490
+ }
491
+ const assetStale = diff.stale.filter(runtimeAssetsMod.isAssetManifestKey);
492
+ if (assetStale.length) {
493
+ runtimeAssetsMod.removeStaleAssets(assetStale, resolvedScope, projectRoot);
494
+ }
495
+
444
496
  if (initConfig && initConfig.mcp && !dryRun) {
445
497
  try {
446
498
  const mcpWriter = require('../lib/install/mcp-writer.cjs');
@@ -455,10 +507,12 @@ async function _runInstallLocked(ctx) {
455
507
  }
456
508
  }
457
509
 
458
- const projectOpencodeJson = path.join(projectRoot, 'opencode.json');
459
- if (!fs.existsSync(projectOpencodeJson) && fs.existsSync(OPENCODE_JSON_TEMPLATE)) {
460
- const template = fs.readFileSync(OPENCODE_JSON_TEMPLATE, 'utf-8');
461
- atomicWriteFileSync(projectOpencodeJson, template);
510
+ if (opencodeSelected) {
511
+ const projectOpencodeJson = path.join(projectRoot, 'opencode.json');
512
+ if (!fs.existsSync(projectOpencodeJson) && fs.existsSync(OPENCODE_JSON_TEMPLATE)) {
513
+ const template = fs.readFileSync(OPENCODE_JSON_TEMPLATE, 'utf-8');
514
+ atomicWriteFileSync(projectOpencodeJson, template);
515
+ }
462
516
  }
463
517
 
464
518
  try { _repairCodexConfig(); } catch (err) {
@@ -503,15 +557,34 @@ function _runUninstallLocked(projectRoot) {
503
557
  }
504
558
  }
505
559
 
560
+ const payloadBase = scope === 'global' ? os.homedir() : projectRoot;
506
561
  let removed = 0;
562
+ const assetDirs = new Set();
507
563
  for (const rel of Object.keys(manifest.files)) {
508
- const abs = path.join(payloadDir, rel);
509
- try { fs.unlinkSync(abs); removed++; } catch (err) {
564
+ const isAsset = runtimeAssetsMod.isAssetManifestKey(rel);
565
+ const abs = isAsset ? path.join(payloadBase, rel) : path.join(payloadDir, rel);
566
+ try {
567
+ fs.unlinkSync(abs);
568
+ removed++;
569
+ if (isAsset) assetDirs.add(path.dirname(abs));
570
+ } catch (err) {
510
571
  if (err && err.code !== 'ENOENT') {
511
572
  console.error(yellow + ' [uninstall] ' + rel + ' not removed: ' + err.message + reset);
512
573
  }
513
574
  }
514
575
  }
576
+ const sortedDirs = Array.from(assetDirs).sort((a, b) => b.length - a.length);
577
+ for (const dir of sortedDirs) {
578
+ let cur = dir;
579
+ while (cur && cur.startsWith(payloadBase) && cur !== payloadBase) {
580
+ try {
581
+ const entries = fs.readdirSync(cur);
582
+ if (entries.length > 0) break;
583
+ fs.rmdirSync(cur);
584
+ } catch { break; }
585
+ cur = path.dirname(cur);
586
+ }
587
+ }
515
588
 
516
589
  try { fs.unlinkSync(path.join(payloadDir, '.manifest.json')); } catch {}
517
590
 
@@ -0,0 +1,145 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+ const crypto = require('node:crypto');
7
+ const registryMod = require('./runtimes-registry.cjs');
8
+
9
+ function _hashFile(file) {
10
+ const h = crypto.createHash('sha256');
11
+ h.update(fs.readFileSync(file));
12
+ return h.digest('hex');
13
+ }
14
+
15
+ function _listMarkdown(dir) {
16
+ if (!fs.existsSync(dir)) return [];
17
+ return fs.readdirSync(dir)
18
+ .filter((n) => n.endsWith('.md'))
19
+ .sort();
20
+ }
21
+
22
+ function _payloadBase(scope, projectRoot) {
23
+ return scope === 'global' ? os.homedir() : projectRoot;
24
+ }
25
+
26
+ function _toPosix(p) {
27
+ return p.split(path.sep).join('/');
28
+ }
29
+
30
+ function planRuntimeAssets({ selectedRuntimes, scope, projectRoot, workflowsDir, agentsDir }) {
31
+ const base = _payloadBase(scope, projectRoot);
32
+ const workflows = _listMarkdown(workflowsDir);
33
+ const agents = _listMarkdown(agentsDir);
34
+ const plans = [];
35
+ for (const id of selectedRuntimes || []) {
36
+ const meta = registryMod.getRuntimeMeta(id);
37
+ if (!meta) continue;
38
+ const configDir = registryMod.runtimeConfigDir(meta, scope, projectRoot);
39
+ if (meta.commandsSubdir) {
40
+ for (const file of workflows) {
41
+ const targetFile = path.join(configDir, meta.commandsSubdir, file);
42
+ plans.push({
43
+ runtime: id,
44
+ kind: 'command',
45
+ sourceFile: path.join(workflowsDir, file),
46
+ targetFile,
47
+ manifestKey: _toPosix(path.relative(base, targetFile)),
48
+ });
49
+ }
50
+ }
51
+ if (meta.agentsSubdir) {
52
+ for (const file of agents) {
53
+ const targetFile = path.join(configDir, meta.agentsSubdir, file);
54
+ plans.push({
55
+ runtime: id,
56
+ kind: 'agent',
57
+ sourceFile: path.join(agentsDir, file),
58
+ targetFile,
59
+ manifestKey: _toPosix(path.relative(base, targetFile)),
60
+ });
61
+ }
62
+ }
63
+ }
64
+ return plans;
65
+ }
66
+
67
+ function manifestEntriesForPlans(plans) {
68
+ const entries = Object.create(null);
69
+ for (const plan of plans) {
70
+ entries[plan.manifestKey] = _hashFile(plan.sourceFile);
71
+ }
72
+ return entries;
73
+ }
74
+
75
+ function writeRuntimeAssets(plans) {
76
+ const written = [];
77
+ for (const plan of plans) {
78
+ fs.mkdirSync(path.dirname(plan.targetFile), { recursive: true });
79
+ fs.copyFileSync(plan.sourceFile, plan.targetFile);
80
+ written.push(plan.targetFile);
81
+ }
82
+ return written;
83
+ }
84
+
85
+ function removeStaleAssets(staleKeys, scope, projectRoot) {
86
+ const base = _payloadBase(scope, projectRoot);
87
+ const removed = [];
88
+ const dirs = new Set();
89
+ for (const key of staleKeys || []) {
90
+ if (!_isAssetKey(key)) continue;
91
+ const abs = path.join(base, key);
92
+ try {
93
+ fs.unlinkSync(abs);
94
+ removed.push(abs);
95
+ dirs.add(path.dirname(abs));
96
+ } catch {}
97
+ }
98
+ _pruneEmptyDirs(dirs, base);
99
+ return removed;
100
+ }
101
+
102
+ function _isAssetKey(key) {
103
+ if (typeof key !== 'string') return false;
104
+ if (key.startsWith('~/')) return true;
105
+ if (key.startsWith('.')) {
106
+ if (key.startsWith('.claude/commands/')) return true;
107
+ if (key.startsWith('.claude/agents/')) return true;
108
+ for (const meta of registryMod.RUNTIMES) {
109
+ if (meta.commandsSubdir) {
110
+ if (key.startsWith(meta.localDir + '/' + meta.commandsSubdir + '/')) return true;
111
+ }
112
+ if (meta.agentsSubdir) {
113
+ if (key.startsWith(meta.localDir + '/' + meta.agentsSubdir + '/')) return true;
114
+ }
115
+ }
116
+ }
117
+ return false;
118
+ }
119
+
120
+ function _pruneEmptyDirs(dirSet, base) {
121
+ const sorted = Array.from(dirSet).sort((a, b) => b.length - a.length);
122
+ for (const dir of sorted) {
123
+ let cur = dir;
124
+ while (cur && cur.startsWith(base) && cur !== base) {
125
+ try {
126
+ const entries = fs.readdirSync(cur);
127
+ if (entries.length > 0) break;
128
+ fs.rmdirSync(cur);
129
+ } catch { break; }
130
+ cur = path.dirname(cur);
131
+ }
132
+ }
133
+ }
134
+
135
+ function isAssetManifestKey(key) {
136
+ return _isAssetKey(key);
137
+ }
138
+
139
+ module.exports = {
140
+ planRuntimeAssets,
141
+ manifestEntriesForPlans,
142
+ writeRuntimeAssets,
143
+ removeStaleAssets,
144
+ isAssetManifestKey,
145
+ };
@@ -13,6 +13,8 @@ const RUNTIMES = [
13
13
  agentsMd: 'CLAUDE.md',
14
14
  agentsMdScope: 'project',
15
15
  payloadSubdir: 'nubos-pilot',
16
+ commandsSubdir: 'commands/np',
17
+ agentsSubdir: 'agents',
16
18
  },
17
19
  {
18
20
  id: 'antigravity',
@@ -113,6 +115,8 @@ const RUNTIMES = [
113
115
  agentsMd: 'AGENTS.md',
114
116
  agentsMdScope: 'dir',
115
117
  payloadSubdir: 'nubos-pilot',
118
+ commandsSubdir: 'command/np',
119
+ agentsSubdir: 'agent',
116
120
  },
117
121
  {
118
122
  id: 'qwen',
@@ -89,8 +89,13 @@ function _parseAnswer(type, rawLine, options, def) {
89
89
  }
90
90
 
91
91
  const NUBOS_BLUE = '\x1b[38;5;33m';
92
+ const ANSI_YELLOW = '\x1b[33m';
92
93
  const ANSI_RESET = '\x1b[0m';
93
94
 
95
+ function _stripAnsi(s) {
96
+ return String(s).replace(/\x1b\[[0-9;]*m/g, '');
97
+ }
98
+
94
99
  function _defaultDisplay(type, options, def) {
95
100
  if (def == null) {
96
101
  if (type === 'confirm') return '[y/n]';
@@ -129,7 +134,7 @@ async function askUserReadline({ type, question, options, def }) {
129
134
  );
130
135
  }
131
136
  process.stderr.write('\n');
132
- process.stderr.write(' ' + question + '\n');
137
+ process.stderr.write(' ' + ANSI_YELLOW + _stripAnsi(question) + ANSI_RESET + '\n');
133
138
  process.stderr.write('\n');
134
139
  if (type === 'select' || type === 'multiselect') {
135
140
  if (options) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nubos-pilot",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
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": {