@opengsd/gsd-core 1.6.0 → 1.7.0-rc.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/.claude-plugin/plugin.json +1 -1
- package/agents/gsd-verifier.md +1 -0
- package/bin/gsd-mcp-server.js +31 -0
- package/bin/install.js +293 -1145
- package/commands/gsd/review.md +6 -0
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/gsd-tools.cjs +116 -1
- package/gsd-core/bin/lib/adapter-declarative.cjs +35 -0
- package/gsd-core/bin/lib/adapter-imperative.cjs +52 -0
- package/gsd-core/bin/lib/assumption-delta.cjs +231 -0
- package/gsd-core/bin/lib/capability-lifecycle.cjs +7 -7
- package/gsd-core/bin/lib/capability-loader.cjs +18 -0
- package/gsd-core/bin/lib/capability-lock.cjs +2 -2
- package/gsd-core/bin/lib/capability-registry.cjs +889 -82
- package/gsd-core/bin/lib/capability-source.cjs +4 -4
- package/gsd-core/bin/lib/capability-validator.cjs +198 -0
- package/gsd-core/bin/lib/cli-skew-check.cjs +44 -0
- package/gsd-core/bin/lib/command-aliases.cjs +8 -0
- package/gsd-core/bin/lib/config.cjs +27 -0
- package/gsd-core/bin/lib/embedding-adapter.cjs +27 -0
- package/gsd-core/bin/lib/external-descriptor-trust.cjs +70 -0
- package/gsd-core/bin/lib/hook-bus.cjs +81 -0
- package/gsd-core/bin/lib/host-integration.cjs +408 -0
- package/gsd-core/bin/lib/init.cjs +1 -1
- package/gsd-core/bin/lib/install-engine.cjs +755 -0
- package/gsd-core/bin/lib/install-profiles.cjs +35 -4
- package/gsd-core/bin/lib/installer-migrations.cjs +1 -1
- package/gsd-core/bin/lib/mcp-server.cjs +194 -0
- package/gsd-core/bin/lib/milestone.cjs +27 -30
- package/gsd-core/bin/lib/model-adapter.cjs +50 -0
- package/gsd-core/bin/lib/phase.cjs +41 -72
- package/gsd-core/bin/lib/planning-workspace.cjs +1 -1
- package/gsd-core/bin/lib/probe-core.cjs +91 -1
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +129 -13
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +3 -2
- package/gsd-core/bin/lib/roadmap.cjs +17 -3
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +65 -9
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +54 -4
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +5 -2
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +1 -1
- package/gsd-core/bin/lib/runtime-name-policy.cjs +160 -30
- package/gsd-core/bin/lib/shell-command-projection.cjs +37 -1
- package/gsd-core/bin/lib/stale-bake-guard.cjs +254 -0
- package/gsd-core/bin/lib/state-command-router.cjs +4 -0
- package/gsd-core/bin/lib/state-io.cjs +55 -0
- package/gsd-core/bin/lib/state-transition.cjs +1588 -0
- package/gsd-core/bin/lib/state.cjs +306 -681
- package/gsd-core/bin/lib/surface.cjs +4 -1
- package/gsd-core/bin/lib/workstream.cjs +4 -4
- package/gsd-core/bin/shared/config-schema.manifest.json +9 -0
- package/gsd-core/references/honest-verifier.md +105 -0
- package/gsd-core/references/reviewer-instances.md +99 -0
- package/gsd-core/workflows/autonomous.md +9 -9
- package/gsd-core/workflows/manager.md +15 -15
- package/gsd-core/workflows/plan-phase.md +1 -1
- package/gsd-core/workflows/review.md +26 -0
- package/gsd-core/workflows/thread.md +4 -4
- package/gsd-core/workflows/verify-phase.md +11 -4
- package/hooks/dist/gsd-graphify-update.sh +7 -1
- package/hooks/gsd-graphify-update.sh +7 -1
- package/package.json +4 -4
- package/scripts/ci-test-scope.cjs +38 -9
- package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -1
- package/scripts/lint-regression-test-names.allowlist.json +3 -0
- package/scripts/lint-test-file-count.allowlist.json +19 -5
- package/scripts/mutation-matrix.cjs +45 -3
- package/scripts/prompt-injection-scan.sh +8 -0
- package/scripts/lint-windows-test-portability.cjs +0 -178
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengsd/gsd-core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0-rc.1",
|
|
4
4
|
"description": "GSD Core is a meta-prompting, context engineering, and spec-driven development system for AI coding agents.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"gsd-core": "bin/install.js",
|
|
7
7
|
"gsd-tools": "gsd-core/bin/gsd-tools.cjs",
|
|
8
|
-
"gsd_run": "gsd-core/bin/gsd_run"
|
|
8
|
+
"gsd_run": "gsd-core/bin/gsd_run",
|
|
9
|
+
"gsd-mcp-server": "bin/gsd-mcp-server.js"
|
|
9
10
|
},
|
|
10
11
|
"files": [
|
|
11
12
|
"bin",
|
|
@@ -94,9 +95,8 @@
|
|
|
94
95
|
"pretest:coverage": "npm run build:lib && npm run lint:skill-deps",
|
|
95
96
|
"lint": "eslint . --cache --cache-location node_modules/.cache/eslint/",
|
|
96
97
|
"lint:fix": "eslint . --fix",
|
|
97
|
-
"lint:ci": "npm run lint && npm run lint:skill-deps && node scripts/lint-test-file-count.cjs && node scripts/lint-command-contract.cjs && node scripts/lint-pr-check-project-dir.cjs && npm run lint:legacy-name && node scripts/lint-regression-test-names.cjs && node scripts/lint-
|
|
98
|
+
"lint:ci": "npm run lint && npm run lint:skill-deps && node scripts/lint-test-file-count.cjs && node scripts/lint-command-contract.cjs && node scripts/lint-pr-check-project-dir.cjs && npm run lint:legacy-name && node scripts/lint-regression-test-names.cjs && node scripts/lint-allow-test-rule-refs.cjs && node scripts/lint-resolution-provenance.cjs",
|
|
98
99
|
"lint:allow-test-rule-refs": "node scripts/lint-allow-test-rule-refs.cjs",
|
|
99
|
-
"lint:windows-test-portability": "node scripts/lint-windows-test-portability.cjs",
|
|
100
100
|
"lint:regression-names": "node scripts/lint-regression-test-names.cjs",
|
|
101
101
|
"lint:descriptions": "node scripts/lint-descriptions.cjs",
|
|
102
102
|
"lint:skill-deps": "node scripts/lint-skill-deps.cjs",
|
|
@@ -118,6 +118,7 @@ const RULES = [
|
|
|
118
118
|
tests: [
|
|
119
119
|
'tests/semver-compare.test.cjs',
|
|
120
120
|
'tests/bug-10-semver-policy-consolidation.test.cjs',
|
|
121
|
+
'tests/golden-install-parity.test.cjs', // any src/installer change can alter emitted install artifacts → re-verify golden install parity (drift guard)
|
|
121
122
|
],
|
|
122
123
|
},
|
|
123
124
|
{
|
|
@@ -134,6 +135,7 @@ const RULES = [
|
|
|
134
135
|
'tests/install-path-detection.test.cjs',
|
|
135
136
|
'tests/release-tarball-smoke.install.test.cjs',
|
|
136
137
|
'tests/runtime-artifact-layout.test.cjs',
|
|
138
|
+
'tests/golden-install-parity.test.cjs', // any src/installer change can alter emitted install artifacts → re-verify golden install parity (drift guard)
|
|
137
139
|
],
|
|
138
140
|
},
|
|
139
141
|
{
|
|
@@ -212,17 +214,44 @@ const RULES = [
|
|
|
212
214
|
],
|
|
213
215
|
},
|
|
214
216
|
{
|
|
215
|
-
|
|
216
|
-
|
|
217
|
+
name: 'configuration',
|
|
218
|
+
match: path => ['config', 'configuration', 'model-catalog', 'model-profile'].some(k => path.includes(k)),
|
|
219
|
+
tests: [
|
|
220
|
+
'tests/config.test.cjs',
|
|
221
|
+
'tests/config-get-default.test.cjs',
|
|
222
|
+
'tests/configuration-migrate-config.test.cjs',
|
|
223
|
+
'tests/model-catalog-runtime-defaults.test.cjs',
|
|
224
|
+
'tests/model-profiles.test.cjs',
|
|
225
|
+
],
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
// ADR-1703 portability lint surface. Editing a rule, the shared vocab/guard
|
|
229
|
+
// helpers, or the eslint config that wires them must re-run the rule suites
|
|
230
|
+
// + the disable-ban. The disable-ban also scans bin/install.js and
|
|
231
|
+
// scripts/build-hooks.js (the Phase 6 glob-expansion surface), so changes
|
|
232
|
+
// to those files re-run it too.
|
|
233
|
+
name: 'portability lint rules (ADR-1703)',
|
|
234
|
+
match: path => path.startsWith('eslint-rules/') ||
|
|
235
|
+
path === 'eslint.config.mjs' ||
|
|
236
|
+
path === 'bin/install.js' ||
|
|
237
|
+
path === 'scripts/build-hooks.js',
|
|
217
238
|
tests: [
|
|
218
|
-
'tests/
|
|
219
|
-
'tests/
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
'tests/
|
|
239
|
+
'tests/portability-rule-disable-ban.test.cjs',
|
|
240
|
+
'tests/portability-vocab-drift.test.cjs',
|
|
241
|
+
// All nine RuleTester suites (P1–P6) — editing any rule / the shared
|
|
242
|
+
// vocab+guard helpers / the eslint config re-runs the full rule family.
|
|
243
|
+
'tests/no-path-literal-in-assert.rule.test.cjs',
|
|
244
|
+
'tests/no-posix-mode-bit-assert.rule.test.cjs',
|
|
245
|
+
'tests/no-unguarded-nonportable-exec.rule.test.cjs',
|
|
246
|
+
'tests/no-crlf-fragile-split.rule.test.cjs',
|
|
247
|
+
'tests/no-hardcoded-tmp.rule.test.cjs',
|
|
248
|
+
'tests/no-bare-npm-exec.rule.test.cjs',
|
|
249
|
+
'tests/require-userprofile-with-home.rule.test.cjs',
|
|
250
|
+
'tests/normalize-path-in-content.rule.test.cjs',
|
|
251
|
+
'tests/require-fs-op-fallback.rule.test.cjs',
|
|
223
252
|
],
|
|
224
253
|
},
|
|
225
|
-
];
|
|
254
|
+
];
|
|
226
255
|
|
|
227
256
|
function usage() {
|
|
228
257
|
return [
|
|
@@ -329,7 +358,7 @@ function classify(files) {
|
|
|
329
358
|
// Determine if this file is product/pipeline code.
|
|
330
359
|
// docs/ and root-level .md files are intentionally excluded.
|
|
331
360
|
if (
|
|
332
|
-
['bin/', 'src/', 'gsd-core/', 'agents/', 'commands/', 'hooks/', 'tests/', 'scripts/'].some(p => file.startsWith(p)) ||
|
|
361
|
+
['bin/', 'src/', 'gsd-core/', 'agents/', 'commands/', 'hooks/', 'tests/', 'scripts/', 'eslint-rules/'].some(p => file.startsWith(p)) ||
|
|
333
362
|
file === 'package.json' || file === 'package-lock.json' ||
|
|
334
363
|
(file.startsWith('tsconfig') && file.endsWith('.json')) ||
|
|
335
364
|
file.startsWith('.github/rulesets/')
|
|
@@ -313,7 +313,6 @@
|
|
|
313
313
|
"tests/verify-test-quality.test.cjs :: source-text-is-the-product",
|
|
314
314
|
"tests/verify-work-auto-transition.test.cjs :: source-text-is-the-product",
|
|
315
315
|
"tests/windows-robustness.test.cjs :: source-text-is-the-product",
|
|
316
|
-
"tests/windows-test-parity-guard.test.cjs :: structural-regression-guard",
|
|
317
316
|
"tests/workflow-compat.test.cjs :: source-text-is-the-product",
|
|
318
317
|
"tests/workflow-guard-registration.test.cjs :: structural-regression-guard",
|
|
319
318
|
"tests/workflow-maintainer-skip.test.cjs :: source-text-is-the-product",
|
|
@@ -5,10 +5,13 @@
|
|
|
5
5
|
"bug-1367-claude-local-flat-command-layout.test.cjs",
|
|
6
6
|
"bug-14-progress-auto-flag-dropped.test.cjs",
|
|
7
7
|
"bug-167-query-meta-command.test.cjs",
|
|
8
|
+
"bug-1695-state-patch-clobbers-phase-name.test.cjs",
|
|
8
9
|
"bug-17-askuserquestion-option-cap.test.cjs",
|
|
9
10
|
"bug-170-workflow-fallback-install-hint.test.cjs",
|
|
10
11
|
"bug-1736-local-install-commands.test.cjs",
|
|
11
12
|
"bug-1754-js-hook-guard.test.cjs",
|
|
13
|
+
"bug-1760-state-prune-noop-template-field.test.cjs",
|
|
14
|
+
"bug-1761-state-sync-wrong-progress.test.cjs",
|
|
12
15
|
"bug-1817-sh-hook-guard.test.cjs",
|
|
13
16
|
"bug-1818-unknown-flags.test.cjs",
|
|
14
17
|
"bug-1826-phases-clear-confirm.test.cjs",
|
|
@@ -99,6 +99,9 @@
|
|
|
99
99
|
},
|
|
100
100
|
"state": {
|
|
101
101
|
"files": [
|
|
102
|
+
"bug-1695-state-patch-clobbers-phase-name.test.cjs",
|
|
103
|
+
"bug-1760-state-prune-noop-template-field.test.cjs",
|
|
104
|
+
"bug-1761-state-sync-wrong-progress.test.cjs",
|
|
102
105
|
"bug-21-state-md-template-frontmatter.test.cjs",
|
|
103
106
|
"bug-2630-state-frontmatter-milestone-switch.test.cjs",
|
|
104
107
|
"bug-3127-state-begin-phase-idempotent.test.cjs",
|
|
@@ -107,10 +110,12 @@
|
|
|
107
110
|
"bug-3454-state-dollar-backreference-growth.test.cjs",
|
|
108
111
|
"bug-397-state-preserve-executor-authored.test.cjs",
|
|
109
112
|
"bug-905-state-syncstatefrontmatter-preserve-scalars.test.cjs",
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
113
|
+
"bug-948-state-noop-write-guard.test.cjs",
|
|
114
|
+
"state-acquirestatelock-non-eexist.test.cjs",
|
|
115
|
+
"state-prune.test.cjs",
|
|
116
|
+
"state-rebuild-cli.test.cjs",
|
|
117
|
+
"state-rebuild.test.cjs",
|
|
118
|
+
"state.test.cjs"
|
|
114
119
|
],
|
|
115
120
|
"issue": "180"
|
|
116
121
|
},
|
|
@@ -142,9 +147,10 @@
|
|
|
142
147
|
"install-regressions.test.cjs",
|
|
143
148
|
"install-runtime-artifacts.test.cjs",
|
|
144
149
|
"install-update-marker.test.cjs",
|
|
150
|
+
"install-write-confinement.test.cjs",
|
|
145
151
|
"install.test.cjs"
|
|
146
152
|
],
|
|
147
|
-
"issue": "
|
|
153
|
+
"issue": "1679"
|
|
148
154
|
},
|
|
149
155
|
"validate": {
|
|
150
156
|
"files": [
|
|
@@ -184,6 +190,14 @@
|
|
|
184
190
|
"fix-1464-docs-manifest-validation.test.cjs"
|
|
185
191
|
],
|
|
186
192
|
"issue": "1496"
|
|
193
|
+
},
|
|
194
|
+
"host-integration": {
|
|
195
|
+
"files": [
|
|
196
|
+
"host-integration.test.cjs",
|
|
197
|
+
"host-integration-validator-parity.test.cjs",
|
|
198
|
+
"host-integration-descriptors.test.cjs"
|
|
199
|
+
],
|
|
200
|
+
"issue": "1684"
|
|
187
201
|
}
|
|
188
202
|
}
|
|
189
203
|
}
|
|
@@ -30,10 +30,52 @@
|
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
32
|
const { execFileSync } = require('child_process');
|
|
33
|
-
const
|
|
33
|
+
const fs = require('fs');
|
|
34
34
|
|
|
35
35
|
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
|
36
36
|
|
|
37
|
+
// ── Resilient stdin reader ────────────────────────────────────────────────────
|
|
38
|
+
// On macOS, libuv sets the stdin pipe fd to non-blocking mode. A synchronous
|
|
39
|
+
// readFileSync(process.stdin.fd) can therefore throw EAGAIN ("resource
|
|
40
|
+
// temporarily unavailable") when the writer hasn't yet filled the pipe — this
|
|
41
|
+
// is intermittent under heavy CI shard load and causes a spurious status 2
|
|
42
|
+
// exit. We work around it by calling fs.readSync in a loop and retrying on
|
|
43
|
+
// EAGAIN with a 1 ms synchronous pause (Atomics.wait on a fresh SharedArrayBuffer
|
|
44
|
+
// — no hot spin, no real-clock dependency, works under --experimental-vm-modules).
|
|
45
|
+
/**
|
|
46
|
+
* Read all of stdin synchronously, retrying on EAGAIN.
|
|
47
|
+
*
|
|
48
|
+
* @returns {string} UTF-8 decoded full stdin content.
|
|
49
|
+
*/
|
|
50
|
+
function readStdinSync() {
|
|
51
|
+
const BUF_SIZE = 64 * 1024; // 64 KB chunks
|
|
52
|
+
const buf = Buffer.allocUnsafe(BUF_SIZE);
|
|
53
|
+
const chunks = [];
|
|
54
|
+
|
|
55
|
+
for (;;) {
|
|
56
|
+
let bytesRead;
|
|
57
|
+
try {
|
|
58
|
+
bytesRead = fs.readSync(process.stdin.fd, buf, 0, BUF_SIZE, null);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
if (err.code === 'EAGAIN') {
|
|
61
|
+
// Non-blocking pipe not yet ready — yield for ~1 ms then retry.
|
|
62
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 1);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (err.code === 'EOF') {
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
70
|
+
if (bytesRead === 0) {
|
|
71
|
+
break; // Clean EOF
|
|
72
|
+
}
|
|
73
|
+
chunks.push(Buffer.from(buf.slice(0, bytesRead)));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
77
|
+
}
|
|
78
|
+
|
|
37
79
|
// ── Per-module mutation score ratchet ─────────────────────────────────────────
|
|
38
80
|
// ADR-456 / issue #1187: every covered module declares a minScore floor.
|
|
39
81
|
//
|
|
@@ -195,7 +237,7 @@ function resolveChangedFiles(args) {
|
|
|
195
237
|
// When --base is absent AND stdin is not a TTY (isTTY is falsy / undefined),
|
|
196
238
|
// read a newline-delimited file list from stdin.
|
|
197
239
|
if (!args.base && process.stdin.isTTY !== true) {
|
|
198
|
-
const raw =
|
|
240
|
+
const raw = readStdinSync();
|
|
199
241
|
return raw.split('\n').map(l => l.trim()).filter(Boolean);
|
|
200
242
|
}
|
|
201
243
|
|
|
@@ -318,6 +360,6 @@ function resolveMutationBreak(raw) {
|
|
|
318
360
|
|
|
319
361
|
// Export internals for programmatic use (tests/mutation-matrix-ratchet.test.cjs).
|
|
320
362
|
// The require.main guard prevents main() from running when this file is require()d.
|
|
321
|
-
module.exports = { COVERED, TARGET_MUTATION_SCORE, resolveMutationBreak };
|
|
363
|
+
module.exports = { COVERED, TARGET_MUTATION_SCORE, resolveMutationBreak, readStdinSync };
|
|
322
364
|
|
|
323
365
|
if (require.main === module) runMain(main);
|
|
@@ -94,6 +94,14 @@ ALLOWLIST=(
|
|
|
94
94
|
# real injection payloads to prove the validator rejects them. See
|
|
95
95
|
# DEFECT.PROMPT-INJECTION-SCAN-COLLISION in CONTEXT.md.
|
|
96
96
|
'tests/windsurf-conversion.test.cjs'
|
|
97
|
+
# RuleTester fixtures for the local/no-unguarded-nonportable-exec ESLint rule
|
|
98
|
+
# contain shell-exec command strings (exec("sh -c …"), execFileSync('bash',['-c',…]))
|
|
99
|
+
# as test DATA the rule must lint — not attack vectors. ADR-1703 Phase 3 (#1720).
|
|
100
|
+
'tests/no-unguarded-nonportable-exec.rule.test.cjs'
|
|
101
|
+
# RuleTester fixtures for the local/no-bare-npm-exec ESLint rule contain npm
|
|
102
|
+
# exec command strings (execFileSync('npm', ['install'])) as test DATA the rule
|
|
103
|
+
# must lint — not attack vectors. ADR-1703 Phase 4 (#1726).
|
|
104
|
+
'tests/no-bare-npm-exec.rule.test.cjs'
|
|
97
105
|
)
|
|
98
106
|
|
|
99
107
|
is_allowlisted() {
|
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* lint-windows-test-portability.cjs — flag tests that combine chmod exec-bit
|
|
5
|
-
* with bare sh/bash -c without a platform guard.
|
|
6
|
-
*
|
|
7
|
-
* ## Why
|
|
8
|
-
*
|
|
9
|
-
* Windows Git Bash (msys2) does not honour Node's chmod exec bit for
|
|
10
|
-
* PATH-executing extension-less scripts. A test that (a) makes a fixture
|
|
11
|
-
* executable via chmodSync and (b) runs it with `sh -c`/`bash -c` will pass
|
|
12
|
-
* on Mac/Linux but fail only in the CI `test (windows-latest, *)` /
|
|
13
|
-
* `full test (windows-latest, *)` lanes, producing a hard-to-diagnose
|
|
14
|
-
* false-negative gate. See CONTEXT.md → DEFECT.WINDOWS-TEST-PORTABILITY.
|
|
15
|
-
*
|
|
16
|
-
* ## What this enforces
|
|
17
|
-
*
|
|
18
|
-
* For every file in tests/**\/*.test.cjs (recursive, excluding node_modules):
|
|
19
|
-
* - makesExecutable: contains chmodSync?( with an exec-bit octal literal
|
|
20
|
-
* - shellDashC: contains a sh/bash -c invocation (array form or string literal)
|
|
21
|
-
* - guarded: contains a process.platform / os.platform() / win32 / isWindows guard
|
|
22
|
-
* - optOut: contains the literal `windows-portability-ok`
|
|
23
|
-
* VIOLATION = makesExecutable && shellDashC && !guarded && !optOut
|
|
24
|
-
*
|
|
25
|
-
* ## Remediation
|
|
26
|
-
*
|
|
27
|
-
* Gate the bare-command execution with `if (process.platform !== 'win32')`,
|
|
28
|
-
* or invoke via an explicit interpreter (`sh <path>`), or annotate
|
|
29
|
-
* `// windows-portability-ok: <reason>`.
|
|
30
|
-
*
|
|
31
|
-
* ## Export contract (for unit tests)
|
|
32
|
-
*
|
|
33
|
-
* When required as a module (`require.main !== module`) this file exports
|
|
34
|
-
* `scanContent(source)` → { makesExecutable, shellDashC, guarded, optOut, violation }.
|
|
35
|
-
*/
|
|
36
|
-
|
|
37
|
-
const fs = require('fs');
|
|
38
|
-
const path = require('path');
|
|
39
|
-
|
|
40
|
-
// ─── Detection regexes ──────────────────────────────────────────────────────
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Match chmod/chmodSync( calls with an octal mode literal whose exec bits are
|
|
44
|
-
* set, e.g. `fs.chmodSync(p, 0o755)` or `chmod(file, 0o111)`.
|
|
45
|
-
*/
|
|
46
|
-
const CHMOD_RE = /chmod(?:Sync)?\s*\([^,;]+,\s*0o([0-7]{3})\b/g;
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Array form: execFileSync/spawnSync/exec* with 'sh' or 'bash' (optionally
|
|
50
|
-
* prefixed) as the first arg and '-c' as an element of the args array.
|
|
51
|
-
* e.g. execFileSync('bash', ['-c', ...]) or spawnSync('/bin/sh', ['-c', ...])
|
|
52
|
-
*/
|
|
53
|
-
const SHELL_ARRAY_RE =
|
|
54
|
-
/(?:execFile(?:Sync)?|spawnSync|spawn|exec)\s*\(\s*['"`](?:\/(?:usr\/)?bin\/)?(?:bash|sh)['"`]\s*,\s*\[[^\]]*['"]-c['"]/;
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* String-literal form: any string containing `bash -c` or `sh -c`.
|
|
58
|
-
*/
|
|
59
|
-
const SHELL_STRING_RE = /['"`][^'"`\n]*(?:bash|sh)\s+-c[^'"`\n]*['"`]/;
|
|
60
|
-
|
|
61
|
-
/** Platform guard presence. */
|
|
62
|
-
const GUARD_RE = /process\.platform|os\.platform\s*\(|\bwin32\b|\bisWindows\b/;
|
|
63
|
-
|
|
64
|
-
/** Opt-out annotation. */
|
|
65
|
-
const OPT_OUT_RE = /windows-portability-ok/;
|
|
66
|
-
|
|
67
|
-
// ─── Pure scanning function (exported for unit tests) ────────────────────────
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Scan a single file's source text and return detection flags.
|
|
71
|
-
*
|
|
72
|
-
* @param {string} source - The file contents as a string.
|
|
73
|
-
* @returns {{ makesExecutable: boolean, shellDashC: boolean, guarded: boolean, optOut: boolean, violation: boolean }}
|
|
74
|
-
*/
|
|
75
|
-
function scanContent(source) {
|
|
76
|
-
// Reset stateful regex before use.
|
|
77
|
-
CHMOD_RE.lastIndex = 0;
|
|
78
|
-
|
|
79
|
-
let makesExecutable = false;
|
|
80
|
-
let match;
|
|
81
|
-
while ((match = CHMOD_RE.exec(source)) !== null) {
|
|
82
|
-
const oct = match[1];
|
|
83
|
-
if ((parseInt(oct, 8) & 0o111) !== 0) {
|
|
84
|
-
makesExecutable = true;
|
|
85
|
-
break;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const shellDashC = SHELL_ARRAY_RE.test(source) || SHELL_STRING_RE.test(source);
|
|
90
|
-
const guarded = GUARD_RE.test(source);
|
|
91
|
-
const optOut = OPT_OUT_RE.test(source);
|
|
92
|
-
const violation = makesExecutable && shellDashC && !guarded && !optOut;
|
|
93
|
-
|
|
94
|
-
return { makesExecutable, shellDashC, guarded, optOut, violation };
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// ─── Filesystem walker ───────────────────────────────────────────────────────
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Recursively collect all *.test.cjs files under `dir`, excluding node_modules.
|
|
101
|
-
*
|
|
102
|
-
* @param {string} dir
|
|
103
|
-
* @param {string[]} [acc]
|
|
104
|
-
* @returns {string[]}
|
|
105
|
-
*/
|
|
106
|
-
function collectTestFiles(dir, acc) {
|
|
107
|
-
acc = acc || [];
|
|
108
|
-
let entries;
|
|
109
|
-
try {
|
|
110
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
111
|
-
} catch {
|
|
112
|
-
return acc;
|
|
113
|
-
}
|
|
114
|
-
for (const entry of entries) {
|
|
115
|
-
if (entry.name === 'node_modules') continue;
|
|
116
|
-
const full = path.join(dir, entry.name);
|
|
117
|
-
if (entry.isDirectory()) {
|
|
118
|
-
collectTestFiles(full, acc);
|
|
119
|
-
} else if (entry.isFile() && entry.name.endsWith('.test.cjs')) {
|
|
120
|
-
acc.push(full);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
return acc;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
127
|
-
|
|
128
|
-
function main() {
|
|
129
|
-
const ROOT = path.join(__dirname, '..');
|
|
130
|
-
const TESTS_DIR = path.join(ROOT, 'tests');
|
|
131
|
-
|
|
132
|
-
const files = collectTestFiles(TESTS_DIR);
|
|
133
|
-
const violations = [];
|
|
134
|
-
|
|
135
|
-
for (const file of files) {
|
|
136
|
-
let source;
|
|
137
|
-
try {
|
|
138
|
-
source = fs.readFileSync(file, 'utf8');
|
|
139
|
-
} catch {
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
const { violation } = scanContent(source);
|
|
143
|
-
if (violation) {
|
|
144
|
-
const rel = path.relative(ROOT, file).replace(/\\/g, '/');
|
|
145
|
-
violations.push(rel);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (violations.length > 0) {
|
|
150
|
-
for (const rel of violations) {
|
|
151
|
-
process.stderr.write(
|
|
152
|
-
`${rel}: chmod-executable + sh/bash -c with no platform guard\n`,
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
process.stderr.write(
|
|
156
|
-
'\nWindows Git Bash does not honor Node\'s chmod exec bit for ' +
|
|
157
|
-
'PATH-executing extension-less scripts ' +
|
|
158
|
-
'(CONTEXT.md → DEFECT.WINDOWS-TEST-PORTABILITY). ' +
|
|
159
|
-
'Gate the bare-command execution with ' +
|
|
160
|
-
'`if (process.platform !== \'win32\')`, or invoke via an explicit ' +
|
|
161
|
-
'interpreter (`sh <path>`), or annotate ' +
|
|
162
|
-
'`// windows-portability-ok: <reason>`.\n',
|
|
163
|
-
);
|
|
164
|
-
process.exitCode = 1;
|
|
165
|
-
} else {
|
|
166
|
-
console.log(
|
|
167
|
-
`ok lint-windows-test-portability: ${files.length} file(s) scanned, no violations`,
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// ─── Module boundary ─────────────────────────────────────────────────────────
|
|
173
|
-
|
|
174
|
-
if (require.main === module) {
|
|
175
|
-
main();
|
|
176
|
-
} else {
|
|
177
|
-
module.exports = { scanContent };
|
|
178
|
-
}
|