@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.
- package/LICENSE +201 -0
- package/README.md +330 -0
- package/_Sprintpilot/.secrets-allowlist +26 -0
- package/_Sprintpilot/Sprintpilot.md +216 -0
- package/_Sprintpilot/lib/runtime/args.js +77 -0
- package/_Sprintpilot/lib/runtime/git.js +24 -0
- package/_Sprintpilot/lib/runtime/http.js +96 -0
- package/_Sprintpilot/lib/runtime/log.js +30 -0
- package/_Sprintpilot/lib/runtime/secrets.js +151 -0
- package/_Sprintpilot/lib/runtime/spawn.js +68 -0
- package/_Sprintpilot/lib/runtime/text.js +26 -0
- package/_Sprintpilot/lib/runtime/yaml-lite.js +160 -0
- package/_Sprintpilot/manifest.yaml +26 -0
- package/_Sprintpilot/modules/autopilot/config.yaml +20 -0
- package/_Sprintpilot/modules/git/branching-and-pr-strategy.md +101 -0
- package/_Sprintpilot/modules/git/config.yaml +83 -0
- package/_Sprintpilot/modules/git/templates/commit-patch.txt +1 -0
- package/_Sprintpilot/modules/git/templates/commit-story.txt +1 -0
- package/_Sprintpilot/modules/git/templates/pr-body.md +20 -0
- package/_Sprintpilot/modules/ma/config.yaml +9 -0
- package/_Sprintpilot/scripts/create-pr.js +284 -0
- package/_Sprintpilot/scripts/detect-platform.js +64 -0
- package/_Sprintpilot/scripts/health-check.js +98 -0
- package/_Sprintpilot/scripts/lint-changed.js +249 -0
- package/_Sprintpilot/scripts/lock.js +195 -0
- package/_Sprintpilot/scripts/sanitize-branch.js +107 -0
- package/_Sprintpilot/scripts/stage-and-commit.js +190 -0
- package/_Sprintpilot/scripts/sync-status.js +141 -0
- package/_Sprintpilot/skills/sprint-autopilot-off/SKILL.md +6 -0
- package/_Sprintpilot/skills/sprint-autopilot-off/workflow.md +154 -0
- package/_Sprintpilot/skills/sprint-autopilot-on/SKILL.md +6 -0
- package/_Sprintpilot/skills/sprint-autopilot-on/workflow.md +1119 -0
- package/_Sprintpilot/skills/sprintpilot-assess/SKILL.md +6 -0
- package/_Sprintpilot/skills/sprintpilot-assess/agents/debt-classifier.md +64 -0
- package/_Sprintpilot/skills/sprintpilot-assess/agents/dependency-auditor.md +57 -0
- package/_Sprintpilot/skills/sprintpilot-assess/agents/migration-analyzer.md +62 -0
- package/_Sprintpilot/skills/sprintpilot-assess/workflow.md +114 -0
- package/_Sprintpilot/skills/sprintpilot-code-review/SKILL.md +6 -0
- package/_Sprintpilot/skills/sprintpilot-code-review/agents/acceptance-auditor.md +51 -0
- package/_Sprintpilot/skills/sprintpilot-code-review/agents/blind-hunter.md +39 -0
- package/_Sprintpilot/skills/sprintpilot-code-review/agents/edge-case-hunter.md +46 -0
- package/_Sprintpilot/skills/sprintpilot-code-review/workflow.md +111 -0
- package/_Sprintpilot/skills/sprintpilot-codebase-map/SKILL.md +6 -0
- package/_Sprintpilot/skills/sprintpilot-codebase-map/agents/architecture-mapper.md +129 -0
- package/_Sprintpilot/skills/sprintpilot-codebase-map/agents/concerns-hunter.md +135 -0
- package/_Sprintpilot/skills/sprintpilot-codebase-map/agents/integration-mapper.md +138 -0
- package/_Sprintpilot/skills/sprintpilot-codebase-map/agents/quality-assessor.md +143 -0
- package/_Sprintpilot/skills/sprintpilot-codebase-map/agents/stack-analyzer.md +133 -0
- package/_Sprintpilot/skills/sprintpilot-codebase-map/workflow.md +120 -0
- package/_Sprintpilot/skills/sprintpilot-migrate/SKILL.md +6 -0
- package/_Sprintpilot/skills/sprintpilot-migrate/agents/dependency-analyzer.md +51 -0
- package/_Sprintpilot/skills/sprintpilot-migrate/agents/risk-assessor.md +55 -0
- package/_Sprintpilot/skills/sprintpilot-migrate/agents/stack-mapper.md +49 -0
- package/_Sprintpilot/skills/sprintpilot-migrate/agents/test-parity-analyzer.md +49 -0
- package/_Sprintpilot/skills/sprintpilot-migrate/resources/coexistence-patterns.md +59 -0
- package/_Sprintpilot/skills/sprintpilot-migrate/resources/strategies.md +43 -0
- package/_Sprintpilot/skills/sprintpilot-migrate/templates/component-card.md +11 -0
- package/_Sprintpilot/skills/sprintpilot-migrate/templates/migration-epics.md +35 -0
- package/_Sprintpilot/skills/sprintpilot-migrate/templates/migration-plan.md +66 -0
- package/_Sprintpilot/skills/sprintpilot-migrate/workflow.md +235 -0
- package/_Sprintpilot/skills/sprintpilot-party-mode/SKILL.md +6 -0
- package/_Sprintpilot/skills/sprintpilot-party-mode/workflow.md +138 -0
- package/_Sprintpilot/skills/sprintpilot-research/SKILL.md +6 -0
- package/_Sprintpilot/skills/sprintpilot-research/workflow.md +128 -0
- package/_Sprintpilot/skills/sprintpilot-reverse-architect/SKILL.md +6 -0
- package/_Sprintpilot/skills/sprintpilot-reverse-architect/agents/component-mapper.md +53 -0
- package/_Sprintpilot/skills/sprintpilot-reverse-architect/agents/data-flow-tracer.md +54 -0
- package/_Sprintpilot/skills/sprintpilot-reverse-architect/agents/pattern-extractor.md +67 -0
- package/_Sprintpilot/skills/sprintpilot-reverse-architect/workflow.md +119 -0
- package/_Sprintpilot/skills/sprintpilot-update/SKILL.md +6 -0
- package/_Sprintpilot/skills/sprintpilot-update/workflow.md +46 -0
- package/_Sprintpilot/templates/agent-rules.md +43 -0
- package/bin/sprintpilot.js +95 -0
- package/lib/commands/check-update.js +54 -0
- package/lib/commands/install.js +876 -0
- package/lib/commands/uninstall.js +218 -0
- package/lib/core/bmad-config.js +113 -0
- package/lib/core/file-ops.js +90 -0
- package/lib/core/gitignore.js +54 -0
- package/lib/core/markers.js +126 -0
- package/lib/core/tool-registry.js +73 -0
- package/lib/core/update-check.js +39 -0
- package/lib/core/v1-detect.js +86 -0
- package/lib/prompts.js +82 -0
- package/lib/substitute.js +39 -0
- 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 };
|