nubos-pilot 0.5.8 → 0.5.9

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
@@ -17,6 +17,7 @@ const backupMod = require('../lib/install/backup.cjs');
17
17
  const registryMod = require('../lib/install/runtimes-registry.cjs');
18
18
  const runtimeAssetsMod = require('../lib/install/runtime-assets.cjs');
19
19
  const languageMod = require('../lib/language.cjs');
20
+ const configDefaults = require('../lib/config-defaults.cjs');
20
21
 
21
22
  const cyan = '\x1b[36m', green = '\x1b[32m', yellow = '\x1b[33m',
22
23
  red = '\x1b[31m', blue = '\x1b[38;5;33m',
@@ -248,16 +249,11 @@ async function _runInitQuestions(detectedRuntime, askUser, flags) {
248
249
  const model_profile = (await askUser({ type: 'select', question: 'Model-Profile?',
249
250
  options: ['frontier', 'quality', 'balanced', 'budget', 'inherit'], default: 'frontier' })).value;
250
251
  const response_language = (await askUser({ type: 'input', question: 'Response language (ISO-639 code)?', default: 'en' })).value;
251
- return {
252
+ return configDefaults.buildInstallConfig({
252
253
  runtime, runtimes, scope, mcp: !!f.mcp,
253
254
  model_profile,
254
255
  response_language,
255
- commit_docs: true,
256
- parallelization: true,
257
- research: true,
258
- plan_checker: true,
259
- verifier: true,
260
- };
256
+ });
261
257
  }
262
258
 
