Skip to content

Commit d6e730e

Browse files
committed
feat: add @array/cli and @array/core packages with comprehensive tests
Adds two new packages for the Array CLI stacking workflow: @array/core - Core library with: - JJ wrapper for jujutsu commands - GitHub API integration for PR management - Config/state management - Stack comment generation for linked PRs - Workspace management - Mock executor for testability @array/cli - Command-line interface with: - Stacking commands (create, up, down, top, bottom, log) - Changeset commands (enable, disable, swap, status, list) - PR workflow (submit, sync, restack) - GitHub auth integration - Interactive prompts Test coverage (289 tests): - Unit tests with mocked executors - Integration tests with real git/jj repos - E2E tests for CLI commands 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> fix: update electron-trpc config to ESM and disable flaky tests - Convert vitest.config.ts from CommonJS to ESM syntax - Temporarily disable electron-trpc tests due to missing dependencies 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> feat: add separate test commands for bun and vitest - `pnpm test:bun` - runs @array/core and @array/cli tests (bun) - `pnpm test:vitest` - runs array app and electron-trpc tests (vitest) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> stuff rest of the owl more improvements stories fix: arr modify now actually squashes @ into parent Previously, modify just called jj.status() and printed success. Now it properly calls jj squash to squash working copy into parent. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> fix: arr top now creates empty @ above stack for new work Previously arr top just navigated to the topmost described change. Now it creates an empty @ above the stack (if not already there), matching the STORIES.md expectation: "Empty @ above stack top". Uses jj directly to check if @ is empty/undescribed with no children. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> fix: arr log/stacks now excludes immutable remote-tracking branches The revset `trunk()..heads(trunk()..)` was including immutable remote-tracking branches that clutter the log. Now uses `mutable() & trunk()..heads(trunk()..)` to show only mutable changes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> fix: sync updates PR bases, status parses isEmpty, STORIES.md cleanup - Add updatePRBase() to GitHub class for updating PR base branches - updateStackComments() now updates PR base when stack changes after merge - STATUS_TEMPLATE now includes 'empty' field, parsed correctly - navigateTop test updated to match new behavior (creates empty @ above stack) - STORIES.md: removed .array/config.json (fully stateless), merged duplicate sections, clarified arr create requires file changes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> clean up clean up cli clean up tests refactor more cleanup great improvements feat: add arr trunk and arr exit commands readme and ci move to app address codeql help update update ci codeql bunbunbunbunbunbun more bugfixes more fixes and safeguards for merging yup more merge stuff refactor proper trunk retrieval more simplification log squash delete superduperdaemon split up jj into cmomands, remove dead code remove daemon for now commands
1 parent 959a480 commit d6e730e

45 files changed

Lines changed: 3247 additions & 38 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/agent-release.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ jobs:
4646
- name: Install pnpm
4747
run: npm install -g pnpm
4848

