@cvr/stacked 0.2.0 → 0.4.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/README.md +78 -22
- package/bin/stacked +0 -0
- package/package.json +3 -2
- package/scripts/build.ts +8 -1
- package/skills/stacked/SKILL.md +221 -39
- package/src/commands/adopt.ts +70 -8
- package/src/commands/amend.ts +107 -0
- package/src/commands/bottom.ts +26 -8
- package/src/commands/checkout.ts +25 -6
- package/src/commands/clean.ts +106 -27
- package/src/commands/create.ts +74 -12
- package/src/commands/delete.ts +92 -24
- package/src/commands/detect.ts +152 -0
- package/src/commands/doctor.ts +124 -0
- package/src/commands/down.ts +62 -0
- package/src/commands/helpers/validate.ts +61 -0
- package/src/commands/index.ts +29 -1
- package/src/commands/init.ts +40 -0
- package/src/commands/list.ts +61 -22
- package/src/commands/log.ts +32 -6
- package/src/commands/rename.ts +49 -0
- package/src/commands/reorder.ts +93 -0
- package/src/commands/split.ts +108 -0
- package/src/commands/stacks.ts +33 -7
- package/src/commands/status.ts +55 -0
- package/src/commands/submit.ts +245 -16
- package/src/commands/sync.ts +127 -19
- package/src/commands/top.ts +24 -8
- package/src/commands/trunk.ts +37 -6
- package/src/commands/up.ts +55 -0
- package/src/errors/index.ts +30 -0
- package/src/global.d.ts +2 -0
- package/src/main.ts +70 -3
- package/src/services/Git.ts +102 -30
- package/src/services/GitHub.ts +56 -18
- package/src/services/Stack.ts +65 -58
- package/src/ui.ts +173 -0
package/src/commands/list.ts
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
|
-
import { Argument, Command } from "effect/unstable/cli";
|
|
1
|
+
import { Argument, Command, Flag } from "effect/unstable/cli";
|
|
2
2
|
import { Console, Effect, Option } from "effect";
|
|
3
3
|
import { GitService } from "../services/Git.js";
|
|
4
4
|
import { GitHubService } from "../services/GitHub.js";
|
|
5
5
|
import { StackService } from "../services/Stack.js";
|
|
6
|
+
import { ErrorCode, StackError } from "../errors/index.js";
|
|
7
|
+
import { stdout } from "../ui.js";
|
|
6
8
|
|
|
7
|
-
const stackNameArg = Argument.string("stack").pipe(
|
|
9
|
+
const stackNameArg = Argument.string("stack").pipe(
|
|
10
|
+
Argument.withDescription("Stack name (defaults to current)"),
|
|
11
|
+
Argument.optional,
|
|
12
|
+
);
|
|
13
|
+
const jsonFlag = Flag.boolean("json").pipe(Flag.withDescription("Output as JSON"));
|
|
8
14
|
|
|
9
|
-
export const list = Command.make("list", { stackName: stackNameArg }).pipe(
|
|
15
|
+
export const list = Command.make("list", { stackName: stackNameArg, json: jsonFlag }).pipe(
|
|
10
16
|
Command.withDescription("Show stack branches (defaults to current stack)"),
|
|
11
|
-
Command.
|
|
17
|
+
Command.withExamples([
|
|
18
|
+
{ command: "stacked list", description: "Show branches in current stack" },
|
|
19
|
+
{ command: "stacked list my-stack --json", description: "JSON output for a specific stack" },
|
|
20
|
+
]),
|
|
21
|
+
Command.withHandler(({ stackName, json }) =>
|
|
12
22
|
Effect.gen(function* () {
|
|
13
23
|
const git = yield* GitService;
|
|
14
24
|
const gh = yield* GitHubService;
|
|
@@ -24,50 +34,79 @@ export const list = Command.make("list", { stackName: stackNameArg }).pipe(
|
|
|
24
34
|
if (Option.isSome(stackName)) {
|
|
25
35
|
const s = data.stacks[stackName.value];
|
|
26
36
|
if (s === undefined) {
|
|
27
|
-
yield*
|
|
28
|
-
|
|
37
|
+
return yield* new StackError({
|
|
38
|
+
code: ErrorCode.STACK_NOT_FOUND,
|
|
39
|
+
message: `Stack "${stackName.value}" not found`,
|
|
40
|
+
});
|
|
29
41
|
}
|
|
30
42
|
targetStackName = stackName.value;
|
|
31
43
|
targetStack = s;
|
|
32
44
|
} else {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
break;
|
|
38
|
-
}
|
|
45
|
+
const found = yield* stacks.findBranchStack(currentBranch);
|
|
46
|
+
if (found !== null) {
|
|
47
|
+
targetStackName = found.name;
|
|
48
|
+
targetStack = found.stack;
|
|
39
49
|
}
|
|
40
50
|
}
|
|
41
51
|
|
|
42
52
|
if (targetStackName === null || targetStack === null) {
|
|
43
|
-
yield*
|
|
53
|
+
return yield* new StackError({
|
|
54
|
+
code: ErrorCode.NOT_IN_STACK,
|
|
55
|
+
message:
|
|
56
|
+
"Not on a stacked branch. Run 'stacked list' to see your stacks, or 'stacked create <name>' to start one.",
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const prStatuses = yield* Effect.forEach(
|
|
61
|
+
targetStack.branches as readonly string[],
|
|
62
|
+
(branch) =>
|
|
63
|
+
gh.getPR(branch).pipe(
|
|
64
|
+
Effect.catchTag("GitHubError", () => Effect.succeed(null)),
|
|
65
|
+
Effect.map((pr) => [branch, pr] as const),
|
|
66
|
+
),
|
|
67
|
+
{ concurrency: 5 },
|
|
68
|
+
);
|
|
69
|
+
const prMap = new Map(prStatuses);
|
|
70
|
+
|
|
71
|
+
if (json) {
|
|
72
|
+
const branches = [...targetStack.branches].map((branch) => {
|
|
73
|
+
const pr = prMap.get(branch) ?? null;
|
|
74
|
+
return {
|
|
75
|
+
name: branch,
|
|
76
|
+
current: branch === currentBranch,
|
|
77
|
+
pr: pr !== null ? { number: pr.number, url: pr.url, state: pr.state } : null,
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
// @effect-diagnostics-next-line effect/preferSchemaOverJson:off
|
|
81
|
+
yield* Console.log(JSON.stringify({ stack: targetStackName, trunk, branches }, null, 2));
|
|
44
82
|
return;
|
|
45
83
|
}
|
|
46
84
|
|
|
47
85
|
const lines: string[] = [];
|
|
48
86
|
|
|
49
|
-
lines.push(`Stack: ${targetStackName}`);
|
|
50
|
-
lines.push(`Trunk: ${trunk}`);
|
|
87
|
+
lines.push(`Stack: ${stdout.bold(targetStackName)}`);
|
|
88
|
+
lines.push(`Trunk: ${stdout.dim(trunk)}`);
|
|
51
89
|
lines.push("");
|
|
52
90
|
|
|
53
91
|
for (let i = targetStack.branches.length - 1; i >= 0; i--) {
|
|
54
92
|
const branch = targetStack.branches[i];
|
|
55
93
|
if (branch === undefined) continue;
|
|
56
94
|
const isCurrent = branch === currentBranch;
|
|
57
|
-
const marker = isCurrent ? "* " : " ";
|
|
58
|
-
const prefix = i === 0 ? "└─" : "├─";
|
|
95
|
+
const marker = isCurrent ? stdout.green("* ") : " ";
|
|
96
|
+
const prefix = stdout.dim(i === 0 ? "└─" : "├─");
|
|
97
|
+
const name = isCurrent ? stdout.bold(branch) : branch;
|
|
59
98
|
|
|
60
|
-
const pr =
|
|
99
|
+
const pr = prMap.get(branch) ?? null;
|
|
61
100
|
const status =
|
|
62
101
|
pr === null
|
|
63
102
|
? ""
|
|
64
103
|
: pr.state === "MERGED"
|
|
65
|
-
? " [merged]"
|
|
104
|
+
? stdout.green(" [merged]")
|
|
66
105
|
: pr.state === "CLOSED"
|
|
67
|
-
? " [closed]"
|
|
68
|
-
: ` [#${pr.number}]
|
|
106
|
+
? stdout.dim(" [closed]")
|
|
107
|
+
: stdout.cyan(` [#${pr.number}]`);
|
|
69
108
|
|
|
70
|
-
lines.push(`${marker}${prefix} ${
|
|
109
|
+
lines.push(`${marker}${prefix} ${name}${status}`);
|
|
71
110
|
}
|
|
72
111
|
|
|
73
112
|
yield* Console.log(lines.join("\n"));
|
package/src/commands/log.ts
CHANGED
|
@@ -1,24 +1,50 @@
|
|
|
1
|
-
import { Command } from "effect/unstable/cli";
|
|
1
|
+
import { Command, Flag } from "effect/unstable/cli";
|
|
2
2
|
import { Console, Effect } from "effect";
|
|
3
3
|
import { GitService } from "../services/Git.js";
|
|
4
4
|
import { StackService } from "../services/Stack.js";
|
|
5
|
+
import { ErrorCode, StackError } from "../errors/index.js";
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
const jsonFlag = Flag.boolean("json").pipe(Flag.withDescription("Output as JSON"));
|
|
8
|
+
|
|
9
|
+
export const log = Command.make("log", { json: jsonFlag }).pipe(
|
|
7
10
|
Command.withDescription("Show commits across all branches in stack"),
|
|
8
|
-
Command.
|
|
11
|
+
Command.withExamples([
|
|
12
|
+
{ command: "stacked log", description: "Show commits per branch" },
|
|
13
|
+
{ command: "stacked log --json", description: "JSON output" },
|
|
14
|
+
]),
|
|
15
|
+
Command.withHandler(({ json }) =>
|
|
9
16
|
Effect.gen(function* () {
|
|
10
17
|
const git = yield* GitService;
|
|
11
18
|
const stacks = yield* StackService;
|
|
12
19
|
|
|
13
20
|
const result = yield* stacks.currentStack();
|
|
14
21
|
if (result === null) {
|
|
15
|
-
yield*
|
|
16
|
-
|
|
22
|
+
return yield* new StackError({
|
|
23
|
+
code: ErrorCode.NOT_IN_STACK,
|
|
24
|
+
message:
|
|
25
|
+
"Not on a stacked branch. Run 'stacked list' to see your stacks, or 'stacked create <name>' to start one.",
|
|
26
|
+
});
|
|
17
27
|
}
|
|
18
28
|
|
|
19
29
|
const trunk = yield* stacks.getTrunk();
|
|
20
30
|
const { branches } = result.stack;
|
|
21
31
|
|
|
32
|
+
if (json) {
|
|
33
|
+
const entries = [];
|
|
34
|
+
for (let i = 0; i < branches.length; i++) {
|
|
35
|
+
const branch = branches[i];
|
|
36
|
+
if (branch === undefined) continue;
|
|
37
|
+
const base = i === 0 ? trunk : (branches[i - 1] ?? trunk);
|
|
38
|
+
const commits = yield* git
|
|
39
|
+
.log(`${base}..${branch}`, { oneline: true })
|
|
40
|
+
.pipe(Effect.catchTag("GitError", () => Effect.succeed("")));
|
|
41
|
+
entries.push({ name: branch, base, commits: commits || "" });
|
|
42
|
+
}
|
|
43
|
+
// @effect-diagnostics-next-line effect/preferSchemaOverJson:off
|
|
44
|
+
yield* Console.log(JSON.stringify({ branches: entries }, null, 2));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
22
48
|
for (let i = 0; i < branches.length; i++) {
|
|
23
49
|
const branch = branches[i];
|
|
24
50
|
if (branch === undefined) continue;
|
|
@@ -26,7 +52,7 @@ export const log = Command.make("log").pipe(
|
|
|
26
52
|
yield* Console.log(`\n── ${branch} ──`);
|
|
27
53
|
const rangeLog = yield* git
|
|
28
54
|
.log(`${base}..${branch}`, { oneline: true })
|
|
29
|
-
.pipe(Effect.
|
|
55
|
+
.pipe(Effect.catchTag("GitError", () => Effect.succeed("(no commits)")));
|
|
30
56
|
yield* Console.log(rangeLog || "(no new commits)");
|
|
31
57
|
}
|
|
32
58
|
}),
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Argument, Command, Flag } from "effect/unstable/cli";
|
|
2
|
+
import { Console, Effect } from "effect";
|
|
3
|
+
import { StackService } from "../services/Stack.js";
|
|
4
|
+
import { ErrorCode, StackError } from "../errors/index.js";
|
|
5
|
+
import { success } from "../ui.js";
|
|
6
|
+
|
|
7
|
+
const oldArg = Argument.string("old").pipe(Argument.withDescription("Current stack name"));
|
|
8
|
+
const newArg = Argument.string("new").pipe(Argument.withDescription("New stack name"));
|
|
9
|
+
const jsonFlag = Flag.boolean("json").pipe(Flag.withDescription("Output as JSON"));
|
|
10
|
+
|
|
11
|
+
export const rename = Command.make("rename", { old: oldArg, new: newArg, json: jsonFlag }).pipe(
|
|
12
|
+
Command.withDescription("Rename a stack (not the branches, just the stack key)"),
|
|
13
|
+
Command.withExamples([
|
|
14
|
+
{ command: "stacked rename old-name new-name", description: "Rename a stack" },
|
|
15
|
+
]),
|
|
16
|
+
Command.withHandler(({ old: oldName, new: newName, json }) =>
|
|
17
|
+
Effect.gen(function* () {
|
|
18
|
+
const stacks = yield* StackService;
|
|
19
|
+
|
|
20
|
+
const data = yield* stacks.load();
|
|
21
|
+
|
|
22
|
+
if (data.stacks[oldName] === undefined) {
|
|
23
|
+
return yield* new StackError({
|
|
24
|
+
code: ErrorCode.STACK_NOT_FOUND,
|
|
25
|
+
message: `Stack "${oldName}" not found`,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (data.stacks[newName] !== undefined) {
|
|
30
|
+
return yield* new StackError({
|
|
31
|
+
code: ErrorCode.STACK_EXISTS,
|
|
32
|
+
message: `Stack "${newName}" already exists`,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const stack = data.stacks[oldName];
|
|
37
|
+
if (stack === undefined) return;
|
|
38
|
+
const { [oldName]: _, ...rest } = data.stacks;
|
|
39
|
+
yield* stacks.save({ ...data, stacks: { ...rest, [newName]: stack } });
|
|
40
|
+
|
|
41
|
+
if (json) {
|
|
42
|
+
// @effect-diagnostics-next-line effect/preferSchemaOverJson:off
|
|
43
|
+
yield* Console.log(JSON.stringify({ old: oldName, new: newName }, null, 2));
|
|
44
|
+
} else {
|
|
45
|
+
yield* success(`Renamed stack "${oldName}" to "${newName}"`);
|
|
46
|
+
}
|
|
47
|
+
}),
|
|
48
|
+
),
|
|
49
|
+
);
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Argument, Command, Flag } from "effect/unstable/cli";
|
|
2
|
+
import { Console, Effect, Option } from "effect";
|
|
3
|
+
import { StackService } from "../services/Stack.js";
|
|
4
|
+
import { ErrorCode, StackError } from "../errors/index.js";
|
|
5
|
+
import { success, warn } from "../ui.js";
|
|
6
|
+
|
|
7
|
+
const branchArg = Argument.string("branch").pipe(Argument.withDescription("Branch to move"));
|
|
8
|
+
const beforeFlag = Flag.string("before").pipe(
|
|
9
|
+
Flag.optional,
|
|
10
|
+
Flag.withDescription("Move branch before this branch"),
|
|
11
|
+
);
|
|
12
|
+
const afterFlag = Flag.string("after").pipe(
|
|
13
|
+
Flag.optional,
|
|
14
|
+
Flag.withDescription("Move branch after this branch"),
|
|
15
|
+
);
|
|
16
|
+
const jsonFlag = Flag.boolean("json").pipe(Flag.withDescription("Output as JSON"));
|
|
17
|
+
|
|
18
|
+
export const reorder = Command.make("reorder", {
|
|
19
|
+
branch: branchArg,
|
|
20
|
+
before: beforeFlag,
|
|
21
|
+
after: afterFlag,
|
|
22
|
+
json: jsonFlag,
|
|
23
|
+
}).pipe(
|
|
24
|
+
Command.withDescription("Move a branch to a different position in the stack"),
|
|
25
|
+
Command.withExamples([
|
|
26
|
+
{ command: "stacked reorder feat-b --before feat-a", description: "Move feat-b before feat-a" },
|
|
27
|
+
{ command: "stacked reorder feat-b --after feat-c", description: "Move feat-b after feat-c" },
|
|
28
|
+
]),
|
|
29
|
+
Command.withHandler(({ branch, before, after, json }) =>
|
|
30
|
+
Effect.gen(function* () {
|
|
31
|
+
const stacks = yield* StackService;
|
|
32
|
+
|
|
33
|
+
if (Option.isNone(before) && Option.isNone(after)) {
|
|
34
|
+
return yield* new StackError({
|
|
35
|
+
message: "Specify --before or --after to indicate target position",
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (Option.isSome(before) && Option.isSome(after)) {
|
|
40
|
+
return yield* new StackError({
|
|
41
|
+
message: "Specify either --before or --after, not both",
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const result = yield* stacks.findBranchStack(branch);
|
|
46
|
+
if (result === null) {
|
|
47
|
+
return yield* new StackError({
|
|
48
|
+
code: ErrorCode.BRANCH_NOT_FOUND,
|
|
49
|
+
message: `Branch "${branch}" not found in any stack`,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const { name: stackName, stack } = result;
|
|
54
|
+
const branches = [...stack.branches];
|
|
55
|
+
const currentIdx = branches.indexOf(branch);
|
|
56
|
+
if (currentIdx === -1) return;
|
|
57
|
+
|
|
58
|
+
const target = Option.isSome(before) ? before.value : Option.getOrElse(after, () => "");
|
|
59
|
+
const targetIdx = branches.indexOf(target);
|
|
60
|
+
if (targetIdx === -1) {
|
|
61
|
+
return yield* new StackError({
|
|
62
|
+
code: ErrorCode.BRANCH_NOT_FOUND,
|
|
63
|
+
message: `Branch "${target}" not found in stack "${stackName}"`,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Remove from current position
|
|
68
|
+
branches.splice(currentIdx, 1);
|
|
69
|
+
|
|
70
|
+
// Insert at target position
|
|
71
|
+
const newTargetIdx = branches.indexOf(target);
|
|
72
|
+
if (Option.isSome(before)) {
|
|
73
|
+
branches.splice(newTargetIdx, 0, branch);
|
|
74
|
+
} else {
|
|
75
|
+
branches.splice(newTargetIdx + 1, 0, branch);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const data = yield* stacks.load();
|
|
79
|
+
yield* stacks.save({
|
|
80
|
+
...data,
|
|
81
|
+
stacks: { ...data.stacks, [stackName]: { branches } },
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (json) {
|
|
85
|
+
// @effect-diagnostics-next-line effect/preferSchemaOverJson:off
|
|
86
|
+
yield* Console.log(JSON.stringify({ branch, stack: stackName, branches }, null, 2));
|
|
87
|
+
} else {
|
|
88
|
+
yield* success(`Moved "${branch}" in stack "${stackName}"`);
|
|
89
|
+
yield* warn("Run 'stacked sync' to rebase branches in new order");
|
|
90
|
+
}
|
|
91
|
+
}),
|
|
92
|
+
),
|
|
93
|
+
);
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { Argument, Command, Flag } from "effect/unstable/cli";
|
|
2
|
+
import { Console, Effect } from "effect";
|
|
3
|
+
import { StackService } from "../services/Stack.js";
|
|
4
|
+
import { ErrorCode, StackError } from "../errors/index.js";
|
|
5
|
+
import { success } from "../ui.js";
|
|
6
|
+
|
|
7
|
+
const branchArg = Argument.string("branch").pipe(
|
|
8
|
+
Argument.withDescription("Branch at which to split the stack"),
|
|
9
|
+
);
|
|
10
|
+
const jsonFlag = Flag.boolean("json").pipe(Flag.withDescription("Output as JSON"));
|
|
11
|
+
const dryRunFlag = Flag.boolean("dry-run").pipe(
|
|
12
|
+
Flag.withDescription("Show what would happen without making changes"),
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
export const split = Command.make("split", {
|
|
16
|
+
branch: branchArg,
|
|
17
|
+
json: jsonFlag,
|
|
18
|
+
dryRun: dryRunFlag,
|
|
19
|
+
}).pipe(
|
|
20
|
+
Command.withDescription("Split a stack at a branch — branches at and above become a new stack"),
|
|
21
|
+
Command.withExamples([
|
|
22
|
+
{ command: "stacked split feat-b", description: "Split at feat-b" },
|
|
23
|
+
{ command: "stacked split feat-b --dry-run", description: "Preview the split" },
|
|
24
|
+
]),
|
|
25
|
+
Command.withHandler(({ branch, json, dryRun }) =>
|
|
26
|
+
Effect.gen(function* () {
|
|
27
|
+
const stacks = yield* StackService;
|
|
28
|
+
|
|
29
|
+
const result = yield* stacks.findBranchStack(branch);
|
|
30
|
+
if (result === null) {
|
|
31
|
+
return yield* new StackError({
|
|
32
|
+
code: ErrorCode.BRANCH_NOT_FOUND,
|
|
33
|
+
message: `Branch "${branch}" not found in any stack`,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const { name: stackName, stack } = result;
|
|
38
|
+
const branches = [...stack.branches];
|
|
39
|
+
const splitIdx = branches.indexOf(branch);
|
|
40
|
+
|
|
41
|
+
if (splitIdx === 0) {
|
|
42
|
+
return yield* new StackError({
|
|
43
|
+
message: `Branch "${branch}" is at the bottom of the stack — nothing to split`,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const below = branches.slice(0, splitIdx);
|
|
48
|
+
const above = branches.slice(splitIdx);
|
|
49
|
+
const newStackName = branch;
|
|
50
|
+
|
|
51
|
+
if (dryRun) {
|
|
52
|
+
if (json) {
|
|
53
|
+
// @effect-diagnostics-next-line effect/preferSchemaOverJson:off
|
|
54
|
+
yield* Console.log(
|
|
55
|
+
JSON.stringify(
|
|
56
|
+
{
|
|
57
|
+
original: { name: stackName, branches: below },
|
|
58
|
+
new: { name: newStackName, branches: above },
|
|
59
|
+
},
|
|
60
|
+
null,
|
|
61
|
+
2,
|
|
62
|
+
),
|
|
63
|
+
);
|
|
64
|
+
} else {
|
|
65
|
+
yield* Console.error(`Would keep "${stackName}": ${below.join(" → ")}`);
|
|
66
|
+
yield* Console.error(`Would create "${newStackName}": ${above.join(" → ")}`);
|
|
67
|
+
}
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const data = yield* stacks.load();
|
|
72
|
+
|
|
73
|
+
if (data.stacks[newStackName] !== undefined) {
|
|
74
|
+
return yield* new StackError({
|
|
75
|
+
code: ErrorCode.STACK_EXISTS,
|
|
76
|
+
message: `Stack "${newStackName}" already exists — choose a different split point or rename it first`,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
yield* stacks.save({
|
|
81
|
+
...data,
|
|
82
|
+
stacks: {
|
|
83
|
+
...data.stacks,
|
|
84
|
+
[stackName]: { branches: below },
|
|
85
|
+
[newStackName]: { branches: above },
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (json) {
|
|
90
|
+
// @effect-diagnostics-next-line effect/preferSchemaOverJson:off
|
|
91
|
+
yield* Console.log(
|
|
92
|
+
JSON.stringify(
|
|
93
|
+
{
|
|
94
|
+
original: { name: stackName, branches: below },
|
|
95
|
+
new: { name: newStackName, branches: above },
|
|
96
|
+
},
|
|
97
|
+
null,
|
|
98
|
+
2,
|
|
99
|
+
),
|
|
100
|
+
);
|
|
101
|
+
} else {
|
|
102
|
+
yield* success(`Split stack "${stackName}" at "${branch}"`);
|
|
103
|
+
yield* Console.error(` "${stackName}": ${below.join(" → ")}`);
|
|
104
|
+
yield* Console.error(` "${newStackName}": ${above.join(" → ")}`);
|
|
105
|
+
}
|
|
106
|
+
}),
|
|
107
|
+
),
|
|
108
|
+
);
|
package/src/commands/stacks.ts
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
|
-
import { Command } from "effect/unstable/cli";
|
|
1
|
+
import { Command, Flag } from "effect/unstable/cli";
|
|
2
2
|
import { Console, Effect } from "effect";
|
|
3
3
|
import { GitService } from "../services/Git.js";
|
|
4
4
|
import { StackService } from "../services/Stack.js";
|
|
5
|
+
import { stdout } from "../ui.js";
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
const jsonFlag = Flag.boolean("json").pipe(Flag.withDescription("Output as JSON"));
|
|
8
|
+
|
|
9
|
+
export const stacks = Command.make("stacks", { json: jsonFlag }).pipe(
|
|
7
10
|
Command.withDescription("List all stacks in the repo"),
|
|
8
|
-
Command.
|
|
11
|
+
Command.withExamples([
|
|
12
|
+
{ command: "stacked stacks", description: "List all stacks" },
|
|
13
|
+
{ command: "stacked stacks --json", description: "JSON output" },
|
|
14
|
+
]),
|
|
15
|
+
Command.withHandler(({ json }) =>
|
|
9
16
|
Effect.gen(function* () {
|
|
10
17
|
const git = yield* GitService;
|
|
11
18
|
const stackService = yield* StackService;
|
|
@@ -15,16 +22,35 @@ export const stacks = Command.make("stacks").pipe(
|
|
|
15
22
|
|
|
16
23
|
const entries = Object.entries(data.stacks);
|
|
17
24
|
if (entries.length === 0) {
|
|
18
|
-
|
|
25
|
+
if (json) {
|
|
26
|
+
// @effect-diagnostics-next-line effect/preferSchemaOverJson:off
|
|
27
|
+
yield* Console.log(JSON.stringify({ stacks: [] }));
|
|
28
|
+
} else {
|
|
29
|
+
yield* Console.error("No stacks");
|
|
30
|
+
}
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (json) {
|
|
35
|
+
const stackList = entries.map(([name, stack]) => ({
|
|
36
|
+
name,
|
|
37
|
+
branches: stack.branches.length,
|
|
38
|
+
current: stack.branches.includes(currentBranch),
|
|
39
|
+
}));
|
|
40
|
+
// @effect-diagnostics-next-line effect/preferSchemaOverJson:off
|
|
41
|
+
yield* Console.log(JSON.stringify({ stacks: stackList }, null, 2));
|
|
19
42
|
return;
|
|
20
43
|
}
|
|
21
44
|
|
|
22
45
|
const lines: string[] = [];
|
|
23
|
-
for (const [name, stack] of
|
|
46
|
+
for (const [name, stack] of entries) {
|
|
24
47
|
const isCurrent = stack.branches.includes(currentBranch);
|
|
25
|
-
const marker = isCurrent ? "* " : " ";
|
|
48
|
+
const marker = isCurrent ? stdout.green("* ") : " ";
|
|
49
|
+
const label = isCurrent ? stdout.bold(name) : name;
|
|
26
50
|
const count = stack.branches.length;
|
|
27
|
-
lines.push(
|
|
51
|
+
lines.push(
|
|
52
|
+
`${marker}${label} ${stdout.dim(`(${count} branch${count === 1 ? "" : "es"})`)}`,
|
|
53
|
+
);
|
|
28
54
|
}
|
|
29
55
|
|
|
30
56
|
yield* Console.log(lines.join("\n"));
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Command, Flag } from "effect/unstable/cli";
|
|
2
|
+
import { Console, Effect } from "effect";
|
|
3
|
+
import { GitService } from "../services/Git.js";
|
|
4
|
+
import { StackService } from "../services/Stack.js";
|
|
5
|
+
import { stdout } from "../ui.js";
|
|
6
|
+
|
|
7
|
+
const jsonFlag = Flag.boolean("json").pipe(Flag.withDescription("Output as JSON"));
|
|
8
|
+
|
|
9
|
+
export const status = Command.make("status", { json: jsonFlag }).pipe(
|
|
10
|
+
Command.withDescription("Show current branch, stack position, and working tree state"),
|
|
11
|
+
Command.withExamples([
|
|
12
|
+
{ command: "stacked status", description: "Show where you are in the stack" },
|
|
13
|
+
{ command: "stacked status --json", description: "JSON output" },
|
|
14
|
+
]),
|
|
15
|
+
Command.withHandler(({ json }) =>
|
|
16
|
+
Effect.gen(function* () {
|
|
17
|
+
const git = yield* GitService;
|
|
18
|
+
const stacks = yield* StackService;
|
|
19
|
+
|
|
20
|
+
const currentBranch = yield* git.currentBranch();
|
|
21
|
+
const clean = yield* git.isClean();
|
|
22
|
+
const result = yield* stacks.currentStack();
|
|
23
|
+
|
|
24
|
+
if (json) {
|
|
25
|
+
const stack =
|
|
26
|
+
result !== null
|
|
27
|
+
? {
|
|
28
|
+
name: result.name,
|
|
29
|
+
position: result.stack.branches.indexOf(currentBranch) + 1,
|
|
30
|
+
total: result.stack.branches.length,
|
|
31
|
+
}
|
|
32
|
+
: null;
|
|
33
|
+
// @effect-diagnostics-next-line effect/preferSchemaOverJson:off
|
|
34
|
+
yield* Console.log(JSON.stringify({ branch: currentBranch, clean, stack }, null, 2));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const lines: string[] = [];
|
|
39
|
+
lines.push(`Branch: ${stdout.bold(currentBranch)}`);
|
|
40
|
+
lines.push(`Working tree: ${clean ? stdout.green("clean") : stdout.yellow("dirty")}`);
|
|
41
|
+
|
|
42
|
+
if (result !== null) {
|
|
43
|
+
const { branches } = result.stack;
|
|
44
|
+
const idx = branches.indexOf(currentBranch);
|
|
45
|
+
lines.push(
|
|
46
|
+
`Stack: ${stdout.bold(result.name)} ${stdout.dim(`(${idx + 1} of ${branches.length})`)}`,
|
|
47
|
+
);
|
|
48
|
+
} else {
|
|
49
|
+
lines.push(stdout.dim("Not in a stack. Run 'stacked create <name>' to start one."));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
yield* Console.log(lines.join("\n"));
|
|
53
|
+
}),
|
|
54
|
+
),
|
|
55
|
+
);
|