gitnexus 1.6.10-rc.30 → 1.6.10-rc.32

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.
@@ -35,10 +35,14 @@ export const TS_JS_TAINT_MODEL = {
35
35
  },
36
36
  ],
37
37
  sinks: [
38
- // Command execution — the command string is argument 0.
38
+ // Command execution — shell strings are arg 0; argv-form APIs also treat
39
+ // the argv array at arg 1 as command/option injection surface.
39
40
  { name: 'exec', kind: 'command-injection', args: [0], module: 'child_process' },
40
41
  { name: 'execSync', kind: 'command-injection', args: [0], module: 'child_process' },
41
- { name: 'spawn', kind: 'command-injection', args: [0], module: 'child_process' },
42
+ { name: 'spawn', kind: 'command-injection', args: [0, 1], module: 'child_process' },
43
+ { name: 'spawnSync', kind: 'command-injection', args: [0, 1], module: 'child_process' },
44
+ { name: 'execFile', kind: 'command-injection', args: [0, 1], module: 'child_process' },
45
+ { name: 'execFileSync', kind: 'command-injection', args: [0, 1], module: 'child_process' },
42
46
  // Code evaluation. `eval` takes code at 0; `new Function(...)` treats
43
47
  // EVERY argument as source text (params + body), so `args` is omitted
44
48
  // (= all positions) rather than pinned to 0.
@@ -53,9 +57,37 @@ export const TS_JS_TAINT_MODEL = {
53
57
  // (mysql2/pg/knex handles go by many names; receiver-conventional).
54
58
  { name: 'query', kind: 'sql-injection', args: [0], anyReceiver: true },
55
59
  { name: 'execute', kind: 'sql-injection', args: [0], anyReceiver: true },
60
+ // Modern DB libraries expose shorter method names with high collision
61
+ // rates (`map.get`, `task.run`, ...), so keep these receiver-conventional.
62
+ {
63
+ name: 'run',
64
+ kind: 'sql-injection',
65
+ args: [0],
66
+ receivers: ['db', 'database', 'conn', 'client', 'pool', 'stmt', 'statement', 'prepared'],
67
+ },
68
+ {
69
+ name: 'all',
70
+ kind: 'sql-injection',
71
+ args: [0],
72
+ receivers: ['db', 'database', 'conn', 'client', 'pool', 'stmt', 'statement', 'prepared'],
73
+ },
74
+ {
75
+ name: 'get',
76
+ kind: 'sql-injection',
77
+ args: [0],
78
+ receivers: ['db', 'database', 'conn', 'client', 'pool', 'stmt', 'statement', 'prepared'],
79
+ },
80
+ {
81
+ name: 'values',
82
+ kind: 'sql-injection',
83
+ args: [0],
84
+ receivers: ['db', 'database', 'conn', 'client', 'pool'],
85
+ },
86
+ { name: 'raw', kind: 'sql-injection', args: [0], receivers: ['db', 'knex', 'sequelize'] },
56
87
  // Reflected XSS — Express response writes, conventional receiver `res`.
57
88
  { name: 'send', kind: 'xss', args: [0], receivers: ['res'] },
58
89
  { name: 'write', kind: 'xss', args: [0], receivers: ['res'] },
90
+ { name: 'render', kind: 'xss', args: [0, 1], receivers: ['res'] },
59
91
  ],
60
92
  sanitizers: [
61
93
  // URL-encoding: neutralizes markup injection AND path separators
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.10-rc.30",
3
+ "version": "1.6.10-rc.32",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",
@@ -52,7 +52,8 @@
52
52
  "postinstall": "node scripts/build-tree-sitter-grammars.cjs",
53
53
  "assert-publish-coverage": "node scripts/assert-publish-grammar-coverage.cjs",
54
54
  "prepare": "node scripts/build.js",
55
- "prepack": "node scripts/assert-publish-grammar-coverage.cjs && node scripts/build.js"
55
+ "prepack": "node scripts/assert-publish-grammar-coverage.cjs && node scripts/build.js",
56
+ "version": "node scripts/sync-plugin-manifests.mjs"
56
57
  },
57
58
  "dependencies": {
58
59
  "@ladybugdb/core": "^0.18.0",
@@ -48,10 +48,11 @@ try {
48
48
 
49
49
  // Per-shard watchdog, default 15 min. Sharding splits the file list by COUNT, not
50
50
  // runtime, so the heaviest spawn suites can cluster on one shard — what this
51
- // bounds is the *busiest* shard, not an even 1/n of wall-clock. With 3 shards
52
- // even that shard clears the watchdog, where the whole unsharded Windows run
53
- // used to trip it. Allow CI/manual runs to add headroom without editing the
54
- // script again.
51
+ // bounds is the *busiest* shard, not an even 1/n of wall-clock. The busiest
52
+ // Windows shard has grown to the default (14m57s on the v1.6.10-rc.19 green
53
+ // run, one observed timeout since — #2449), so CI raises the budget to 20
54
+ // minutes via GITNEXUS_CROSS_PLATFORM_TIMEOUT_MINUTES; the default stays 15
55
+ // for local runs.
55
56
  const DEFAULT_TIMEOUT_MIN = 15;
56
57
  const timeoutMinutes = Number.parseInt(
57
58
  process.env.GITNEXUS_CROSS_PLATFORM_TIMEOUT_MINUTES ?? String(DEFAULT_TIMEOUT_MIN),
@@ -67,6 +68,7 @@ console.log(
67
68
  `${shardArg ? ` (${shardArg.replace('--shard=', 'shard ')})` : ''}...\n`,
68
69
  );
69
70
 
71
+ const startedAt = Date.now();
70
72
  try {
71
73
  execFileSync('npx', ['vitest', 'run', ...ALL_CROSS_PLATFORM, ...(shardArg ? [shardArg] : [])], {
72
74
  cwd: ROOT,
@@ -76,9 +78,22 @@ try {
76
78
  });
77
79
  } catch (err) {
78
80
  // execFileSync sets `killed`/`signal` when the watchdog above kills vitest.
79
- const e = err as { killed?: boolean; signal?: NodeJS.Signals | null };
81
+ const e = err as {
82
+ killed?: boolean;
83
+ signal?: NodeJS.Signals | null;
84
+ status?: number | null;
85
+ code?: string;
86
+ };
80
87
  if (e.killed || e.signal) {
81
88
  console.error(`vitest timed out after ${Math.round(timeoutMs / 60_000)} minutes`);
82
89
  }
90
+ // #2449: Windows shards have died with a bare `status: null`, empty stderr
91
+ // and nothing to triage from. Always leave the child's exit facts behind.
92
+ const elapsedSec = Math.round((Date.now() - startedAt) / 1000);
93
+ console.error(
94
+ `vitest exited abnormally: status=${e.status ?? 'null'} signal=${e.signal ?? 'none'} ` +
95
+ `killed=${e.killed === true} spawnCode=${e.code ?? 'none'} elapsed=${elapsedSec}s ` +
96
+ `budget=${Math.round(timeoutMs / 60_000)}min`,
97
+ );
83
98
  process.exit(1);
84
99
  }
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Fail-closed version sync for the plugin manifest surfaces (#2445).
4
+ *
5
+ * `publish.yml` bumps only `gitnexus/package.json` when it cuts an RC, so
6
+ * every RC tag through v1.6.10-rc.28 shipped manifests frozen at the last
7
+ * stable version and failed its own unit suite (the cli-commands version
8
+ * contract). This script pins all four manifest surfaces to the package
9
+ * version:
10
+ *
11
+ * - gitnexus-claude-plugin/.claude-plugin/plugin.json (top-level version)
12
+ * - .claude-plugin/marketplace.json (plugins[gitnexus])
13
+ * - gitnexus-claude-plugin/.codex-plugin/plugin.json (top-level version)
14
+ * - .agents/plugins/marketplace.json (plugins[gitnexus])
15
+ *
16
+ * Modes:
17
+ * node scripts/sync-plugin-manifests.mjs rewrite stale surfaces
18
+ * node scripts/sync-plugin-manifests.mjs --check verify only, exit 1 on drift
19
+ *
20
+ * Fail-closed: a missing file, unparseable JSON, an absent version field, or
21
+ * anything other than exactly one `gitnexus` marketplace entry aborts with a
22
+ * non-zero exit rather than letting a release ship a partial sync.
23
+ */
24
+ import { readFileSync, writeFileSync } from 'node:fs';
25
+ import path from 'node:path';
26
+ import { fileURLToPath } from 'node:url';
27
+
28
+ const MANIFEST_SURFACES = [
29
+ { file: 'gitnexus-claude-plugin/.claude-plugin/plugin.json', kind: 'plugin' },
30
+ { file: '.claude-plugin/marketplace.json', kind: 'marketplace' },
31
+ { file: 'gitnexus-claude-plugin/.codex-plugin/plugin.json', kind: 'plugin' },
32
+ { file: '.agents/plugins/marketplace.json', kind: 'marketplace' },
33
+ ];
34
+
35
+ const PLUGIN_NAME = 'gitnexus';
36
+
37
+ function readJson(filePath) {
38
+ let raw;
39
+ try {
40
+ raw = readFileSync(filePath, 'utf8');
41
+ } catch (err) {
42
+ throw new Error(`Cannot read manifest surface ${filePath}: ${err.message}`);
43
+ }
44
+ try {
45
+ return { raw, parsed: JSON.parse(raw) };
46
+ } catch (err) {
47
+ throw new Error(`Manifest surface ${filePath} is not valid JSON: ${err.message}`);
48
+ }
49
+ }
50
+
51
+ function versionTarget(manifest, kind, filePath) {
52
+ if (kind === 'plugin') {
53
+ if (typeof manifest.version !== 'string' || manifest.version.length === 0) {
54
+ throw new Error(`Manifest surface ${filePath} has no version field to sync`);
55
+ }
56
+ return manifest;
57
+ }
58
+ const entries = (Array.isArray(manifest.plugins) ? manifest.plugins : []).filter(
59
+ (plugin) => plugin?.name === PLUGIN_NAME,
60
+ );
61
+ if (entries.length !== 1) {
62
+ throw new Error(
63
+ `Manifest surface ${filePath} must contain exactly one "${PLUGIN_NAME}" plugin entry, found ${entries.length}`,
64
+ );
65
+ }
66
+ if (typeof entries[0].version !== 'string' || entries[0].version.length === 0) {
67
+ throw new Error(`Manifest surface ${filePath} has no version field to sync`);
68
+ }
69
+ return entries[0];
70
+ }
71
+
72
+ /**
73
+ * Sync (or with `check: true`, only inspect) every manifest surface under
74
+ * `rootDir`. Returns `{ version, synced, stale }` where `stale` lists the
75
+ * surfaces that did not match the package version when the run started.
76
+ */
77
+ export function syncPluginManifests(rootDir, { check = false } = {}) {
78
+ const pkgPath = path.join(rootDir, 'gitnexus', 'package.json');
79
+ const version = readJson(pkgPath).parsed.version;
80
+ if (typeof version !== 'string' || version.length === 0) {
81
+ throw new Error(`No version found in ${pkgPath}`);
82
+ }
83
+
84
+ const synced = [];
85
+ const stale = [];
86
+ for (const { file, kind } of MANIFEST_SURFACES) {
87
+ const manifestPath = path.join(rootDir, file);
88
+ const { raw, parsed } = readJson(manifestPath);
89
+ const target = versionTarget(parsed, kind, manifestPath);
90
+ if (target.version === version) continue;
91
+
92
+ stale.push({ file, from: target.version });
93
+ if (check) continue;
94
+
95
+ // Textual surgery instead of re-serializing: JSON.stringify would refold
96
+ // arrays and fight prettier, turning a one-line version bump into
97
+ // formatting churn inside the release commit. The needle is built from
98
+ // the parsed current version, and anything other than exactly one
99
+ // occurrence aborts rather than guessing.
100
+ const needle = `"version": "${target.version}"`;
101
+ const occurrences = raw.split(needle).length - 1;
102
+ if (occurrences !== 1) {
103
+ throw new Error(
104
+ `Manifest surface ${manifestPath} has ${occurrences} occurrences of ${needle}; ` +
105
+ 'expected exactly one, refusing to sync',
106
+ );
107
+ }
108
+ writeFileSync(manifestPath, raw.replace(needle, `"version": "${version}"`));
109
+ synced.push(file);
110
+ }
111
+
112
+ return { version, synced, stale };
113
+ }
114
+
115
+ const invokedDirectly =
116
+ process.argv[1] !== undefined && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
117
+
118
+ if (invokedDirectly) {
119
+ const check = process.argv.includes('--check');
120
+ const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
121
+ const result = syncPluginManifests(rootDir, { check });
122
+
123
+ if (check && result.stale.length > 0) {
124
+ for (const { file, from } of result.stale) {
125
+ console.error(
126
+ `::error::${file} is at ${from} but gitnexus/package.json is at ${result.version}. ` +
127
+ 'Run `node gitnexus/scripts/sync-plugin-manifests.mjs` and commit the result.',
128
+ );
129
+ }
130
+ process.exit(1);
131
+ }
132
+
133
+ for (const file of result.synced) {
134
+ console.log(`synced ${file} -> ${result.version}`);
135
+ }
136
+ console.log(
137
+ result.stale.length === 0 && result.synced.length === 0
138
+ ? `all plugin manifests already at ${result.version}`
139
+ : `plugin manifests now at ${result.version}`,
140
+ );
141
+ }