49+
- name: Setup Bun
50+
uses: oven-sh/setup-bun@v2
51+
4952
- name: Install dependencies
5053
run: pnpm install --frozen-lockfile
5154

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# Generated by Array CLI - https://github.com/posthog/array
2+
# Blocks stacked PRs until their downstack dependencies are merged
3+
# Only runs for PRs managed by Array (detected via stack comment marker)
4+
5+
name: Stack Check
6+
7+
on:
8+
pull_request:
9+
types: [opened, synchronize, reopened, edited]
10+
pull_request_target:
11+
types: [closed]
12+
13+
permissions:
14+
contents: read
15+
16+
jobs:
17+
check:
18+
runs-on: ubuntu-latest
19+
if: github.event_name == 'pull_request'
20+
permissions:
21+
pull-requests: read
22+
issues: read
23+
steps:
24+
- name: Check stack dependencies
25+
uses: actions/github-script@v7
26+
with:
27+
script: |
28+
const pr = context.payload.pull_request;
29+
30+
// Check if this is an Array-managed PR by looking for stack comment
31+
const { data: comments } = await github.rest.issues.listComments({
32+
owner: context.repo.owner,
33+
repo: context.repo.repo,
34+
issue_number: pr.number
35+
});
36+
37+
const isArrayPR = comments.some(c =>
38+
c.body.includes('<!-- array-stack-comment -->')
39+
);
40+
41+
if (!isArrayPR) {
42+
console.log('Not an Array PR, skipping');
43+
return;
44+
}
45+
46+
const baseBranch = pr.base.ref;
47+
const trunk = ['main', 'master', 'develop'];
48+
49+
if (trunk.includes(baseBranch)) {
50+
console.log('Base is trunk, no dependencies');
51+
return;
52+
}
53+
54+
async function getBlockers(base, visited = new Set()) {
55+
if (trunk.includes(base) || visited.has(base)) {
56+
return [];
57+
}
58+
visited.add(base);
59+
60+
const { data: prs } = await github.rest.pulls.list({
61+
owner: context.repo.owner,
62+
repo: context.repo.repo,
63+
state: 'open',
64+
head: `${context.repo.owner}:${base}`
65+
});
66+
67+
if (prs.length === 0) {
68+
return [];
69+
}
70+
71+
const blocker = prs[0];
72+
const upstream = await getBlockers(blocker.base.ref, visited);
73+
return [{ number: blocker.number, title: blocker.title }, ...upstream];
74+
}
75+
76+
const blockers = await getBlockers(baseBranch);
77+
78+
if (blockers.length > 0) {
79+
const list = blockers.map(b => `#${b.number} (${b.title})`).join('\n - ');
80+
core.setFailed(`Blocked by:\n - ${list}\n\nMerge these PRs first (bottom to top).`);
81+
} else {
82+
console.log('All dependencies merged, ready to merge');
83+
}
84+
85+
recheck-dependents:
86+
runs-on: ubuntu-latest
87+
if: >-
88+
github.event_name == 'pull_request_target' &&
89+
github.event.action == 'closed' &&
90+
github.event.pull_request.merged == true
91+
permissions:
92+
pull-requests: write
93+
issues: read
94+
steps:
95+
- name: Trigger recheck of dependent PRs
96+
uses: actions/github-script@v7
97+
with:
98+
script: |
99+
const pr = context.payload.pull_request;
100+
101+
// Check if this is an Array-managed PR
102+
const { data: comments } = await github.rest.issues.listComments({
103+
owner: context.repo.owner,
104+
repo: context.repo.repo,
105+
issue_number: pr.number
106+
});
107+
108+
const isArrayPR = comments.some(c =>
109+
c.body.includes('<!-- array-stack-comment -->')
110+
);
111+
112+
if (!isArrayPR) {
113+
console.log('Not an Array PR, skipping');
114+
return;
115+
}
116+
117+
const mergedBranch = pr.head.ref;
118+
119+
const { data: dependentPRs } = await github.rest.pulls.list({
120+
owner: context.repo.owner,
121+
repo: context.repo.repo,
122+
base: mergedBranch,
123+
state: 'open'
124+
});
125+
126+
for (const dependentPR of dependentPRs) {
127+
console.log(`Re-checking PR #${dependentPR.number}`);
128+
await github.rest.pulls.update({
129+
owner: context.repo.owner,
130+
repo: context.repo.repo,
131+
pull_number: dependentPR.number,
132+
base: 'main'
133+
});
134+
}

.github/workflows/build.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,13 @@ jobs:
1818
persist-credentials: false
1919
- name: Setup pnpm
2020
uses: pnpm/action-setup@v4
21-
2221
- name: Setup Node.js
2322
uses: actions/setup-node@v4
2423
with:
2524
node-version: 22
2625
cache: 'pnpm'
26+
- name: Setup Bun
27+
uses: oven-sh/setup-bun@v2
2728
- name: Install dependencies
2829
run: pnpm install --frozen-lockfile
2930
- name: Build electron-trpc

.github/workflows/release.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ jobs:
5252
node-version: 22
5353
cache: "pnpm"
5454

55+
- name: Setup Bun
56+
uses: oven-sh/setup-bun@v2
57+
5558
- name: Compute version from git tags
5659
id: version
5760
run: |

