Skip to content

Commit 640704e

Browse files
committed
great improvements
1 parent 465e296 commit 640704e

Some content is hidden

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

56 files changed

+4936
-2023
lines changed

CLAUDE.md

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,14 @@ See [ARCHITECTURE.md](./ARCHITECTURE.md) for detailed patterns (DI, services, tR
7979
- CLI only handles: argument parsing, calling core, formatting output
8080
- No data transformation, tree building, or complex logic in CLI
8181

82+
### Core Package (packages/core)
83+
84+
- Shared business logic for jj/GitHub operations
85+
- `pnpm --filter @array/core test` - Run bun tests
86+
- `pnpm --filter @array/core typecheck` - Type check
87+
- Uses Bun's test runner with `mock()` for test doubles
88+
- Test helpers in `tests/helpers/` (MockGitHub, TestRepo, withTestRepo)
89+
8290
## Key Libraries
8391

8492
- React 18, Radix UI Themes, Tailwind CSS
@@ -96,6 +104,31 @@ TODO: Update me
96104

97105
## Testing
98106

99-
- Tests use vitest with jsdom environment
100-
- Test helpers in `src/test/`
101-
- Run specific test: `pnpm --filter array test -- path/to/test`
107+
- `pnpm test` - Run tests across all packages
108+
- Array app: Vitest with jsdom, helpers in `apps/array/src/test/`
109+
- Core package: Bun test runner, helpers in `packages/core/tests/helpers/`
110+
111+
### Running Specific Tests (Bun)
112+
113+
```bash
114+
# Run all tests in a directory
115+
bun test tests/integration
116+
117+
# Run a specific test file
118+
bun test tests/integration/sync/sync.test.ts
119+
120+
# Run tests matching a pattern (--test-name-pattern / -t)
121+
bun test -t "sync fetches"
122+
123+
# Run with concurrency (default: 20)
124+
bun test --concurrent
125+
126+
# Limit concurrent tests
127+
bun test --concurrent --max-concurrency 10
128+
129+
# Increase timeout (default: 5000ms)
130+
bun test --timeout 15000
131+
132+
# Watch mode
133+
bun test --watch
134+
```

packages/cli/src/commands/bottom.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ import { printNav } from "../utils/output";
22
import { createJJ, unwrap } from "../utils/run";
33

44
export async function bottom(): Promise<void> {
5-
printNav("Jumped to bottom", unwrap(await createJJ().navigateBottom()));
5+
printNav("down", unwrap(await createJJ().navigateBottom()));
66
}

packages/cli/src/commands/down.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
1-
import { printNav } from "../utils/output";
1+
import { cyan, dim, formatChangeId, green } from "../utils/output";
22
import { createJJ, unwrap } from "../utils/run";
33

44
export async function down(): Promise<void> {
5-
printNav("Moved down", unwrap(await createJJ().navigateDown()));
5+
const result = unwrap(await createJJ().navigateDown());
6+
7+
if (result.createdOnTrunk) {
8+
console.log(`${green("◉")} Started fresh on ${cyan("main")}`);
9+
console.log(dim(` Run ${cyan("arr top")} to go back to your stack`));
10+
} else {
11+
const shortId = formatChangeId(
12+
result.changeId.slice(0, 8),
13+
result.changeIdPrefix,
14+
);
15+
const desc = result.description || dim("(empty)");
16+
console.log(`↓ ${green(desc)} ${shortId}`);
17+
}
618
}

packages/cli/src/commands/log.ts

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
import { datePrefixedLabel, formatRelativeTime, GitHub } from "@array/core";
2-
import { cyan, dim, green, magenta, red, yellow } from "../utils/output";
2+
import {
3+
cyan,
4+
dim,
5+
formatChangeId,
6+
formatCommitId,
7+
green,
8+
magenta,
9+
red,
10+
yellow,
11+
} from "../utils/output";
312
import { createJJ, unwrap } from "../utils/run";
413

514
interface PRInfo {
@@ -12,7 +21,9 @@ export async function log(): Promise<void> {
1221
const jj = createJJ();
1322
const github = new GitHub(process.cwd());
1423

15-
const { entries, trunk, isOnTrunk } = unwrap(await jj.getLog());
24+
const { entries, trunk, isOnTrunk, hasEmptyWorkingCopy } = unwrap(
25+
await jj.getLog(),
26+
);
1627

1728
if (entries.length === 0 && isOnTrunk) {
1829
console.log(dim("No changes in stack"));
@@ -49,20 +60,47 @@ export async function log(): Promise<void> {
4960

5061
console.log();
5162

63+
// Show empty working copy if present
64+
if (hasEmptyWorkingCopy) {
65+
console.log(`${green("◉")} ${dim("(working copy)")}`);
66+
console.log(`│ ${dim("Empty")}`);
67+
console.log(`│`);
68+
console.log(`│ ${dim("Edit files, then:")}`);
69+
console.log(
70+
`│ ${cyan("arr create")} ${dim('"message"')} ${dim("to save as new change")}`,
71+
);
72+
console.log(
73+
`│ ${cyan("arr modify")} ${dim("to update the change below")}`,
74+
);
75+
console.log(`│`);
76+
}
77+
78+
let modifiedCount = 0;
79+
5280
for (let i = 0; i < entries.length; i++) {
53-
const { change, prefix, isCurrent, isLastInStack, stackIndex } = entries[i];
81+
const { change, prefix, isCurrent, isLastInStack, stackIndex, isModified } =
82+
entries[i];
5483
const label = datePrefixedLabel(change.description, change.timestamp);
5584

85+
// Track modified changes for summary
86+
if (isModified) modifiedCount++;
87+
5688
// Determine status badges
5789
const badges: string[] = [];
90+
if (isModified) badges.push(yellow("modified"));
5891
if (change.hasConflicts) badges.push(yellow("conflicts"));
5992

60-
const marker = isCurrent ? green("◉") : "○";
93+
// Don't show current marker on entries when empty WC is shown above
94+
const marker = isCurrent && !hasEmptyWorkingCopy ? green("◉") : "○";
6195
const badgeStr =
6296
badges.length > 0 ? ` ${dim("(")}${badges.join(", ")}${dim(")")}` : "";
6397

64-
// Line 1: marker + branch name + badges
65-
console.log(`${prefix}${marker} ${label}${badgeStr}`);
98+
// Line 1: marker + branch name + change ID + badges
99+
const shortId = formatChangeId(
100+
change.changeId.slice(0, 8),
101+
change.changeIdPrefix,
102+
);
103+
console.log(`${prefix}${marker} ${label} ${shortId}${badgeStr}`);
66104

67105
// Line 2: relative time
68106
console.log(`${prefix}${dim(formatRelativeTime(change.timestamp))}`);
@@ -91,8 +129,12 @@ export async function log(): Promise<void> {
91129

92130
// Line 4: commit hash
93131
console.log(`${prefix}│`);
132+
const shortCommitId = formatCommitId(
133+
change.commitId.slice(0, 7),
134+
change.commitIdPrefix,
135+
);
94136
console.log(
95-
`${prefix}${dim(change.commitId.slice(0, 7))} - ${change.description || dim("(no description)")}`,
137+
`${prefix}${shortCommitId} - ${change.description || dim("(no description)")}`,
96138
);
97139
console.log(`${prefix}│`);
98140

@@ -112,6 +154,15 @@ export async function log(): Promise<void> {
112154
console.log(`○ ${dim(trunk)}`);
113155
}
114156
console.log();
157+
158+
// Show summary/guidance when there are modified changes
159+
if (modifiedCount > 0) {
160+
const changeWord = modifiedCount === 1 ? "change" : "changes";
161+
console.log(
162+
`${modifiedCount} ${changeWord} modified. Run ${cyan("arr submit")} to update PRs.`,
163+
);
164+
console.log();
165+
}
115166
}
116167

117168
async function getRepoSlug(github: GitHub): Promise<string> {
Lines changed: 67 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,45 @@
1-
import { changeLabel } from "@array/core";
2-
import { bold, cyan, dim, green, red, yellow } from "../utils/output";
1+
import { cyan, dim, formatChangeId, green, red, yellow } from "../utils/output";
32
import { createJJ, unwrap } from "../utils/run";
43

54
export async function status(): Promise<void> {
6-
const { workingCopy, modifiedFiles, conflicts } = unwrap(
7-
await createJJ().status(),
5+
const info = unwrap(await createJJ().getStatusInfo());
6+
7+
// Check if on main with no stack above (fresh start)
8+
const isOnMainFresh =
9+
info.isUndescribed &&
10+
info.stackPath.length === 1 &&
11+
info.stackPath[0] === "main";
12+
13+
// Line 1: Current position with change ID (prefix highlighted)
14+
const changeId = formatChangeId(
15+
info.changeId.slice(0, 8),
16+
info.changeIdPrefix,
817
);
18+
if (isOnMainFresh) {
19+
console.log(`${green("◉")} On ${cyan("main")} ${changeId}`);
20+
} else if (info.isUndescribed) {
21+
const label = info.hasChanges ? "(unsaved)" : "(empty)";
22+
console.log(`${green(label)} ${changeId}`);
23+
console.log(dim(` ↳ ${info.stackPath.join(" → ")}`));
24+
} else {
25+
console.log(`${green(info.name)} ${changeId}`);
26+
console.log(dim(` ↳ ${info.stackPath.join(" → ")}`));
27+
}
928

10-
// Show current change
11-
const label = changeLabel(workingCopy.description, workingCopy.changeId);
12-
const desc = workingCopy.description || dim("(no description)");
13-
console.log(`${bold("On:")} ${cyan(label)} ${desc}`);
29+
// Conflicts
30+
if (info.conflicts.length > 0) {
31+
console.log();
32+
console.log(yellow("Conflicts:"));
33+
for (const conflict of info.conflicts) {
34+
console.log(` ${red("C")} ${conflict.path}`);
35+
}
36+
}
1437

15-
// Show working copy changes
16-
if (modifiedFiles.length > 0) {
17-
console.log(`\n${bold("Working copy changes:")}`);
18-
for (const file of modifiedFiles) {
38+
// Modified files
39+
if (info.modifiedFiles.length > 0) {
40+
console.log();
41+
console.log(dim("Modified:"));
42+
for (const file of info.modifiedFiles) {
1943
const color =
2044
file.status === "added"
2145
? green
@@ -26,15 +50,39 @@ export async function status(): Promise<void> {
2650
file.status === "added" ? "A" : file.status === "deleted" ? "D" : "M";
2751
console.log(` ${color(prefix)} ${file.path}`);
2852
}
29-
} else {
30-
console.log(`\n${dim("No changes")}`);
3153
}
3254

33-
// Show conflicts if any
34-
if (conflicts.length > 0) {
35-
console.log(`\n${yellow("⚠ Conflicts:")}`);
36-
for (const conflict of conflicts) {
37-
console.log(` ${red("C")} ${conflict.path}`);
55+
// Guidance
56+
console.log();
57+
const { action, reason } = info.nextAction;
58+
59+
if (isOnMainFresh && !info.hasChanges) {
60+
console.log(
61+
`Edit files, then run ${cyan("arr create")} to start a new stack`,
62+
);
63+
console.log(
64+
dim(`Or run ${cyan("arr top")} to return to your previous stack`),
65+
);
66+
} else {
67+
switch (action) {
68+
case "continue":
69+
console.log(`Fix conflicts, then run ${cyan("arr continue")}`);
70+
break;
71+
case "create":
72+
if (reason === "unsaved") {
73+
console.log(`Run ${cyan("arr create")} to save as a new change`);
74+
} else {
75+
console.log(`Edit files, then run ${cyan("arr create")}`);
76+
}
77+
break;
78+
case "submit":
79+
console.log(
80+
`Run ${cyan("arr submit")} to ${reason === "update_pr" ? "update PR" : "create PR"}`,
81+
);
82+
break;
83+
case "up":
84+
console.log(`Run ${cyan("arr up")} to start a new change`);
85+
break;
3886
}
3987
}
4088
}

packages/cli/src/commands/submit.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import { checkPrerequisites, isGhInstalled } from "@array/core";
22
import {
33
cyan,
44
dim,
5-
formatSuccess,
65
green,
76
printInstallInstructions,
7+
yellow,
88
} from "../utils/output";
99
import { createStacks, unwrap } from "../utils/run";
1010

@@ -32,13 +32,23 @@ export async function submit(
3232
await createStacks().submitStack({ draft: Boolean(flags.draft) }),
3333
);
3434

35-
for (const pr of result.prs) {
36-
console.log(formatSuccess(`PR #${pr.prNumber}: ${cyan(pr.bookmarkName)}`));
35+
// Only show PRs that were created or pushed (not synced)
36+
const changedPrs = result.prs.filter((pr) => pr.status !== "synced");
37+
38+
for (const pr of changedPrs) {
39+
const label = pr.status === "created" ? green("Created") : yellow("Pushed");
40+
console.log(`${label} PR #${pr.prNumber}: ${cyan(pr.bookmarkName)}`);
3741
console.log(` ${cyan(pr.prUrl)}`);
3842
}
3943

40-
console.log();
41-
console.log(
42-
`${green("Created:")} ${result.created} ${dim("Updated:")} ${result.updated}`,
43-
);
44+
// Summary
45+
const parts: string[] = [];
46+
if (result.created > 0) parts.push(`${green("Created:")} ${result.created}`);
47+
if (result.pushed > 0) parts.push(`${yellow("Pushed:")} ${result.pushed}`);
48+
if (result.synced > 0) parts.push(`${dim(`(${result.synced} unchanged)`)}`);
49+
50+
if (parts.length > 0) {
51+
console.log();
52+
console.log(parts.join(" "));
53+
}
4454
}

packages/cli/src/commands/sync.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,15 @@ export async function sync(): Promise<void> {
2929
}
3030

3131
const stacks = createStackManager(jj);
32+
33+
// Check for changes with merged PRs that weren't caught by rebase
34+
const cleanupResult = await stacks.cleanupMergedChanges();
35+
if (cleanupResult.ok && cleanupResult.value.abandoned.length > 0) {
36+
for (const { prNumber } of cleanupResult.value.abandoned) {
37+
console.log(formatSuccess(`Cleaned up merged PR #${prNumber}`));
38+
}
39+
}
40+
3241
const updateResult = await stacks.updateStackComments();
3342
if (updateResult.ok && updateResult.value.updated > 0) {
3443
console.log(formatSuccess("Updated stack comments"));

packages/cli/src/commands/top.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ import { printNav } from "../utils/output";
22
import { createJJ, unwrap } from "../utils/run";
33

44
export async function top(): Promise<void> {
5-
printNav("Jumped to top", unwrap(await createJJ().navigateTop()));
5+
printNav("up", unwrap(await createJJ().navigateTop()));
66
}

packages/cli/src/commands/up.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ import { printNav } from "../utils/output";
22
import { createJJ, unwrap } from "../utils/run";
33

44
export async function up(): Promise<void> {
5-
printNav("Moved up", unwrap(await createJJ().navigateUp()));
5+
printNav("up", unwrap(await createJJ().navigateUp()));
66
}

0 commit comments

Comments
 (0)