@ikunin/sprintpilot 1.0.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.
Files changed (86) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +330 -0
  3. package/_Sprintpilot/.secrets-allowlist +26 -0
  4. package/_Sprintpilot/Sprintpilot.md +216 -0
  5. package/_Sprintpilot/lib/runtime/args.js +77 -0
  6. package/_Sprintpilot/lib/runtime/git.js +24 -0
  7. package/_Sprintpilot/lib/runtime/http.js +96 -0
  8. package/_Sprintpilot/lib/runtime/log.js +30 -0
  9. package/_Sprintpilot/lib/runtime/secrets.js +151 -0
  10. package/_Sprintpilot/lib/runtime/spawn.js +68 -0
  11. package/_Sprintpilot/lib/runtime/text.js +26 -0
  12. package/_Sprintpilot/lib/runtime/yaml-lite.js +160 -0
  13. package/_Sprintpilot/manifest.yaml +26 -0
  14. package/_Sprintpilot/modules/autopilot/config.yaml +20 -0
  15. package/_Sprintpilot/modules/git/branching-and-pr-strategy.md +101 -0
  16. package/_Sprintpilot/modules/git/config.yaml +83 -0
  17. package/_Sprintpilot/modules/git/templates/commit-patch.txt +1 -0
  18. package/_Sprintpilot/modules/git/templates/commit-story.txt +1 -0
  19. package/_Sprintpilot/modules/git/templates/pr-body.md +20 -0
  20. package/_Sprintpilot/modules/ma/config.yaml +9 -0
  21. package/_Sprintpilot/scripts/create-pr.js +284 -0
  22. package/_Sprintpilot/scripts/detect-platform.js +64 -0
  23. package/_Sprintpilot/scripts/health-check.js +98 -0
  24. package/_Sprintpilot/scripts/lint-changed.js +249 -0
  25. package/_Sprintpilot/scripts/lock.js +195 -0
  26. package/_Sprintpilot/scripts/sanitize-branch.js +107 -0
  27. package/_Sprintpilot/scripts/stage-and-commit.js +190 -0
  28. package/_Sprintpilot/scripts/sync-status.js +141 -0
  29. package/_Sprintpilot/skills/sprint-autopilot-off/SKILL.md +6 -0
  30. package/_Sprintpilot/skills/sprint-autopilot-off/workflow.md +154 -0
  31. package/_Sprintpilot/skills/sprint-autopilot-on/SKILL.md +6 -0
  32. package/_Sprintpilot/skills/sprint-autopilot-on/workflow.md +1119 -0
  33. package/_Sprintpilot/skills/sprintpilot-assess/SKILL.md +6 -0
  34. package/_Sprintpilot/skills/sprintpilot-assess/agents/debt-classifier.md +64 -0
  35. package/_Sprintpilot/skills/sprintpilot-assess/agents/dependency-auditor.md +57 -0
  36. package/_Sprintpilot/skills/sprintpilot-assess/agents/migration-analyzer.md +62 -0
  37. package/_Sprintpilot/skills/sprintpilot-assess/workflow.md +114 -0
  38. package/_Sprintpilot/skills/sprintpilot-code-review/SKILL.md +6 -0
  39. package/_Sprintpilot/skills/sprintpilot-code-review/agents/acceptance-auditor.md +51 -0
  40. package/_Sprintpilot/skills/sprintpilot-code-review/agents/blind-hunter.md +39 -0
  41. package/_Sprintpilot/skills/sprintpilot-code-review/agents/edge-case-hunter.md +46 -0
  42. package/_Sprintpilot/skills/sprintpilot-code-review/workflow.md +111 -0
  43. package/_Sprintpilot/skills/sprintpilot-codebase-map/SKILL.md +6 -0
  44. package/_Sprintpilot/skills/sprintpilot-codebase-map/agents/architecture-mapper.md +129 -0
  45. package/_Sprintpilot/skills/sprintpilot-codebase-map/agents/concerns-hunter.md +135 -0
  46. package/_Sprintpilot/skills/sprintpilot-codebase-map/agents/integration-mapper.md +138 -0
  47. package/_Sprintpilot/skills/sprintpilot-codebase-map/agents/quality-assessor.md +143 -0
  48. package/_Sprintpilot/skills/sprintpilot-codebase-map/agents/stack-analyzer.md +133 -0
  49. package/_Sprintpilot/skills/sprintpilot-codebase-map/workflow.md +120 -0
  50. package/_Sprintpilot/skills/sprintpilot-migrate/SKILL.md +6 -0
  51. package/_Sprintpilot/skills/sprintpilot-migrate/agents/dependency-analyzer.md +51 -0
  52. package/_Sprintpilot/skills/sprintpilot-migrate/agents/risk-assessor.md +55 -0
  53. package/_Sprintpilot/skills/sprintpilot-migrate/agents/stack-mapper.md +49 -0
  54. package/_Sprintpilot/skills/sprintpilot-migrate/agents/test-parity-analyzer.md +49 -0
  55. package/_Sprintpilot/skills/sprintpilot-migrate/resources/coexistence-patterns.md +59 -0
  56. package/_Sprintpilot/skills/sprintpilot-migrate/resources/strategies.md +43 -0
  57. package/_Sprintpilot/skills/sprintpilot-migrate/templates/component-card.md +11 -0
  58. package/_Sprintpilot/skills/sprintpilot-migrate/templates/migration-epics.md +35 -0
  59. package/_Sprintpilot/skills/sprintpilot-migrate/templates/migration-plan.md +66 -0
  60. package/_Sprintpilot/skills/sprintpilot-migrate/workflow.md +235 -0
  61. package/_Sprintpilot/skills/sprintpilot-party-mode/SKILL.md +6 -0
  62. package/_Sprintpilot/skills/sprintpilot-party-mode/workflow.md +138 -0
  63. package/_Sprintpilot/skills/sprintpilot-research/SKILL.md +6 -0
  64. package/_Sprintpilot/skills/sprintpilot-research/workflow.md +128 -0
  65. package/_Sprintpilot/skills/sprintpilot-reverse-architect/SKILL.md +6 -0
  66. package/_Sprintpilot/skills/sprintpilot-reverse-architect/agents/component-mapper.md +53 -0
  67. package/_Sprintpilot/skills/sprintpilot-reverse-architect/agents/data-flow-tracer.md +54 -0
  68. package/_Sprintpilot/skills/sprintpilot-reverse-architect/agents/pattern-extractor.md +67 -0
  69. package/_Sprintpilot/skills/sprintpilot-reverse-architect/workflow.md +119 -0
  70. package/_Sprintpilot/skills/sprintpilot-update/SKILL.md +6 -0
  71. package/_Sprintpilot/skills/sprintpilot-update/workflow.md +46 -0
  72. package/_Sprintpilot/templates/agent-rules.md +43 -0
  73. package/bin/sprintpilot.js +95 -0
  74. package/lib/commands/check-update.js +54 -0
  75. package/lib/commands/install.js +876 -0
  76. package/lib/commands/uninstall.js +218 -0
  77. package/lib/core/bmad-config.js +113 -0
  78. package/lib/core/file-ops.js +90 -0
  79. package/lib/core/gitignore.js +54 -0
  80. package/lib/core/markers.js +126 -0
  81. package/lib/core/tool-registry.js +73 -0
  82. package/lib/core/update-check.js +39 -0
  83. package/lib/core/v1-detect.js +86 -0
  84. package/lib/prompts.js +82 -0
  85. package/lib/substitute.js +39 -0
  86. package/package.json +49 -0