263
259
  function _repairCodexConfig() {
@@ -48,6 +48,7 @@ const COMMANDS = [
48
48
  { name: 'generate-slug', category: 'Utility', description: 'Slugify text via lib/layout.cjs.slugify' },
49
49
  { name: 'stats', category: 'Utility', description: 'Aggregated project stats (roadmap + STATE + git + metrics JSON shape)' },
50
50
  { name: 'detect-runtime', category: 'Utility', description: 'Print detected runtime id (claude, codex, gemini, …) — reads config.json ∨ env ∨ default' },
51
+ { name: 'template-path', category: 'Utility', description: 'Print absolute path to a package-shipped template by name (e.g. VALIDATION, milestone/CONTEXT)' },
51
52
 
52
53
  { name: 'thread', category: 'Utility', description: 'Cross-session thread CRUD (create/resume under .nubos-pilot/threads/)' },
53
54
  { name: 'session-report', category: 'Utility', description: 'Generate session report from metrics since .last-session pointer' },
@@ -1,6 +1,7 @@
1
1
  const { execFileSync } = require('node:child_process');
2
+ const fs = require('node:fs');
2
3
  const path = require('node:path');
3
- const { NubosPilotError } = require('../../lib/core.cjs');
4
+ const { NubosPilotError, findProjectRoot } = require('../../lib/core.cjs');
4
5
  const { assertCommittablePaths } = require('../../lib/git.cjs');
5
6
  const { resolveCommitArtifacts } = require('../../lib/commit-policy.cjs');
6
7
 
@@ -47,19 +48,34 @@ function _parseArgs(argv) {
47
48
  return { msg, files };
48
49
  }
49
50
 
50
- function _validateFiles(files) {
51
- for (const f of files) {
51
+ function _realpathOrResolve(p) {
52
+ try { return fs.realpathSync(p); } catch { return path.resolve(p); }
53
+ }
54
+
55
+ function _normalizeFiles(files, cwd, root) {
56
+ const realRoot = _realpathOrResolve(root);
57
+ return files.map((f) => {
52
58
  if (typeof f !== 'string' || f.length === 0) {
53
59
  throw new NubosPilotError('commit-invalid-path', 'commit path must be non-empty string', { path: f });
54
60
  }
55
- const segments = String(f).split(/[/\\]/);
56
- for (const seg of segments) {
57
- if (seg === '..') {
58
- throw new NubosPilotError('commit-path-traversal', 'commit path must not contain ".." segments', { path: f });
59
- }
61
+ const abs = path.isAbsolute(f) ? path.resolve(f) : path.resolve(cwd, f);
62
+ const realAbs = _realpathOrResolve(abs);
63
+ const rel = path.relative(realRoot, realAbs);
64
+ if (rel === '' || rel.startsWith('..') || path.isAbsolute(rel)) {
65
+ throw new NubosPilotError(
66
+ 'commit-path-outside-project',
67
+ 'commit path must resolve inside project root',
68
+ { path: f, root },
69
+ );
60
70
  }
61
- if (path.isAbsolute(f)) {
62
- throw new NubosPilotError('commit-path-absolute', 'commit path must be relative', { path: f });
71
+ return rel;
72
+ });
73
+ }
74
+
75
+ function _validateFiles(files) {
76
+ for (const f of files) {
77
+ if (typeof f !== 'string' || f.length === 0) {
78
+ throw new NubosPilotError('commit-invalid-path', 'commit path must be non-empty string', { path: f });
63
79
  }
64
80
  }
65
81
  }
@@ -87,13 +103,15 @@ function run(argv, ctx) {
87
103
  stdout.write(JSON.stringify({ committed: false, reason: 'commit_artifacts=false', files }) + '\n');
88
104
  return 0;
89
105
  }
90
- const committable = assertCommittablePaths(files);
106
+ const root = findProjectRoot(cwd);
107
+ const normalized = _normalizeFiles(files, cwd, root);
108
+ const committable = assertCommittablePaths(normalized, { cwd: root });
91
109
  if (committable.length === 0) {
92
110
  throw new NubosPilotError('commit-no-paths', 'commit invoked with no committable paths', { files });
93
111
  }
94
- execFileSync('git', ['add', '--', ...committable], { stdio: 'pipe' });
95
- execFileSync('git', ['commit', '-m', msg, '--', ...committable], { stdio: 'pipe' });
96
- const sha = execFileSync('git', ['rev-parse', '--short', 'HEAD'], { encoding: 'utf-8' }).trim();
112
+ execFileSync('git', ['add', '--', ...committable], { cwd: root, stdio: 'pipe' });
113
+ execFileSync('git', ['commit', '-m', msg, '--', ...committable], { cwd: root, stdio: 'pipe' });
114
+ const sha = execFileSync('git', ['rev-parse', '--short', 'HEAD'], { cwd: root, encoding: 'utf-8' }).trim();
97
115
  stdout.write(JSON.stringify({ committed: true, sha, files: committable }) + '\n');
98
116
  return 0;
99
117
  } catch (err) {
@@ -57,12 +57,39 @@ test('COMMIT-1: happy path commits a single file and prints sha JSON', () => {
57
57
  assert.match(stdout.toString(), /"committed":\s*true/);
58
58
  });
59
59
 
60
- test('COMMIT-2: path with ".." segment rejected with commit-path-traversal', () => {
60
+ test('COMMIT-2: path resolving outside project root rejected with commit-path-outside-project', () => {
61
+ const sb = makeSandbox();
62
+ initGit(sb);
63
+ const stdout = makeSink();
64
+ const stderr = makeSink();
65
+ const code = commitCli.run(['feat: x', '--files', '../outside.txt'], { cwd: sb, stdout, stderr });
66
+ assert.equal(code, 1);
67
+ assert.match(stderr.toString(), /"code":\s*"commit-path-outside-project"/);
68
+ });
69
+
70
+ test('COMMIT-2b: absolute path inside project root is accepted and normalized', () => {
71
+ const sb = makeSandbox();
72
+ initGit(sb);
73
+ const absFile = path.join(sb, 'note.md');
74
+ fs.writeFileSync(absFile, 'hi\n');
75
+ const stdout = makeSink();
76
+ const stderr = makeSink();
77
+ const code = commitCli.run(['docs: note', '--files', absFile], { cwd: sb, stdout, stderr });
78
+ assert.equal(code, 0, 'stderr=' + stderr.toString());
79
+ assert.match(stdout.toString(), /"committed":\s*true/);
80
+ });
81
+
82
+ test('COMMIT-2c: absolute path outside project root rejected', () => {
83
+ const sb = makeSandbox();
84
+ initGit(sb);
85
+ const outside = path.join(os.tmpdir(), 'np-commit-outside-' + Date.now() + '.txt');
86
+ fs.writeFileSync(outside, 'x');
61
87
  const stdout = makeSink();
62
88
  const stderr = makeSink();
63
- const code = commitCli.run(['feat: x', '--files', '../outside.txt'], { stdout, stderr });
89
+ const code = commitCli.run(['docs: x', '--files', outside], { cwd: sb, stdout, stderr });
90
+ try { fs.unlinkSync(outside); } catch {}
64
91
  assert.equal(code, 1);
65
- assert.match(stderr.toString(), /"code":\s*"commit-path-traversal"/);
92
+ assert.match(stderr.toString(), /"code":\s*"commit-path-outside-project"/);
66
93
  });
67
94
 
68
95
  test('COMMIT-3: empty message prints usage and exits 1', () => {
@@ -0,0 +1,61 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { NubosPilotError } = require('../../lib/core.cjs');
6
+
7
+ const PACKAGE_TEMPLATES_DIR = path.resolve(__dirname, '..', '..', 'templates');
8
+
9
+ function _emitError(err, stderr) {
10
+ const code = err && err.name === 'NubosPilotError' ? err.code : 'template-path-internal-error';
11
+ const message = (err && err.message) || String(err);
12
+ const details = (err && err.details) || null;
13
+ stderr.write(JSON.stringify({ code, message, details }) + '\n');
14
+ }
15
+
16
+ function resolveTemplatePath(name) {
17
+ if (!name || typeof name !== 'string') {
18
+ throw new NubosPilotError('template-invalid-name', 'template name must be a non-empty string', { name });
19
+ }
20
+ const segments = name.split(/[/\\]/);
21
+ for (const seg of segments) {
22
+ if (seg === '' || seg === '..' || seg === '.') {
23
+ throw new NubosPilotError('template-invalid-name', 'template name segment invalid: ' + seg, { name, segment: seg });
24
+ }
25
+ }
26
+ const withExt = /\.[a-z0-9]+$/i.test(name) ? name : name + '.md';
27
+ const full = path.resolve(PACKAGE_TEMPLATES_DIR, withExt);
28
+ const guard = PACKAGE_TEMPLATES_DIR + path.sep;
29
+ if (!full.startsWith(guard)) {
30
+ throw new NubosPilotError('template-path-traversal', 'template name escapes templates directory', { name, resolved: full });
31
+ }
32
+ if (!fs.existsSync(full)) {
33
+ throw new NubosPilotError('template-not-found', 'template not found: ' + name, { name, path: full });
34
+ }
35
+ return full;
36
+ }
37
+
38
+ function run(argv, ctx) {
39
+ const context = ctx || {};
40
+ const stdout = context.stdout || process.stdout;
41
+ const stderr = context.stderr || process.stderr;
42
+ const args = Array.isArray(argv) ? argv.slice() : [];
43
+ if (args.length === 0 || args[0] === '--help') {
44
+ stderr.write('Usage: np-tools.cjs template-path <name>\n');
45
+ return 1;
46
+ }
47
+ try {
48
+ const out = resolveTemplatePath(args[0]);
49
+ stdout.write(out);
50
+ return 0;
51
+ } catch (err) {
52
+ _emitError(err, stderr);
53
+ return 1;
54
+ }
55
+ }
56
+
57
+ module.exports = { run, resolveTemplatePath, PACKAGE_TEMPLATES_DIR };
58
+
59
+ if (require.main === module) {
60
+ process.exit(run(process.argv.slice(2)));
61
+ }
@@ -0,0 +1,86 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { test } = require('node:test');
6
+ const assert = require('node:assert/strict');
7
+ const { Writable } = require('node:stream');
8
+
9
+ const cli = require('./template-path.cjs');
10
+ const { resolveTemplatePath, PACKAGE_TEMPLATES_DIR } = cli;
11
+
12
+ function makeSink() {
13
+ const chunks = [];
14
+ const w = new Writable({ write(chunk, _enc, cb) { chunks.push(chunk); cb(); } });
15
+ w.toString = () => Buffer.concat(chunks.map((c) => Buffer.isBuffer(c) ? c : Buffer.from(String(c)))).toString('utf-8');
16
+ return w;
17
+ }
18
+
19
+ test('TPL-1: resolves VALIDATION to absolute path in package templates dir', () => {
20
+ const out = resolveTemplatePath('VALIDATION');
21
+ assert.ok(path.isAbsolute(out));
22
+ assert.ok(out.startsWith(PACKAGE_TEMPLATES_DIR + path.sep));
23
+ assert.ok(fs.existsSync(out));
24
+ });
25
+
26
+ test('TPL-2: accepts nested name like milestone/CONTEXT', () => {
27
+ const out = resolveTemplatePath('milestone/CONTEXT');
28
+ assert.ok(fs.existsSync(out));
29
+ assert.match(out, /templates[/\\]milestone[/\\]CONTEXT\.md$/);
30
+ });
31
+
32
+ test('TPL-3: appends .md when extension absent', () => {
33
+ const out = resolveTemplatePath('VALIDATION');
34
+ assert.ok(out.endsWith('.md'));
35
+ });
36
+
37
+ test('TPL-4: keeps explicit extension', () => {
38
+ const out = resolveTemplatePath('VALIDATION.md');
39
+ assert.ok(out.endsWith('VALIDATION.md'));
40
+ assert.doesNotMatch(out, /\.md\.md$/);
41
+ });
42
+
43
+ test('TPL-5: rejects traversal ..', () => {
44
+ assert.throws(
45
+ () => resolveTemplatePath('../etc/passwd'),
46
+ (err) => err && err.code === 'template-invalid-name',
47
+ );
48
+ });
49
+
50
+ test('TPL-6: rejects empty segments', () => {
51
+ assert.throws(
52
+ () => resolveTemplatePath('milestone//CONTEXT'),
53
+ (err) => err && err.code === 'template-invalid-name',
54
+ );
55
+ });
56
+
57
+ test('TPL-7: rejects non-existent template with template-not-found', () => {
58
+ assert.throws(
59
+ () => resolveTemplatePath('NONEXISTENT'),
60
+ (err) => err && err.code === 'template-not-found',
61
+ );
62
+ });
63
+
64
+ test('TPL-8: CLI prints path and exits 0 for valid template', () => {
65
+ const stdout = makeSink();
66
+ const stderr = makeSink();
67
+ const code = cli.run(['VALIDATION'], { stdout, stderr });
68
+ assert.equal(code, 0, 'stderr=' + stderr.toString());
69
+ assert.ok(fs.existsSync(stdout.toString()));
70
+ });
71
+
72
+ test('TPL-9: CLI emits error JSON and exits 1 for missing template', () => {
73
+ const stdout = makeSink();
74
+ const stderr = makeSink();
75
+ const code = cli.run(['NOPE'], { stdout, stderr });
76
+ assert.equal(code, 1);
77
+ assert.match(stderr.toString(), /"code":\s*"template-not-found"/);
78
+ });
79
+
80
+ test('TPL-10: CLI with no args prints usage to stderr and exits 1', () => {
81
+ const stdout = makeSink();
82
+ const stderr = makeSink();
83
+ const code = cli.run([], { stdout, stderr });
84
+ assert.equal(code, 1);
85
+ assert.match(stderr.toString(), /Usage:/);
86
+ });
@@ -0,0 +1,40 @@
1
+ 'use strict';
2
+
3
+ const DEFAULT_WORKFLOW = Object.freeze({
4
+ commit_docs: true,
5
+ commit_artifacts: true,
6
+ });
7
+
8
+ const DEFAULT_AGENTS = Object.freeze({
9
+ parallelization: true,
10
+ research: true,
11
+ plan_checker: true,
12
+ verifier: true,
13
+ });
14
+
15
+ const DEFAULT_MODEL_PROFILE = 'frontier';
16
+ const DEFAULT_SCOPE = 'local';
17
+ const DEFAULT_RESPONSE_LANGUAGE = 'en';
18
+
19
+ function buildInstallConfig(answers) {
20
+ const a = answers || {};
21
+ return {
22
+ runtime: a.runtime || null,
23
+ runtimes: Array.isArray(a.runtimes) ? a.runtimes.slice() : (a.runtime ? [a.runtime] : []),
24
+ scope: a.scope || DEFAULT_SCOPE,
25
+ mcp: !!a.mcp,
26
+ model_profile: a.model_profile || DEFAULT_MODEL_PROFILE,
27
+ response_language: a.response_language || DEFAULT_RESPONSE_LANGUAGE,
28
+ workflow: { ...DEFAULT_WORKFLOW },
29
+ agents: { ...DEFAULT_AGENTS },
30
+ };
31
+ }
32
+
33
+ module.exports = {
34
+ DEFAULT_WORKFLOW,
35
+ DEFAULT_AGENTS,
36
+ DEFAULT_MODEL_PROFILE,
37
+ DEFAULT_SCOPE,
38
+ DEFAULT_RESPONSE_LANGUAGE,
39
+ buildInstallConfig,
40
+ };
package/lib/git.cjs CHANGED
@@ -8,12 +8,14 @@ function _isFatalCheckIgnore(err) {
8
8
  return err && err.status !== 1;
9
9
  }
10
10
 
11
- function isPathIgnored(p) {
11
+ function isPathIgnored(p, opts) {
12
+ const spawnOpts = { stdio: 'pipe' };
13
+ if (opts && opts.cwd) spawnOpts.cwd = opts.cwd;
12
14
  try {
13
- execFileSync('git', ['check-ignore', '--quiet', '--', p], { stdio: 'pipe' });
14
- return true;
15
+ execFileSync('git', ['check-ignore', '--quiet', '--', p], spawnOpts);
16
+ return true;
15
17
  } catch (err) {
16
- if (err && err.status === 1) return false;
18
+ if (err && err.status === 1) return false;
17
19
  if (err && err.status === 128) {
18
20
 
19
21
  throw err;
@@ -22,7 +24,7 @@ function isPathIgnored(p) {
22
24
  }
23
25
  }
24
26
 
25
- function assertCommittablePaths(paths) {
27
+ function assertCommittablePaths(paths, opts) {
26
28
  if (!Array.isArray(paths)) {
27
29
  throw new NubosPilotError(
28
30
  'commit-paths-invalid',
@@ -30,11 +32,13 @@ function assertCommittablePaths(paths) {
30
32
  { got: typeof paths },
31
33
  );
32
34
  }
35
+ const spawnOpts = { stdio: 'pipe' };
36
+ if (opts && opts.cwd) spawnOpts.cwd = opts.cwd;
33
37
  const ignored = [];
34
38
  for (const p of paths) {
35
39
  try {
36
- execFileSync('git', ['check-ignore', '--quiet', '--', p], { stdio: 'pipe' });
37
- ignored.push(p);
40
+ execFileSync('git', ['check-ignore', '--quiet', '--', p], spawnOpts);
41
+ ignored.push(p);
38
42
  } catch (err) {
39
43
  if (_isFatalCheckIgnore(err)) {
40
44
  if (err.status === 128) throw err;
package/np-tools.cjs CHANGED
@@ -48,6 +48,7 @@ const topLevelCommands = {
48
48
  'lang-directive': require('./bin/np-tools/lang-directive.cjs'),
49
49
  'text-mode': require('./bin/np-tools/text-mode.cjs'),
50
50
  'detect-runtime': require('./bin/np-tools/detect-runtime.cjs'),
51
+ 'template-path': require('./bin/np-tools/template-path.cjs'),
51
52
  };
52
53
 
53
54
  const THRESHOLD = 16 * 1024;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nubos-pilot",
3
- "version": "0.5.8",
3
+ "version": "0.5.9",
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": {
@@ -356,13 +356,14 @@ CONTEXT_PATH=$(echo "$INIT" | node -e 'let d="";process.stdin.on("data",c=>d+=c)
356
356
  mkdir -p "$MILESTONE_DIR"
357
357
  mkdir -p "$MILESTONE_DIR/slices"
358
358
 
359
+ TPL_PATH=$(node .nubos-pilot/bin/np-tools.cjs template-path milestone/CONTEXT)
359
360
  node -e '
360
361
  const { render } = require("./lib/template.cjs");
361
362
  const fs = require("node:fs");
362
- const tpl = fs.readFileSync("templates/milestone/CONTEXT.md", "utf-8");
363
- const vars = JSON.parse(process.argv[1]);
363
+ const tpl = fs.readFileSync(process.argv[1], "utf-8");
364
+ const vars = JSON.parse(process.argv[2]);
364
365
  process.stdout.write(render(tpl, vars));
365
- ' "$VARS_JSON" > "$CONTEXT_PATH"
366
+ ' "$TPL_PATH" "$VARS_JSON" > "$CONTEXT_PATH"
366
367
  ```
367
368
 
368
369
  `$VARS_JSON` is the JSON-serialised accumulator from Steps 2–5 (keys map to
@@ -42,7 +42,7 @@ the main chat. Auto-enabled in Claude Code (CLAUDECODE=1); opt-in via
42
42
  MILESTONE_ID=$(echo "$INIT" | jq -r '.milestone_id')
43
43
  MILESTONE_DIR=$(echo "$INIT" | jq -r '.milestone_dir')
44
44
  VALIDATION_PATH="${MILESTONE_DIR}/${MILESTONE_ID}-VALIDATION.md"
45
- TEMPLATE_PATH="templates/VALIDATION.md"
45
+ TEMPLATE_PATH=$(node .nubos-pilot/bin/np-tools.cjs template-path VALIDATION)
46
46
  REQS_PATH=".nubos-pilot/REQUIREMENTS.md"
47
47
  PLAN_ID="${MILESTONE_ID}-validate"
48
48
  TASK_ID="${MILESTONE_ID}-validate"
@@ -93,9 +93,9 @@ fi
93
93
  ### Gate 3 — Template present
94
94
 
95
95
  ```bash
96
- if [[ ! -f "$TEMPLATE_PATH" ]]; then
97
- echo "Error: $TEMPLATE_PATH missing." >&2
98
- echo "Re-run 'npx nubos-pilot install' or restore templates/VALIDATION.md from source." >&2
96
+ if [[ -z "$TEMPLATE_PATH" || ! -f "$TEMPLATE_PATH" ]]; then
97
+ echo "Error: VALIDATION template not resolvable via np-tools.cjs template-path." >&2
98
+ echo "Re-run 'npx nubos-pilot install' or check the package's templates/ dir." >&2
99
99
  exit 1
100
100
  fi
101
101
  ```