Skip to content

Commit 5dd0cf3

Browse files
committed
feat: initial project scaffold (#1, #2, #3, #4, #5, #6)
- Project infrastructure: package.json, bunfig.toml, tsconfig.json, knip.json, .oxfmtrc.json, .oxlintrc.json, .gitignore, build.ts, install.sh, entitlements.plist - Core types (SearchResult, EpicMetadata, ChecklistItem, DiffResult, DispatchGroup) - API layer: fetchWithRetry, paginatedFetch, full GitHub REST wrappers - stdin parser: markdown and JSON formats, replay command extraction - Checklist engine: build/parse/diff/apply + BODY_LIMIT=65_000 - HTML-comment metadata: embed/extract/update/strip - Ownership resolver chain: teams → CODEOWNERS → JSON mapping → fallback - Issue deduplication: search-based pre-flight check - Output formatters: epic body, sub-issue body, plan table, overflow splitting - $EDITOR helper (respects $VISUAL / $EDITOR / vi) - Commands: issue create, issue refresh, issue dispatch - Main entry point: Commander 14, --non-interactive, --dry-run - Self-upgrade command using GitHub Releases - CI/CD workflows: ci.yaml, cd.yaml, docs.yml - VitePress documentation site - Copilot instruction files (.github/instructions/) - Community files: AGENTS.md, CONTRIBUTING.md, LICENSE.md, etc. - 52 unit tests, all passing - oxlint: 0 errors, oxfmt: clean, knip: clean
0 parents  commit 5dd0cf3

54 files changed

Lines changed: 5575 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
applyTo: "src/api/**"
3+
---
4+
5+
# GitHub API layer
6+
7+
## Files
8+
9+
- `src/api/api-utils.ts``fetchWithRetry`, `paginatedFetch`, `formatRetryWait`
10+
- `src/api/github-api.ts` — typed wrappers for all GitHub REST endpoints used
11+
12+
## Rules
13+
14+
- **Always** use `fetchWithRetry` — never call `fetch(...)` directly in API functions.
15+
- **Always** use `paginatedFetch` for endpoints that are paginated (labels, teams, issues search).
16+
- Errors must be thrown as plain `Error` using `throwApiError`; preserve HTTP status in the message.
17+
- Set `X-GitHub-Api-Version: 2022-11-28` on every request.
18+
- Authentication: `Authorization: Bearer ${token}`.
19+
- `createSubIssueLink` silently ignores 404/422 — the sub-issues API is not universally available.
20+
21+
## Adding a new endpoint
22+
23+
1. Add a typed function in `github-api.ts`.
24+
2. Use `fetchWithRetry` for single-page responses, `paginatedFetch` for lists.
25+
3. Add the corresponding type to `src/types.ts` if a new shape is returned.
26+
4. Write a test that mocks `globalThis.fetch`.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
applyTo: "src/commands/**"
3+
---
4+
5+
# Command implementations
6+
7+
## Files
8+
9+
| File | Commander subcommand |
10+
| -------------------------- | -------------------- |
11+
| `src/commands/create.ts` | `issue create` |
12+
| `src/commands/refresh.ts` | `issue refresh` |
13+
| `src/commands/dispatch.ts` | `issue dispatch` |
14+
15+
## Conventions
16+
17+
- Each command exports an `*Action(options)` async function called by the Commander `.action()` handler.
18+
- `options` include a `token: string` injected from `getToken()` in the main entry point.
19+
- Use `@clack/prompts` for all interactive UX: `p.intro`, `p.spinner`, `p.confirm`, `p.text`, `p.multiselect`, `p.outro`, `p.cancel`.
20+
- `p.cancel()` then `process.exit(0)` for user-initiated aborts (not errors).
21+
- `process.exit(1)` for errors; write the message to `process.stderr` with `pc.red("✘")` prefix.
22+
- `--non-interactive` flag skips all prompts; required flags must then be provided via CLI.
23+
- `--dry-run` runs the full logic but skips mutating API calls; prints the result body.
24+
25+
## Dispatch mode
26+
27+
- `--mode plan` (default): show the plan table, exit 0.
28+
- `--mode apply`: run the plan after a confirmation prompt (skipped with `--non-interactive`).
29+
30+
## Error propagation
31+
32+
All command `*Action` functions throw on API errors. The entrypoint catches them via `.catch(exitOnError)`.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
applyTo: "src/core/**"
3+
---
4+
5+
# Core business logic
6+
7+
## Files
8+
9+
| File | Responsibility |
10+
| ----------------------- | ------------------------------------------------------------------ |
11+
| `src/core/checklist.ts` | Build/parse/diff GitHub-markdown checklists; `BODY_LIMIT = 65_000` |
12+
| `src/core/metadata.ts` | Embed/extract/update HTML-comment metadata blocks |
13+
| `src/core/ownership.ts` | Resolver chain: teams → CODEOWNERS → JSON mapping → fallback `[]` |
14+
| `src/core/dedup.ts` | Search for pre-existing dispatch issues to avoid duplicates |
15+
16+
## Metadata format
17+
18+
```
19+
<!-- github-issue-ops:metadata
20+
{"version":1,"replayCommand":"...","createdAt":"...","config":{...}}
21+
-->
22+
```
23+
24+
- `version: 1` — increment on breaking changes; add a migration in `extractMetadata`.
25+
- Never display this block to end users — use `stripMetadata` before showing body text.
26+
27+
## Checklist format
28+
29+
Each item: `- [ ] \`owner/repo\`\`path/to/file.ts:42\` — matched text`
30+
31+
Key function for the item identity key (used in diff): `repo:path:line:text`.
32+
33+
## Body limit
34+
35+
`BODY_LIMIT = 65_000` chars (GitHub's actual limit is 65 536). The `checkBodyLength` function returns `{ok, length, limit}`. When `!ok`:
36+
37+
- `issue create` → hard error; user must reduce input or edit body
38+
- `issue refresh` → truncate + `addComment` with overflow
39+
40+
## Ownership resolver chain
41+
42+
Each resolver is `async (ctx: OwnershipContext) => string[] | null`.
43+
The chain stops on the first non-null result. The fallback resolver always returns `[]`.
44+
45+
To add a resolver, implement `OwnershipResolver` and insert it before `fallbackResolver` in `defaultResolverChain`.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
applyTo: "**"
3+
---
4+
5+
# github-issue-ops — project overview
6+
7+
`github-issue-ops` is a Bun-based CLI (`bun run build → dist/github-issue-ops`) for industrializing GitHub issue campaigns from `github-code-search` result sets.
8+
9+
## Three subcommands
10+
11+
| Command | Purpose |
12+
| ---------------- | -------------------------------------------------------------------------------------- |
13+
| `issue create` | Reads stdin (markdown or JSON), creates an EPIC issue with a full checklist + metadata |
14+
| `issue refresh` | Reads new stdin results, diffs against the existing EPIC checklist, updates in-place |
15+
| `issue dispatch` | Reads EPIC checklist, creates one sub-issue per repo |
16+
17+
## Key architectural rules
18+
19+
1. **Runtime is Bun only** — no Node.js, no npm. Use `bun install`, `bun test`, `bun run build.ts`.
20+
2. **Single-file executable**`Bun.build({ compile: true })` in `build.ts`.
21+
3. **Metadata in HTML comments**`<!-- github-issue-ops:metadata\n{JSON}\n-->` at end of issue body.
22+
4. **Body limit = 65 000** — hard limit in `src/core/checklist.ts#BODY_LIMIT`. Overflow → `addComment`.
23+
5. **No direct `fetch`** — always use `fetchWithRetry` from `src/api/api-utils.ts`.
24+
6. **Tests co-located**`*.test.ts` next to the file under test. Run with `bun test`.
25+
7. **Signed commits**`git commit -S -m "..."`. Never push via MCP tools.
26+
8. **Linter = oxlint**, **formatter = oxfmt** — run `bun run lint` and `bun run format:check`.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
---
2+
applyTo: "**/*.test.ts"
3+
---
4+
5+
# Testing conventions
6+
7+
## Runner
8+
9+
Bun built-in test runner — `bun test`. No Jest, no Vitest.
10+
11+
```bash
12+
bun test # run all tests
13+
bun test --coverage # with coverage report
14+
bun test src/core/ # specific directory
15+
```
16+
17+
## Setup
18+
19+
`bunfig.toml` preloads `./src/test-setup.ts` before each test file:
20+
21+
```toml
22+
[test]
23+
preload = ["./src/test-setup.ts"]
24+
```
25+
26+
`test-setup.ts` only sets `process.env.FORCE_COLOR = "1"` — keep it minimal.
27+
28+
## Mocking fetch
29+
30+
Mock `globalThis.fetch` directly:
31+
32+
```typescript
33+
import { afterEach } from "bun:test";
34+
const originalFetch = globalThis.fetch;
35+
afterEach(() => {
36+
globalThis.fetch = originalFetch;
37+
});
38+
39+
globalThis.fetch = () =>
40+
Promise.resolve(new Response(JSON.stringify({ items: [] }), { status: 200 }));
41+
```
42+
43+
## Coverage thresholds (bunfig.toml)
44+
45+
```toml
46+
[test.coverage.threshold]
47+
line = 0.75
48+
function = 0.80
49+
statement = 0.80
50+
```
51+
52+
## File placement
53+
54+
Tests are co-located with source: `src/core/checklist.ts``src/core/checklist.test.ts`.
55+
56+
## What to test
57+
58+
- Pure functions (checklist, metadata, stdin parsing, dedup): full unit tests with various edge cases.
59+
- API wrappers: mock `globalThis.fetch`; test success, 404, 503 retry paths.
60+
- Ownership resolvers: test the resolver chain stitching; individual resolvers via mocked fetch.
61+
- Commands: integration-style tests are optional; prefer testing the underlying helpers.

.github/workflows/cd.yaml

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
name: CD
2+
3+
on:
4+
push:
5+
tags:
6+
- "v*.*.*"
7+
8+
permissions:
9+
contents: write
10+
11+
jobs:
12+
release:
13+
name: Build & Release
14+
runs-on: ubuntu-latest
15+
16+
steps:
17+
- uses: actions/checkout@v4
18+
with:
19+
fetch-depth: 0
20+
21+
- uses: oven-sh/setup-bun@v2
22+
with:
23+
bun-version: latest
24+
25+
- name: Install dependencies
26+
run: bun install --frozen-lockfile
27+
28+
- name: Build all targets
29+
run: |
30+
bun run build --target bun-linux-x64
31+
bun run build --target bun-linux-arm64
32+
bun run build --target bun-darwin-x64
33+
bun run build --target bun-darwin-arm64
34+
bun run build --target bun-windows-x64
35+
bun run build --target bun-linux-x64-musl
36+
37+
- name: Package artifacts
38+
run: |
39+
mkdir -p release
40+
for f in dist/github-issue-ops-*; do
41+
target=$(basename "$f" | sed 's/^github-issue-ops-//')
42+
if [[ "$f" == *.exe ]]; then
43+
zip "release/github-issue-ops-${target}.zip" "$f"
44+
else
45+
tar -czf "release/github-issue-ops-${target}.tar.gz" -C dist "$(basename $f)"
46+
fi
47+
done
48+
49+
- name: Create GitHub Release
50+
uses: softprops/action-gh-release@v2
51+
with:
52+
files: release/*
53+
generate_release_notes: true
54+
draft: false
55+
prerelease: ${{ contains(github.ref_name, '-') }}
56+
env:
57+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.github/workflows/ci.yaml

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
test:
14+
name: Test & Lint
15+
runs-on: ubuntu-latest
16+
17+
steps:
18+
- uses: actions/checkout@v4
19+
20+
- uses: oven-sh/setup-bun@v2
21+
with:
22+
bun-version: latest
23+
24+
- name: Install dependencies
25+
run: bun install --frozen-lockfile
26+
27+
- name: Format check
28+
run: bun run format:check
29+
30+
- name: Lint
31+
run: bun run lint
32+
33+
- name: Dead code check
34+
run: bun run knip
35+
36+
- name: Run tests
37+
run: bun test --coverage
38+
39+
- name: Build (smoke test)
40+
run: bun run build --target bun
41+
42+
build-matrix:
43+
name: Cross-compile (${{ matrix.target }})
44+
runs-on: ubuntu-latest
45+
needs: test
46+
strategy:
47+
fail-fast: false
48+
matrix:
49+
target:
50+
- bun-linux-x64
51+
- bun-linux-arm64
52+
- bun-darwin-x64
53+
- bun-darwin-arm64
54+
- bun-windows-x64
55+
56+
steps:
57+
- uses: actions/checkout@v4
58+
59+
- uses: oven-sh/setup-bun@v2
60+
with:
61+
bun-version: latest
62+
63+
- name: Install dependencies
64+
run: bun install --frozen-lockfile
65+
66+
- name: Build for ${{ matrix.target }}
67+
run: bun run build --target ${{ matrix.target }}
68+
69+
- name: Upload artifact
70+
uses: actions/upload-artifact@v4
71+
with:
72+
name: github-issue-ops-${{ matrix.target }}
73+
path: dist/

.github/workflows/docs.yml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
name: Deploy Docs
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths:
7+
- "docs/**"
8+
- ".vitepress/**"
9+
- "package.json"
10+
11+
permissions:
12+
contents: read
13+
pages: write
14+
id-token: write
15+
16+
concurrency:
17+
group: pages
18+
cancel-in-progress: false
19+
20+
jobs:
21+
build:
22+
name: Build VitePress
23+
runs-on: ubuntu-latest
24+
25+
steps:
26+
- uses: actions/checkout@v4
27+
28+
- uses: oven-sh/setup-bun@v2
29+
with:
30+
bun-version: latest
31+
32+
- name: Install dependencies
33+
run: bun install --frozen-lockfile
34+
35+
- name: Build docs
36+
run: bun run docs:build
37+
38+
- name: Upload artifact
39+
uses: actions/upload-pages-artifact@v3
40+
with:
41+
path: docs/.vitepress/dist
42+
43+
deploy:
44+
name: Deploy to GitHub Pages
45+
needs: build
46+
runs-on: ubuntu-latest
47+
environment:
48+
name: github-pages
49+
url: ${{ steps.deployment.outputs.page_url }}
50+
51+
steps:
52+
- name: Deploy to GitHub Pages
53+
id: deployment
54+
uses: actions/deploy-pages@v4

.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
node_modules/
2+
dist/
3+
coverage/
4+
.env
5+
*.local
6+
7+
# VitePress
8+
docs/.vitepress/cache
9+
docs/.vitepress/dist

.oxfmtrc.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"$schema": "./node_modules/oxfmt/configuration_schema.json",
3+
"ignorePatterns": []
4+
}

0 commit comments

Comments
 (0)