@@ -0,0 +1,216 @@
1
+ # BMad Method Workflow — Mandatory for All AI Agents
2
+
3
+ This project uses the **BMad Method** with **Sprintpilot** (autopilot + multi-agent addon). Always use the BMad Method workflow for every story. Never skip steps. When unsure, invoke `bmad-help` first.
4
+
5
+ ---
6
+
7
+ ## Navigation & orientation
8
+
9
+ | Skill | When to use |
10
+ |-------|-------------|
11
+ | `bmad-help` | First resort when unsure what to do next — analyzes current state and recommends the right skill or workflow |
12
+
13
+ ---
14
+
15
+ ## Sprintpilot — autopilot & git workflow
16
+
17
+ | Skill | When to use |
18
+ |-------|-------------|
19
+ | `sprint-autopilot-on` | Start autonomous story execution with git branching, commits, and PRs |
20
+ | `sprint-autopilot-off` | Disengage autopilot, show sprint + git status report |
21
+
22
+ When Sprintpilot or the git addon is active:
23
+ - **NEVER** use `git add -A` or `git add .` — always stage files explicitly by name
24
+ - **NEVER** commit secrets, API keys, tokens, or credentials
25
+ - Each story gets its own isolated worktree and branch (`story/<key>`)
26
+ - Commits use conventional format: `feat(<epic>): <title> (<key>)`
27
+
28
+ ---
29
+
30
+ ## Full skill reference by lifecycle phase
31
+
32
+ ### Phase 0 — Project inception (new projects)
33
+
34
+ | Skill | When to use |
35
+ |-------|-------------|
36
+ | `bmad-product-brief` | Start here for a new project — collaborative discovery of goals, constraints, users |
37
+ | `bmad-create-prd` | Create a full Product Requirements Document from scratch |
38
+ | `bmad-edit-prd` | Update or revise an existing PRD |
39
+ | `bmad-validate-prd` | Validate a PRD against BMad Method standards before moving to design |
40
+ | `bmad-create-architecture` | Create technical architecture and solution design decisions |
41
+ | `bmad-create-ux-design` | Plan UX patterns and design specifications |
42
+ | `bmad-create-epics-and-stories` | Break PRD + architecture into epics and story list |
43
+ | `bmad-generate-project-context` | Generate `project-context.md` with AI rules (run once after inception) |
44
+ | `bmad-document-project` | Document an existing (brownfield) project to give AI agents context |
45
+
46
+ ### Phase 1 — Sprint planning (before development starts)
47
+
48
+ | Skill | When to use |
49
+ |-------|-------------|
50
+ | `bmad-sprint-planning` | Generate `sprint-status.yaml` tracking file from epics list |
51
+ | `bmad-sprint-status` | Check current sprint status and surface risks at any time |
52
+ | `bmad-correct-course` | Manage significant scope or direction changes mid-sprint |
53
+
54
+ ### Phase 2 — Story development (the mandatory per-story loop)
55
+
56
+ See **Mandatory sequence per story** section below.
57
+
58
+ ### Phase 3 — Epic close-out
59
+
60
+ | Skill | When to use |
61
+ |-------|-------------|
62
+ | `bmad-retrospective` | Run after all stories in an epic are `done`; saves lessons, marks epic `done` |
63
+
64
+ ---
65
+
66
+ ## Quick path — for small changes without full story ceremony
67
+
68
+ | Skill | When to use |
69
+ |-------|-------------|
70
+ | `bmad-quick-dev` | Implement any user intent (bug fix, tweak, refactor) directly — follows project conventions |
71
+
72
+ > Use the quick path only for genuinely small, isolated changes. For anything touching multiple components or requiring E2E coverage, use the full story loop.
73
+
74
+ ---
75
+
76
+ ## Sprintpilot multi-agent skills
77
+
78
+ These launch parallel subagents for deeper, faster analysis:
79
+
80
+ | Skill | Agents | When to use |
81
+ |-------|--------|-------------|
82
+ | `sprintpilot-code-review` | 3 | Parallel adversarial code review (Blind Hunter + Edge Case + Acceptance) |
83
+ | `sprintpilot-codebase-map` | 5 | Brownfield codebase mapping (stack, architecture, quality, concerns, integrations) |
84
+ | `sprintpilot-assess` | 3 | Tech debt assessment (dependency audit, debt classification, migration analysis) |
85
+ | `sprintpilot-reverse-architect` | 3 | Extract architecture from existing code (components, data flow, patterns) |
86
+ | `sprintpilot-migrate` | 4 | Full migration planning — 12 steps from current stack to target stack |
87
+ | `sprintpilot-research` | N | Parallel research fan-out with web search |
88
+ | `sprintpilot-party-mode` | 2-3 | Multi-persona discussion (architect, PM, QA, etc. debating in parallel) |
89
+
90
+ ### Brownfield analysis pipeline
91
+
92
+ ```
93
+ sprintpilot-codebase-map → sprintpilot-assess → sprintpilot-reverse-architect → sprintpilot-migrate
94
+ ```
95
+
96
+ Run `sprintpilot-codebase-map` first on any existing codebase. The other multi-agent skills consume its outputs.
97
+
98
+ ---
99
+
100
+ ## Research & discovery skills
101
+
102
+ | Skill | When to use |
103
+ |-------|-------------|
104
+ | `bmad-technical-research` | Research technologies, frameworks, or architectural trade-offs |
105
+ | `bmad-domain-research` | Research a business domain or industry |
106
+ | `bmad-market-research` | Research market competition and customers |
107
+ | `bmad-brainstorming` | Facilitate structured ideation sessions |
108
+ | `bmad-advanced-elicitation` | Push the model to reconsider and refine recent output |
109
+
110
+ ---
111
+
112
+ ## QA & test architecture skills
113
+
114
+ | Skill | When to use |
115
+ |-------|-------------|
116
+ | `bmad-qa-generate-e2e-tests` | Generate E2E tests for existing features (retroactive coverage) |
117
+ | `bmad-testarch-framework` | Initialize test framework (Playwright / Cypress) |
118
+ | `bmad-testarch-atdd` | Generate failing acceptance tests (TDD cycle) |
119
+ | `bmad-testarch-test-design` | Create system-level or epic-level test plans |
120
+ | `bmad-testarch-nfr` | Assess non-functional requirements (performance, security, reliability) |
121
+ | `bmad-testarch-ci` | Scaffold CI/CD quality pipeline with test execution |
122
+ | `bmad-testarch-trace` | Generate traceability matrix and quality gate decisions |
123
+ | `bmad-testarch-test-review` | Review test quality against best practices |
124
+ | `bmad-testarch-automate` | Expand test automation coverage across the codebase |
125
+ | `bmad-teach-me-testing` | Interactive testing education sessions |
126
+
127
+ ---
128
+
129
+ ## Review skills
130
+
131
+ | Skill | When to use |
132
+ |-------|-------------|
133
+ | `bmad-code-review` | Full adversarial code review (3 layers) — mandatory step 5 of per-story loop |
134
+ | `bmad-review-adversarial-general` | Cynical critical review of any artifact (specs, designs, docs) |
135
+ | `bmad-review-edge-case-hunter` | Exhaustive edge-case and boundary analysis of code or specs |
136
+ | `bmad-editorial-review-structure` | Structural editing of documents |
137
+ | `bmad-editorial-review-prose` | Copy-editing for communication issues in documents |
138
+
139
+ ---
140
+
141
+ ## Agent role personas
142
+
143
+ These skills activate an interactive agent persona. They stay in character until given an exit command.
144
+
145
+ | Skill | Persona |
146
+ |-------|---------|
147
+ | `bmad-agent-pm` | Product Manager |
148
+ | `bmad-agent-analyst` | Business Analyst |
149
+ | `bmad-agent-architect` | Solution Architect |
150
+ | `bmad-agent-ux-designer` | UX Designer |
151
+ | `bmad-agent-dev` | Developer |
152
+ | `bmad-agent-qa` | QA Engineer |
153
+ | `bmad-agent-sm` | Scrum Master |
154
+ | `bmad-agent-tech-writer` | Technical Writer |
155
+ | `bmad-agent-quick-flow-solo-dev` | Rapid full-stack solo developer |
156
+ | `bmad-party-mode` | All agents in one group discussion |
157
+
158
+ ---
159
+
160
+ ## Document utilities
161
+
162
+ | Skill | When to use |
163
+ |-------|-------------|
164
+ | `bmad-shard-doc` | Split a large markdown document into organized smaller files |
165
+ | `bmad-index-docs` | Generate or update an `index.md` for a docs folder |
166
+ | `bmad-distillator` | Lossless LLM-optimized compression of source documents |
167
+
168
+ ---
169
+
170
+ ## Mandatory sequence per story
171
+
172
+ | Step | Skill | sprint-status.yaml transition | Definition of Done |
173
+ |------|-------|-------------------------------|--------------------|
174
+ | 1 | `bmad-create-story` | → `ready-for-dev` | Story file exists, all sections complete |
175
+ | 2 | `bmad-check-implementation-readiness` | (no change) | Readiness check passes with no blockers |
176
+ | 3 | `bmad-dev-story` (RED) | → `in-progress` | Tests written and **confirmed failing** before any implementation |
177
+ | 4 | `bmad-dev-story` (GREEN) | → `review` | All tests pass; pass count stated explicitly (e.g. "9/9 passed") |
178
+ | 5 | `bmad-code-review` | (no change) | All review layers complete; findings triaged |
179
+ | 6 | Apply `patch` findings + re-run tests | → `done` | All patch tasks completed, tests still green, pass count confirmed |
180
+ | 7 | `bmad-retrospective` (per epic, after all stories done) | epic → `done` | Retrospective output saved; epic marked done |
181
+
182
+ ## Task list hygiene
183
+
184
+ BMad Method skills only update `{implementation_artifacts}/sprint-status.yaml` — they do NOT update the coding agent's task list.
185
+
186
+ The task list must always reflect the full BMad Method work breakdown. Before starting a story, create a task for **each step** above. Keep the list granular enough that anyone can see exactly where work stands at a glance.
187
+
188
+ Rules:
189
+ - Create all step-tasks for a story **before** starting work on it
190
+ - Mark each step-task `in_progress` when you begin it
191
+ - Mark each step-task `completed` immediately when done — never batch
192
+ - Do not start step N+1 until step N is `completed`
193
+ - `sprint-status.yaml` is updated automatically by BMad Method skills — do NOT edit it manually
194
+
195
+ ## Issue and patch tracking
196
+
197
+ When tests fail **or** code review produces `patch` findings, each issue gets its own task:
198
+
199
+ - Create one task per distinct issue / patch item (not one task for all issues)
200
+ - Name it clearly: e.g. "Fix: `deleteTask` transaction not awaited" or "Patch P1: rollback missing on Ctrl+Down"
201
+ - Mark it `in_progress` when fixing, `completed` only after the fix is applied **and the relevant test passes**
202
+ - State the test result explicitly: "fixed — 10/10 passed"
203
+ - Do not mark the parent step-task `completed` until all its issue/patch tasks are `completed`
204
+
205
+ ## Story file updates
206
+
207
+ After `bmad-dev-story` completes, fill in the story file's `Dev Agent Record` section:
208
+ - List all files changed
209
+ - Note any non-obvious decisions or deviations from the spec
210
+ - Record final test pass count
211
+
212
+ ## Test result transparency
213
+
214
+ Every time tests are run, state the result explicitly in your response:
215
+ - `N/N passed` — or — `N passed, M failed` with failure details
216
+ - Never say "tests pass" without the count
@@ -0,0 +1,77 @@
1
+ 'use strict';
2
+
3
+ function parseArgs(argv, { booleanFlags = [], positionalActions = null } = {}) {
4
+ const opts = {};
5
+ const positional = [];
6
+ const flatBool = new Set(booleanFlags);
7
+ const actionSet = positionalActions ? new Set(positionalActions) : null;
8
+ const actions = [];
9
+
10
+ let i = 0;
11
+ while (i < argv.length) {
12
+ const token = argv[i];
13
+
14
+ if (token === '-h' || token === '--help') {
15
+ opts.help = true;
16
+ i++;
17
+ continue;
18
+ }
19
+
20
+ if (token.startsWith('--')) {
21
+ const eq = token.indexOf('=');
22
+ let name;
23
+ let value;
24
+ if (eq !== -1) {
25
+ name = token.slice(2, eq);
26
+ value = token.slice(eq + 1);
27
+ opts[name] = value;
28
+ i++;
29
+ continue;
30
+ }
31
+ name = token.slice(2);
32
+ if (flatBool.has(name)) {
33
+ opts[name] = true;
34
+ i++;
35
+ continue;
36
+ }
37
+ const next = argv[i + 1];
38
+ if (next === undefined || next.startsWith('-')) {
39
+ opts[name] = true;
40
+ i++;
41
+ } else {
42
+ opts[name] = next;
43
+ i += 2;
44
+ }
45
+ continue;
46
+ }
47
+
48
+ if (token.startsWith('-') && token.length === 2) {
49
+ const name = token.slice(1);
50
+ const next = argv[i + 1];
51
+ if (flatBool.has(name)) {
52
+ opts[name] = true;
53
+ i++;
54
+ continue;
55
+ }
56
+ if (next === undefined || next.startsWith('-')) {
57
+ opts[name] = true;
58
+ i++;
59
+ } else {
60
+ opts[name] = next;
61
+ i += 2;
62
+ }
63
+ continue;
64
+ }
65
+
66
+ if (actionSet && actionSet.has(token)) {
67
+ actions.push(token);
68
+ } else {
69
+ positional.push(token);
70
+ }
71
+ i++;
72
+ }
73
+
74
+ return { opts, positional, actions };
75
+ }
76
+
77
+ module.exports = { parseArgs };
@@ -0,0 +1,24 @@
1
+ 'use strict';
2
+
3
+ const { run, tryRun } = require('./spawn');
4
+
5
+ async function git(args, opts = {}) {
6
+ return run('git', args, opts);
7
+ }
8
+
9
+ async function tryGit(args, opts = {}) {
10
+ return tryRun('git', args, opts);
11
+ }
12
+
13
+ async function gitStdout(args, opts = {}) {
14
+ const { stdout } = await git(args, opts);
15
+ return stdout.trim();
16
+ }
17
+
18
+ async function tryGitStdout(args, opts = {}) {
19
+ const r = await tryGit(args, opts);
20
+ if (r.exitCode !== 0) return null;
21
+ return r.stdout.trim();
22
+ }
23
+
24
+ module.exports = { git, tryGit, gitStdout, tryGitStdout };
@@ -0,0 +1,96 @@
1
+ 'use strict';
2
+
3
+ const https = require('node:https');
4
+ const http = require('node:http');
5
+ const { URL } = require('node:url');
6
+
7
+ // Bound response bodies so a malicious / misconfigured server cannot OOM the
8
+ // process with an unbounded chunked response. 5 MB is generous for any PR
9
+ // API response we'd expect.
10
+ const MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
11
+
12
+ function postJson(urlStr, body, { headers = {}, timeoutMs = 15_000 } = {}) {
13
+ return new Promise((resolve, reject) => {
14
+ let url;
15
+ try {
16
+ url = new URL(urlStr);
17
+ } catch (e) {
18
+ return reject(e);
19
+ }
20
+
21
+ // Single resolve/reject guard — both `req.on('error')` and `res.on('error')`
22
+ // can fire on the size-cap abort path (we call req.destroy(err)). Without
23
+ // this, the observed error message becomes non-deterministic.
24
+ let settled = false;
25
+ const done = (fn, val) => { if (!settled) { settled = true; fn(val); } };
26
+ const ok = (val) => done(resolve, val);
27
+ const fail = (err) => done(reject, err);
28
+
29
+ const payload = typeof body === 'string' ? body : JSON.stringify(body);
30
+ // Pick the transport based on URL scheme so http:// can be used by local
31
+ // integration tests without standing up a TLS cert. Production callers
32
+ // always use https://.
33
+ const transport = url.protocol === 'http:' ? http : https;
34
+ const defaultPort = url.protocol === 'http:' ? 80 : 443;
35
+ const req = transport.request(
36
+ {
37
+ method: 'POST',
38
+ hostname: url.hostname,
39
+ port: url.port || defaultPort,
40
+ path: `${url.pathname}${url.search || ''}`,
41
+ headers: {
42
+ 'Content-Type': 'application/json',
43
+ 'Content-Length': Buffer.byteLength(payload),
44
+ 'User-Agent': 'sprintpilot',
45
+ ...headers,
46
+ },
47
+ timeout: timeoutMs,
48
+ },
49
+ (res) => {
50
+ // We do NOT follow redirects: the caller's Authorization header
51
+ // could leak to an unintended host, and upstream PR APIs don't
52
+ // return 3xx for normal success paths. Surface redirects explicitly
53
+ // so the caller can fix the base URL.
54
+ if (res.statusCode >= 300 && res.statusCode < 400) {
55
+ const location = res.headers.location || '<no Location header>';
56
+ res.resume(); // drain
57
+ return ok({
58
+ statusCode: res.statusCode,
59
+ body: `redirect not supported; Location: ${location}`,
60
+ json: null,
61
+ });
62
+ }
63
+
64
+ const chunks = [];
65
+ let total = 0;
66
+ let aborted = false;
67
+ res.on('data', (c) => {
68
+ if (aborted) return;
69
+ total += c.length;
70
+ if (total > MAX_RESPONSE_BYTES) {
71
+ aborted = true;
72
+ req.destroy(new Error(`response exceeded ${MAX_RESPONSE_BYTES} bytes`));
73
+ return;
74
+ }
75
+ chunks.push(c);
76
+ });
77
+ res.on('end', () => {
78
+ if (aborted) return; // error path handles it
79
+ const text = Buffer.concat(chunks).toString('utf8');
80
+ let json = null;
81
+ try { json = JSON.parse(text); } catch { /* non-json */ }
82
+ ok({ statusCode: res.statusCode, body: text, json });
83
+ });
84
+ res.on('error', fail);
85
+ }
86
+ );
87
+ req.on('timeout', () => {
88
+ req.destroy(new Error(`HTTP timeout after ${timeoutMs}ms`));
89
+ });
90
+ req.on('error', fail);
91
+ req.write(payload);
92
+ req.end();
93
+ });
94
+ }
95
+
96
+ module.exports = { postJson, MAX_RESPONSE_BYTES };
@@ -0,0 +1,30 @@
1
+ 'use strict';
2
+
3
+ function out(msg) {
4
+ process.stdout.write(String(msg));
5
+ if (!String(msg).endsWith('\n')) process.stdout.write('\n');
6
+ }
7
+
8
+ function err(msg) {
9
+ process.stderr.write(String(msg));
10
+ if (!String(msg).endsWith('\n')) process.stderr.write('\n');
11
+ }
12
+
13
+ function warn(msg) {
14
+ err(`WARN: ${msg}`);
15
+ }
16
+
17
+ function info(msg) {
18
+ err(`INFO: ${msg}`);
19
+ }
20
+
21
+ function error(msg) {
22
+ err(`ERROR: ${msg}`);
23
+ }
24
+
25
+ function fail(msg, code = 1) {
26
+ error(msg);
27
+ process.exit(code);
28
+ }
29
+
30
+ module.exports = { out, err, warn, info, error, fail };
@@ -0,0 +1,151 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+
5
+ // Keyword-based fuzzy matches (high false-positive rate, but useful as a
6
+ // last-resort catch for things like `API_KEY = "..."`).
7
+ const SECRET_KEYWORD = /API_KEY|SECRET|TOKEN|PASSWORD|aws_access|private_key/i;
8
+
9
+ // Concrete, high-confidence key prefixes / formats — these are what real
10
+ // secrets actually look like. Matching here is much less noisy than the
11
+ // keyword list above.
12
+ const SECRET_FORMATS = [
13
+ /\bAKIA[0-9A-Z]{16}\b/, // AWS Access Key ID
14
+ /\bASIA[0-9A-Z]{16}\b/, // AWS temporary Access Key ID
15
+ /\bghp_[A-Za-z0-9]{30,}\b/, // GitHub personal access token
16
+ /\bgho_[A-Za-z0-9]{30,}\b/, // GitHub OAuth token
17
+ /\bghu_[A-Za-z0-9]{30,}\b/, // GitHub user-to-server token
18
+ /\bghs_[A-Za-z0-9]{30,}\b/, // GitHub server-to-server token
19
+ /\bghr_[A-Za-z0-9]{30,}\b/, // GitHub refresh token
20
+ /\bgithub_pat_[A-Za-z0-9_]{20,}\b/, // GitHub fine-grained PAT
21
+ /\bsk-[A-Za-z0-9_-]{20,}\b/, // OpenAI / Anthropic-like
22
+ /\bsk_live_[A-Za-z0-9]{20,}\b/, // Stripe live secret
23
+ /\bsk_test_[A-Za-z0-9]{20,}\b/, // Stripe test secret
24
+ /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/, // Slack tokens
25
+ /\bAIza[0-9A-Za-z_-]{35,99}\b/, // Google API key (standard is 39 chars = AIza + 35)
26
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----/, // PEM private key header
27
+ ];
28
+
29
+ // Deprecated: callers should use `matchesSecret(line)` which applies both
30
+ // the keyword list and the concrete-format list. Retained only for
31
+ // compatibility with anyone importing the old symbol.
32
+ const SECRET_PATTERN = SECRET_KEYWORD;
33
+
34
+ function matchesSecret(line) {
35
+ if (SECRET_KEYWORD.test(line)) return true;
36
+ for (const re of SECRET_FORMATS) {
37
+ if (re.test(line)) return true;
38
+ }
39
+ return false;
40
+ }
41
+
42
+ function scanLinesForSecrets(text, maxHits = 3) {
43
+ if (!text) return [];
44
+ const out = [];
45
+ const lines = text.split(/\r?\n/);
46
+ for (let i = 0; i < lines.length; i++) {
47
+ if (matchesSecret(lines[i])) {
48
+ out.push({ line: i + 1, text: lines[i] });
49
+ if (out.length >= maxHits) break;
50
+ }
51
+ }
52
+ return out;
53
+ }
54
+
55
+ function globToRegex(glob) {
56
+ let src = '^';
57
+ let i = 0;
58
+ while (i < glob.length) {
59
+ const c = glob[i];
60
+ if (c === '*') {
61
+ if (glob[i + 1] === '*') {
62
+ src += '.*';
63
+ i += 2;
64
+ if (glob[i] === '/') i++;
65
+ } else {
66
+ src += '[^/]*';
67
+ i++;
68
+ }
69
+ continue;
70
+ }
71
+ if (c === '?') {
72
+ src += '[^/]';
73
+ i++;
74
+ continue;
75
+ }
76
+ if ('.+^$(){}|\\'.indexOf(c) !== -1) {
77
+ src += '\\' + c;
78
+ i++;
79
+ continue;
80
+ }
81
+ if (c === '[') {
82
+ const close = glob.indexOf(']', i);
83
+ if (close === -1) {
84
+ src += '\\[';
85
+ i++;
86
+ continue;
87
+ }
88
+ src += glob.slice(i, close + 1);
89
+ i = close + 1;
90
+ continue;
91
+ }
92
+ src += c;
93
+ i++;
94
+ }
95
+ src += '$';
96
+ return new RegExp(src);
97
+ }
98
+
99
+ function parseAllowlist(filePath) {
100
+ if (!filePath) return [];
101
+ let raw;
102
+ try {
103
+ raw = fs.readFileSync(filePath, 'utf8');
104
+ } catch {
105
+ return [];
106
+ }
107
+ return raw
108
+ .split(/\r?\n/)
109
+ .map((l) => l.trim())
110
+ .filter((l) => l && !l.startsWith('#'))
111
+ .map((glob) => ({ glob, regex: globToRegex(glob) }));
112
+ }
113
+
114
+ function isAllowlisted(filePath, patterns) {
115
+ return patterns.some(({ regex }) => regex.test(filePath));
116
+ }
117
+
118
+ function isTextSafeSample(buf) {
119
+ // Binary detection: any NUL byte -> binary.
120
+ for (let i = 0; i < buf.length; i++) {
121
+ if (buf[i] === 0) return false;
122
+ }
123
+ return true;
124
+ }
125
+
126
+ function isBinaryFile(filePath) {
127
+ try {
128
+ const fd = fs.openSync(filePath, 'r');
129
+ try {
130
+ const buf = Buffer.alloc(8192);
131
+ const bytes = fs.readSync(fd, buf, 0, buf.length, 0);
132
+ if (bytes === 0) return false;
133
+ return !isTextSafeSample(buf.subarray(0, bytes));
134
+ } finally {
135
+ fs.closeSync(fd);
136
+ }
137
+ } catch {
138
+ return false;
139
+ }
140
+ }
141
+
142
+ module.exports = {
143
+ SECRET_PATTERN, // deprecated — prefer matchesSecret
144
+ SECRET_FORMATS,
145
+ matchesSecret,
146
+ scanLinesForSecrets,
147
+ globToRegex,
148
+ parseAllowlist,
149
+ isAllowlisted,
150
+ isBinaryFile,
151
+ };
@@ -0,0 +1,68 @@
1
+ 'use strict';
2
+
3
+ const child = require('node:child_process');
4
+
5
+ function run(file, args, { cwd, timeoutMs = 30_000, input, env } = {}) {
6
+ return new Promise((resolve, reject) => {
7
+ const proc = child.execFile(
8
+ file,
9
+ args,
10
+ {
11
+ cwd,
12
+ timeout: timeoutMs,
13
+ windowsHide: true,
14
+ maxBuffer: 10 * 1024 * 1024,
15
+ env: env || process.env,
16
+ },
17
+ (err, stdout, stderr) => {
18
+ if (err) {
19
+ const e = new Error(
20
+ `${file} ${args.join(' ')} failed with code ${err.code ?? err.signal ?? 'unknown'}`
21
+ );
22
+ e.exitCode = typeof err.code === 'number' ? err.code : 1;
23
+ e.signal = err.signal || null;
24
+ e.stdout = String(stdout || '');
25
+ e.stderr = String(stderr || '');
26
+ e.originalError = err;
27
+ return reject(e);
28
+ }
29
+ resolve({ stdout: String(stdout || ''), stderr: String(stderr || ''), exitCode: 0 });
30
+ }
31
+ );
32
+ // Rejecting on spawn-time errors (ENOENT etc) ensures the caller sees a
33
+ // rejected Promise rather than hanging, even if `proc.stdin` was never
34
+ // attached. Without this, a missing binary can crash via `proc.stdin.write`.
35
+ proc.on('error', reject);
36
+ if (input !== undefined && proc.stdin) {
37
+ proc.stdin.on('error', () => { /* ignore EPIPE etc */ });
38
+ try { proc.stdin.write(input); } catch { /* ignore */ }
39
+ try { proc.stdin.end(); } catch { /* ignore */ }
40
+ }
41
+ });
42
+ }
43
+
44
+ async function tryRun(file, args, opts) {
45
+ try {
46
+ return await run(file, args, opts);
47
+ } catch (e) {
48
+ return { stdout: e.stdout || '', stderr: e.stderr || '', exitCode: e.exitCode ?? 1, error: e };
49
+ }
50
+ }
51
+
52
+ function runInherit(file, args, { cwd, env, input } = {}) {
53
+ return new Promise((resolve, reject) => {
54
+ const proc = child.spawn(file, args, {
55
+ cwd,
56
+ env: env || process.env,
57
+ stdio: input !== undefined ? ['pipe', 'inherit', 'inherit'] : 'inherit',
58
+ windowsHide: true,
59
+ });
60
+ proc.on('error', reject);
61
+ proc.on('close', (code) => resolve({ exitCode: code ?? 0 }));
62
+ if (input !== undefined && proc.stdin) {
63
+ try { proc.stdin.write(input); proc.stdin.end(); } catch { /* ignore */ }
64
+ }
65
+ });
66
+ }
67
+
68
+ module.exports = { run, tryRun, runInherit };
@@ -0,0 +1,26 @@
1
+ 'use strict';
2
+
3
+ function splitLines(text) {
4
+ if (!text) return [];
5
+ const trimmedTrailing = text.replace(/\r?\n$/, '');
6
+ if (trimmedTrailing === '') return [];
7
+ return trimmedTrailing.split(/\r?\n/);
8
+ }
9
+
10
+ function headLines(text, n) {
11
+ if (n <= 0) return '';
12
+ const lines = splitLines(text);
13
+ return lines.slice(0, n).join('\n');
14
+ }
15
+
16
+ function countLines(text) {
17
+ return splitLines(text).length;
18
+ }
19
+
20
+ function extractUrl(text) {
21
+ if (!text) return null;
22
+ const m = text.match(/https?:\/\/[^\s"'<>)]+/);
23
+ return m ? m[0] : null;
24
+ }
25
+
26
+ module.exports = { splitLines, headLines, countLines, extractUrl };