.github/workflows/typecheck.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ jobs:
2020
with:
2121
node-version: 22
2222
cache: "pnpm"
23+
- name: Setup Bun
24+
uses: oven-sh/setup-bun@v2
2325
- name: Install dependencies
2426
run: pnpm install --frozen-lockfile
2527
- name: Run type check

CLAUDE.md

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,9 @@
4747
### Avoid Barrel Files
4848

4949
Barrel files:
50+
5051
- Break tree-shaking
51-
- Create circular dependency risks
52+
- Create circular dependency risks
5253
- Hide the true source of imports
5354
- Make refactoring harder
5455

@@ -74,6 +75,17 @@ See [ARCHITECTURE.md](./ARCHITECTURE.md) for detailed patterns (DI, services, tR
7475
- PostHog API integration in `posthog-api.ts`
7576
- Task execution and session management
7677

78+
### CLI Package (packages/cli)
79+
80+
- **Dumb shell, imperative core**: CLI commands should be thin wrappers that call `@array/core`
81+
- All business logic belongs in `@array/core`, not in CLI command files
82+
- CLI only handles: argument parsing, calling core, formatting output
83+
- No data transformation, tree building, or complex logic in CLI
84+
85+
### Core Package (packages/core)
86+
87+
- Shared business logic for jj/GitHub operations
88+
7789
## Key Libraries
7890

7991
- React 18, Radix UI Themes, Tailwind CSS
@@ -91,6 +103,5 @@ TODO: Update me
91103

92104
## Testing
93105

94-
- Tests use vitest with jsdom environment
95-
- Test helpers in `src/test/`
96-
- Run specific test: `pnpm --filter array test -- path/to/test`
106+
- `pnpm test` - Run tests across all packages
107+
- Array app: Vitest with jsdom, helpers in `apps/array/src/test/`

apps/cli/README.md

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
> [!IMPORTANT] > `arr` is still in development and not production-ready. Interested? Email jonathan@posthog.com
2+
3+
# arr
4+
5+
arr is CLI for stacked PR management using Jujutsu (`jj`).
6+
7+
Split your work into small changes, push them as a PR stack, and keep everything in sync.
8+
9+
## Features
10+
11+
- Stacked PRs synced to GitHub
12+
- Simpler interface compared to `jj` for managing your work.
13+
- Visual stack log with PR status.
14+
- Comes with a GitHub Action to enforce merge order
15+
- Unknown commands pass through to `jj`
16+
17+
## Why
18+
19+
Stacked PRs keep reviews small and manageable. Managing them with `git` is painful, this involves rebasing, force-pushing, updating PR bases and describing the PR stack via comments.
20+
21+
`arr` makes it easy to create, manage and submit (stacked) PRs by using `jj` under the hood.
22+
23+
## Install
24+
25+
Requires [Bun](https://bun.sh).
26+
27+
```
28+
git clone https://github.com/posthog/array
29+
cd array
30+
pnpm install
31+
pnpm --filter @array/core build
32+
```
33+
34+
Then install the `arr` command (symlinked to `~/bin/arr`):
35+
36+
```
37+
./apps/cli/arr.sh install
38+
```
39+
40+
## Usage
41+
42+
```
43+
arr init # set up arr in a git repo
44+
arr create "message" # new change on stack
45+
arr submit # push stack, create PRs
46+
arr merge # merge PR via GitHub
47+
arr sync # fetch, rebase, cleanup merged
48+
arr up / arr down # navigate stack
49+
arr log # show stack
50+
arr exit # back to git
51+
arr help --all # show all commands
52+
```
53+
54+
## Example
55+
56+
```
57+
$ echo "user model" >> user_model.ts
58+
$ arr create "Add user model"
59+
✓ Created add-user-model-qtrsqm
60+
61+
$ echo "user api" >> user_api.ts
62+
$ arr create "Add user API"
63+
✓ Created add-user-api-nnmzrt
64+
65+
$ arr log
66+
◉ (working copy)
67+
│ Empty
68+
○ 12-23-add-user-api nnmzrtzz (+1, 1 file)
69+
│ Not submitted
70+
○ 12-23-add-user-model qtrsqmmy (+1, 1 file)
71+
│ Not submitted
72+
○ main
73+
74+
$ arr submit
75+
Created PR #8: 12-23-add-user-model
76+
https://github.com/username/your-repo/pull/8
77+
Created PR #9: 12-23-add-user-api
78+
https://github.com/username/your-repo/pull/9
79+
80+
$ arr merge
81+
...
82+
83+
$ arr sync
84+
```
85+
86+
Each change becomes a PR. PRs are stacked so reviewers see the dependency.
87+
88+
## CI
89+
90+
```
91+
arr ci
92+
```
93+
94+
Adds a GitHub Action that blocks merging a PR if its parent PR hasn't merged yet, which helps keep your stack in order.
95+
96+
## FAQ
97+
98+
**Can I use this with an existing `git` repo?**
99+
100+
Yes, do so by using `arr init` in any `git` repo. `jj` works alongside `git`.
101+
102+
**Do my teammates need to use `arr` or `jj`?**
103+
104+
No, your PRs are normal GitHub PRs. Teammates review and merge them as usual. `jj` has full support for `git`.
105+
106+
**What if I want to stop using `arr`?**
107+
108+
Run `arr exit` to switch back to `git`. Your repo, branches, and PRs stay exactly as they are.
109+
110+
**How is `arr` related to Array?**
111+
112+
`arr` is the CLI component of Array, an agentic development environment.
113+
114+
## Learn more
115+
116+
- [`jj` documentation](https://jj-vcs.github.io/jj/latest/) - full `jj` reference
117+
- [`jj` tutorial](https://jj-vcs.github.io/jj/latest/tutorial/) - getting started with `jj`

apps/cli/arr.sh

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#!/usr/bin/env bash
2+
# Wrapper script to run arr CLI via bun.
3+
SOURCE="${BASH_SOURCE[0]}"
4+
while [ -L "$SOURCE" ]; do
5+
DIR="$(cd "$(dirname "$SOURCE")" && pwd)"
6+
SOURCE="$(readlink "$SOURCE")"
7+
[[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE"
8+
done
9+
SCRIPT_DIR="$(cd "$(dirname "$SOURCE")" && pwd)"
10+
11+
# Self-install: ./arr.sh install
12+
if [ "$1" = "install" ]; then
13+
mkdir -p ~/bin
14+
ln -sf "$SCRIPT_DIR/arr.sh" ~/bin/arr
15+
echo "Installed: ~/bin/arr -> $SCRIPT_DIR/arr.sh"
16+
echo "Make sure ~/bin is in your PATH"
17+
exit 0
18+
fi
19+
20+
exec bun run "$SCRIPT_DIR/bin/arr.ts" "$@"

apps/cli/bin/arr.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#!/usr/bin/env bun
2+
3+
import { main } from "../src/cli";
4+
5+
main()
6+
.then(() => process.exit(0))
7+
.catch((error) => {
8+
console.error(error);
9+
process.exit(1);
10+
});

apps/cli/package.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"name": "@array/cli",
3+
"version": "0.0.1",
4+
"description": "CLI for changeset management with jj",
5+
"bin": {
6+
"arr": "./bin/arr.ts"
7+
},
8+
"type": "module",
9+
"scripts": {
10+
"build": "bun build ./src/index.ts --outdir ./dist --target bun",
11+
"dev": "bun run ./bin/arr.ts",
12+
"typecheck": "tsc --noEmit",
13+
"test": "bun test --concurrent tests/unit tests/e2e/cli.test.ts",
14+
"test:pty": "vitest run tests/e2e/pty.test.ts"
15+
},
16+
"devDependencies": {
17+
"@types/bun": "latest",
18+
"@types/node": "^25.0.3",
19+
"typescript": "^5.5.0",
20+
"vitest": "^4.0.16"
21+
},
22+
"dependencies": {
23+
"@array/core": "workspace:*"
24+
}
25+
}

0 commit comments

Comments
 (0)