@cvr/stacked 0.3.0 → 0.4.1
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 +74 -23
- package/bin/stacked +0 -0
- package/package.json +11 -10
- package/scripts/build.ts +8 -1
- package/skills/stacked/SKILL.md +210 -43
- package/src/commands/adopt.ts +71 -8
- package/src/commands/amend.ts +114 -0
- package/src/commands/bottom.ts +26 -8
- package/src/commands/checkout.ts +25 -6
- package/src/commands/clean.ts +110 -27
- package/src/commands/create.ts +74 -12
- package/src/commands/delete.ts +92 -24
- package/src/commands/detect.ts +95 -54
- package/src/commands/doctor.ts +124 -0
- package/src/commands/down.ts +62 -0
- package/src/commands/helpers/detect.ts +22 -0
- package/src/commands/helpers/validate.ts +61 -0
- package/src/commands/index.ts +27 -1
- package/src/commands/init.ts +42 -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 +103 -0
- package/src/commands/split.ts +108 -0
- package/src/commands/stacks.ts +32 -7
- package/src/commands/status.ts +57 -0
- package/src/commands/submit.ts +272 -16
- package/src/commands/sync.ts +131 -20
- 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 +37 -1
- package/src/global.d.ts +2 -0
- package/src/main.ts +78 -3
- package/src/services/Git.ts +96 -31
- package/src/services/GitHub.ts +56 -18
- package/src/services/Stack.ts +65 -58
- package/src/ui.ts +247 -0
|
@@ -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 { ErrorCode, StackError } from "../errors/index.js";
|
|
6
|
+
import { success } from "../ui.js";
|
|
7
|
+
|
|
8
|
+
const jsonFlag = Flag.boolean("json").pipe(Flag.withDescription("Output as JSON"));
|
|
9
|
+
|
|
10
|
+
export const up = Command.make("up", { json: jsonFlag }).pipe(
|
|
11
|
+
Command.withDescription("Move up one branch in the stack"),
|
|
12
|
+
Command.withExamples([{ command: "stacked up", description: "Move to the next branch above" }]),
|
|
13
|
+
Command.withHandler(({ json }) =>
|
|
14
|
+
Effect.gen(function* () {
|
|
15
|
+
const git = yield* GitService;
|
|
16
|
+
const stacks = yield* StackService;
|
|
17
|
+
|
|
18
|
+
const currentBranch = yield* git.currentBranch();
|
|
19
|
+
const result = yield* stacks.currentStack();
|
|
20
|
+
if (result === null) {
|
|
21
|
+
return yield* new StackError({
|
|
22
|
+
code: ErrorCode.NOT_IN_STACK,
|
|
23
|
+
message:
|
|
24
|
+
"Not on a stacked branch. Run 'stacked list' to see your stacks, or 'stacked create <name>' to start one.",
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const { branches } = result.stack;
|
|
29
|
+
const idx = branches.indexOf(currentBranch);
|
|
30
|
+
if (idx === -1) {
|
|
31
|
+
return yield* new StackError({
|
|
32
|
+
code: ErrorCode.NOT_IN_STACK,
|
|
33
|
+
message: "Current branch not found in stack",
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const next = branches[idx + 1];
|
|
38
|
+
if (next === undefined) {
|
|
39
|
+
return yield* new StackError({
|
|
40
|
+
code: ErrorCode.ALREADY_AT_TOP,
|
|
41
|
+
message: "Already at the top of the stack",
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
yield* git.checkout(next);
|
|
46
|
+
|
|
47
|
+
if (json) {
|
|
48
|
+
// @effect-diagnostics-next-line effect/preferSchemaOverJson:off
|
|
49
|
+
yield* Console.log(JSON.stringify({ branch: next, from: currentBranch }, null, 2));
|
|
50
|
+
} else {
|
|
51
|
+
yield* success(`Switched to ${next}`);
|
|
52
|
+
}
|
|
53
|
+
}),
|
|
54
|
+
),
|
|
55
|
+
);
|
package/src/errors/index.ts
CHANGED
|
@@ -1,15 +1,51 @@
|
|
|
1
|
-
import { Schema } from "effect";
|
|
1
|
+
import { Runtime, Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Error Codes
|
|
5
|
+
// ============================================================================
|
|
6
|
+
|
|
7
|
+
export const ErrorCode = {
|
|
8
|
+
BRANCH_EXISTS: "BRANCH_EXISTS",
|
|
9
|
+
BRANCH_NOT_FOUND: "BRANCH_NOT_FOUND",
|
|
10
|
+
NOT_IN_STACK: "NOT_IN_STACK",
|
|
11
|
+
DIRTY_WORKTREE: "DIRTY_WORKTREE",
|
|
12
|
+
REBASE_CONFLICT: "REBASE_CONFLICT",
|
|
13
|
+
GH_NOT_INSTALLED: "GH_NOT_INSTALLED",
|
|
14
|
+
STACK_NOT_FOUND: "STACK_NOT_FOUND",
|
|
15
|
+
INVALID_BRANCH_NAME: "INVALID_BRANCH_NAME",
|
|
16
|
+
NOT_A_GIT_REPO: "NOT_A_GIT_REPO",
|
|
17
|
+
ALREADY_AT_TOP: "ALREADY_AT_TOP",
|
|
18
|
+
ALREADY_AT_BOTTOM: "ALREADY_AT_BOTTOM",
|
|
19
|
+
STACK_EMPTY: "STACK_EMPTY",
|
|
20
|
+
TRUNK_ERROR: "TRUNK_ERROR",
|
|
21
|
+
STACK_EXISTS: "STACK_EXISTS",
|
|
22
|
+
} as const;
|
|
23
|
+
|
|
24
|
+
export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
|
|
25
|
+
|
|
26
|
+
// ============================================================================
|
|
27
|
+
// Error Classes
|
|
28
|
+
// ============================================================================
|
|
2
29
|
|
|
3
30
|
export class GitError extends Schema.TaggedErrorClass<GitError>()("GitError", {
|
|
4
31
|
message: Schema.String,
|
|
5
32
|
command: Schema.optional(Schema.String),
|
|
33
|
+
code: Schema.optional(Schema.String),
|
|
6
34
|
}) {}
|
|
7
35
|
|
|
8
36
|
export class StackError extends Schema.TaggedErrorClass<StackError>()("StackError", {
|
|
9
37
|
message: Schema.String,
|
|
38
|
+
code: Schema.optional(Schema.String),
|
|
10
39
|
}) {}
|
|
11
40
|
|
|
12
41
|
export class GitHubError extends Schema.TaggedErrorClass<GitHubError>()("GitHubError", {
|
|
13
42
|
message: Schema.String,
|
|
14
43
|
command: Schema.optional(Schema.String),
|
|
44
|
+
code: Schema.optional(Schema.String),
|
|
15
45
|
}) {}
|
|
46
|
+
|
|
47
|
+
export class GlobalFlagConflictError extends Error {
|
|
48
|
+
override readonly name = "GlobalFlagConflictError";
|
|
49
|
+
override readonly [Runtime.errorExitCode] = 2;
|
|
50
|
+
override readonly [Runtime.errorReported] = false;
|
|
51
|
+
}
|
package/src/global.d.ts
ADDED
package/src/main.ts
CHANGED
|
@@ -1,14 +1,44 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { Command } from "effect/unstable/cli";
|
|
3
3
|
import { BunRuntime, BunServices } from "@effect/platform-bun";
|
|
4
|
-
import { Effect, Layer } from "effect";
|
|
4
|
+
import { Console, Effect, Layer } from "effect";
|
|
5
5
|
import { command } from "./commands/index.js";
|
|
6
6
|
import { GitService } from "./services/Git.js";
|
|
7
7
|
import { StackService } from "./services/Stack.js";
|
|
8
8
|
import { GitHubService } from "./services/GitHub.js";
|
|
9
|
+
import { OutputConfig } from "./ui.js";
|
|
10
|
+
import { GlobalFlagConflictError } from "./errors/index.js";
|
|
11
|
+
|
|
12
|
+
const version = typeof __VERSION__ !== "undefined" ? __VERSION__ : "dev";
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Global Flags (parsed before CLI framework, stripped from argv)
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
const globalFlags = new Set(["--verbose", "--quiet", "-q", "--no-color", "--yes", "-y"]);
|
|
19
|
+
const flagArgs = new Set(process.argv.filter((a) => globalFlags.has(a)));
|
|
20
|
+
process.argv = process.argv.filter((a) => !globalFlags.has(a));
|
|
21
|
+
|
|
22
|
+
const isVerbose = flagArgs.has("--verbose");
|
|
23
|
+
const isQuiet = flagArgs.has("--quiet") || flagArgs.has("-q");
|
|
24
|
+
const isNoColor = flagArgs.has("--no-color");
|
|
25
|
+
const isYes = flagArgs.has("--yes") || flagArgs.has("-y");
|
|
26
|
+
|
|
27
|
+
if (isNoColor) process.env["NO_COLOR"] = "1";
|
|
28
|
+
|
|
29
|
+
const preflight =
|
|
30
|
+
isVerbose && isQuiet
|
|
31
|
+
? Console.error("Error: --verbose and --quiet are mutually exclusive").pipe(
|
|
32
|
+
Effect.andThen(Effect.fail(new GlobalFlagConflictError())),
|
|
33
|
+
)
|
|
34
|
+
: Effect.void;
|
|
35
|
+
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// CLI
|
|
38
|
+
// ============================================================================
|
|
9
39
|
|
|
10
40
|
const cli = Command.run(command, {
|
|
11
|
-
version
|
|
41
|
+
version,
|
|
12
42
|
});
|
|
13
43
|
|
|
14
44
|
const ServiceLayer = StackService.layer.pipe(
|
|
@@ -18,5 +48,50 @@ const ServiceLayer = StackService.layer.pipe(
|
|
|
18
48
|
|
|
19
49
|
const AppLayer = Layer.mergeAll(ServiceLayer, BunServices.layer);
|
|
20
50
|
|
|
51
|
+
// Usage errors (bad args, invalid state) → exit 2
|
|
52
|
+
// Operational errors (git/gh failures) → exit 1
|
|
53
|
+
const usageCodes = new Set([
|
|
54
|
+
"INVALID_BRANCH_NAME",
|
|
55
|
+
"BRANCH_EXISTS",
|
|
56
|
+
"NOT_IN_STACK",
|
|
57
|
+
"STACK_NOT_FOUND",
|
|
58
|
+
"STACK_EMPTY",
|
|
59
|
+
"ALREADY_AT_TOP",
|
|
60
|
+
"ALREADY_AT_BOTTOM",
|
|
61
|
+
"TRUNK_ERROR",
|
|
62
|
+
"STACK_EXISTS",
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
const handleKnownError = (e: { message: string; code?: string | undefined }) =>
|
|
66
|
+
Console.error(e.code !== undefined ? `Error [${e.code}]: ${e.message}` : e.message).pipe(
|
|
67
|
+
Effect.andThen(
|
|
68
|
+
Effect.sync(() => {
|
|
69
|
+
process.exitCode = e.code !== undefined && usageCodes.has(e.code) ? 2 : 1;
|
|
70
|
+
}),
|
|
71
|
+
),
|
|
72
|
+
);
|
|
73
|
+
|
|
21
74
|
// @effect-diagnostics-next-line effect/strictEffectProvide:off
|
|
22
|
-
BunRuntime.runMain(
|
|
75
|
+
BunRuntime.runMain(
|
|
76
|
+
preflight.pipe(
|
|
77
|
+
Effect.andThen(cli),
|
|
78
|
+
Effect.provideService(OutputConfig, { verbose: isVerbose, quiet: isQuiet, yes: isYes }),
|
|
79
|
+
Effect.provide(AppLayer),
|
|
80
|
+
Effect.catchTags({
|
|
81
|
+
GitError: (e) => handleKnownError(e),
|
|
82
|
+
StackError: (e) => handleKnownError(e),
|
|
83
|
+
GitHubError: (e) => handleKnownError(e),
|
|
84
|
+
}),
|
|
85
|
+
Effect.catchIf(
|
|
86
|
+
(e): e is GlobalFlagConflictError => e instanceof GlobalFlagConflictError,
|
|
87
|
+
Effect.fail,
|
|
88
|
+
(e) => {
|
|
89
|
+
const msg =
|
|
90
|
+
e !== null && typeof e === "object" && "message" in e
|
|
91
|
+
? String(e.message)
|
|
92
|
+
: JSON.stringify(e, null, 2);
|
|
93
|
+
return handleKnownError({ message: `Unexpected error: ${msg}` });
|
|
94
|
+
},
|
|
95
|
+
),
|
|
96
|
+
),
|
|
97
|
+
);
|
package/src/services/Git.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
1
2
|
import { Effect, Layer, ServiceMap } from "effect";
|
|
2
3
|
import { GitError } from "../errors/index.js";
|
|
3
4
|
|
|
@@ -11,33 +12,63 @@ export class GitService extends ServiceMap.Service<
|
|
|
11
12
|
readonly deleteBranch: (name: string, force?: boolean) => Effect.Effect<void, GitError>;
|
|
12
13
|
readonly checkout: (name: string) => Effect.Effect<void, GitError>;
|
|
13
14
|
readonly rebase: (onto: string) => Effect.Effect<void, GitError>;
|
|
15
|
+
readonly rebaseOnto: (
|
|
16
|
+
branch: string,
|
|
17
|
+
newBase: string,
|
|
18
|
+
oldBase: string,
|
|
19
|
+
) => Effect.Effect<void, GitError>;
|
|
20
|
+
readonly rebaseAbort: () => Effect.Effect<void, GitError>;
|
|
14
21
|
readonly push: (branch: string, options?: { force?: boolean }) => Effect.Effect<void, GitError>;
|
|
15
22
|
readonly log: (
|
|
16
23
|
branch: string,
|
|
17
24
|
options?: { limit?: number; oneline?: boolean },
|
|
18
25
|
) => Effect.Effect<string, GitError>;
|
|
19
|
-
readonly mergeBase: (a: string, b: string) => Effect.Effect<string, GitError>;
|
|
20
26
|
readonly isClean: () => Effect.Effect<boolean, GitError>;
|
|
21
27
|
readonly revParse: (ref: string) => Effect.Effect<string, GitError>;
|
|
22
|
-
readonly diff: (
|
|
23
|
-
a: string,
|
|
24
|
-
b: string,
|
|
25
|
-
options?: { stat?: boolean },
|
|
26
|
-
) => Effect.Effect<string, GitError>;
|
|
27
28
|
readonly isAncestor: (ancestor: string, descendant: string) => Effect.Effect<boolean, GitError>;
|
|
28
|
-
readonly
|
|
29
|
+
readonly mergeBase: (a: string, b: string) => Effect.Effect<string, GitError>;
|
|
30
|
+
readonly isRebaseInProgress: () => Effect.Effect<boolean>;
|
|
31
|
+
readonly commitAmend: (options?: { edit?: boolean }) => Effect.Effect<void, GitError>;
|
|
29
32
|
readonly fetch: (remote?: string) => Effect.Effect<void, GitError>;
|
|
33
|
+
readonly deleteRemoteBranch: (branch: string) => Effect.Effect<void, GitError>;
|
|
30
34
|
}
|
|
31
35
|
>()("@cvr/stacked/services/Git/GitService") {
|
|
32
36
|
static layer: Layer.Layer<GitService> = Layer.sync(GitService, () => {
|
|
33
37
|
const run = Effect.fn("git.run")(function* (args: readonly string[]) {
|
|
34
|
-
const proc =
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
const proc = yield* Effect.sync(() =>
|
|
39
|
+
Bun.spawn(["git", ...args], {
|
|
40
|
+
stdout: "pipe",
|
|
41
|
+
stderr: "pipe",
|
|
42
|
+
}),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const exitCode = yield* Effect.tryPromise({
|
|
46
|
+
try: () => proc.exited,
|
|
47
|
+
catch: (e) =>
|
|
48
|
+
new GitError({ message: `Process failed: ${e}`, command: `git ${args.join(" ")}` }),
|
|
49
|
+
}).pipe(
|
|
50
|
+
Effect.onInterrupt(() =>
|
|
51
|
+
Effect.sync(() => {
|
|
52
|
+
proc.kill();
|
|
53
|
+
}),
|
|
54
|
+
),
|
|
55
|
+
);
|
|
56
|
+
const stdout = yield* Effect.tryPromise({
|
|
57
|
+
try: () => new Response(proc.stdout).text(),
|
|
58
|
+
catch: (e) =>
|
|
59
|
+
new GitError({
|
|
60
|
+
message: `Failed to read stdout: ${e}`,
|
|
61
|
+
command: `git ${args.join(" ")}`,
|
|
62
|
+
}),
|
|
63
|
+
});
|
|
64
|
+
const stderr = yield* Effect.tryPromise({
|
|
65
|
+
try: () => new Response(proc.stderr).text(),
|
|
66
|
+
catch: (e) =>
|
|
67
|
+
new GitError({
|
|
68
|
+
message: `Failed to read stderr: ${e}`,
|
|
69
|
+
command: `git ${args.join(" ")}`,
|
|
70
|
+
}),
|
|
37
71
|
});
|
|
38
|
-
const exitCode = yield* Effect.promise(() => proc.exited);
|
|
39
|
-
const stdout = yield* Effect.promise(() => new Response(proc.stdout).text());
|
|
40
|
-
const stderr = yield* Effect.promise(() => new Response(proc.stderr).text());
|
|
41
72
|
|
|
42
73
|
if (exitCode !== 0) {
|
|
43
74
|
return yield* new GitError({
|
|
@@ -49,10 +80,25 @@ export class GitService extends ServiceMap.Service<
|
|
|
49
80
|
});
|
|
50
81
|
|
|
51
82
|
return {
|
|
52
|
-
currentBranch: () =>
|
|
83
|
+
currentBranch: () =>
|
|
84
|
+
run(["rev-parse", "--abbrev-ref", "HEAD"]).pipe(
|
|
85
|
+
Effect.filterOrFail(
|
|
86
|
+
(branch) => branch !== "HEAD",
|
|
87
|
+
() =>
|
|
88
|
+
new GitError({
|
|
89
|
+
message: "HEAD is detached — checkout a branch first",
|
|
90
|
+
command: "git rev-parse --abbrev-ref HEAD",
|
|
91
|
+
}),
|
|
92
|
+
),
|
|
93
|
+
),
|
|
53
94
|
|
|
54
95
|
listBranches: () =>
|
|
55
|
-
run([
|
|
96
|
+
run([
|
|
97
|
+
"for-each-ref",
|
|
98
|
+
"--sort=-committerdate",
|
|
99
|
+
"--format=%(refname:short)",
|
|
100
|
+
"refs/heads",
|
|
101
|
+
]).pipe(
|
|
56
102
|
Effect.map((output) =>
|
|
57
103
|
output
|
|
58
104
|
.split("\n")
|
|
@@ -62,9 +108,9 @@ export class GitService extends ServiceMap.Service<
|
|
|
62
108
|
),
|
|
63
109
|
|
|
64
110
|
branchExists: (name) =>
|
|
65
|
-
run(["rev-parse", "--verify", name]).pipe(
|
|
111
|
+
run(["rev-parse", "--verify", `refs/heads/${name}`]).pipe(
|
|
66
112
|
Effect.as(true),
|
|
67
|
-
Effect.
|
|
113
|
+
Effect.catchTag("GitError", () => Effect.succeed(false)),
|
|
68
114
|
),
|
|
69
115
|
|
|
70
116
|
createBranch: (name, from) => {
|
|
@@ -73,15 +119,21 @@ export class GitService extends ServiceMap.Service<
|
|
|
73
119
|
},
|
|
74
120
|
|
|
75
121
|
deleteBranch: (name, force) =>
|
|
76
|
-
run(["branch", force === true ? "-D" : "-d", name]).pipe(Effect.asVoid),
|
|
122
|
+
run(["branch", force === true ? "-D" : "-d", "--", name]).pipe(Effect.asVoid),
|
|
77
123
|
|
|
78
124
|
checkout: (name) => run(["checkout", name]).pipe(Effect.asVoid),
|
|
79
125
|
|
|
80
126
|
rebase: (onto) => run(["rebase", onto]).pipe(Effect.asVoid),
|
|
81
127
|
|
|
128
|
+
rebaseOnto: (branch, newBase, oldBase) =>
|
|
129
|
+
run(["rebase", "--onto", newBase, oldBase, branch]).pipe(Effect.asVoid),
|
|
130
|
+
|
|
131
|
+
rebaseAbort: () => run(["rebase", "--abort"]).pipe(Effect.asVoid),
|
|
132
|
+
|
|
82
133
|
push: (branch, options) => {
|
|
83
|
-
const args = ["push", "-u", "origin"
|
|
134
|
+
const args = ["push", "-u", "origin"];
|
|
84
135
|
if (options?.force === true) args.splice(1, 0, "--force-with-lease");
|
|
136
|
+
args.push(branch);
|
|
85
137
|
return run(args).pipe(Effect.asVoid);
|
|
86
138
|
},
|
|
87
139
|
|
|
@@ -92,27 +144,37 @@ export class GitService extends ServiceMap.Service<
|
|
|
92
144
|
return run(args);
|
|
93
145
|
},
|
|
94
146
|
|
|
95
|
-
mergeBase: (a, b) => run(["merge-base", a, b]),
|
|
96
|
-
|
|
97
147
|
isClean: () => run(["status", "--porcelain"]).pipe(Effect.map((r) => r === "")),
|
|
98
148
|
|
|
99
149
|
revParse: (ref) => run(["rev-parse", ref]),
|
|
100
150
|
|
|
101
|
-
diff: (a, b, options) => {
|
|
102
|
-
const args = ["diff", a, b];
|
|
103
|
-
if (options?.stat === true) args.push("--stat");
|
|
104
|
-
return run(args);
|
|
105
|
-
},
|
|
106
|
-
|
|
107
151
|
isAncestor: (ancestor, descendant) =>
|
|
108
152
|
run(["merge-base", "--is-ancestor", ancestor, descendant]).pipe(
|
|
109
153
|
Effect.as(true),
|
|
154
|
+
Effect.catchTag("GitError", () => Effect.succeed(false)),
|
|
155
|
+
),
|
|
156
|
+
|
|
157
|
+
mergeBase: (a, b) => run(["merge-base", a, b]),
|
|
158
|
+
|
|
159
|
+
isRebaseInProgress: () =>
|
|
160
|
+
run(["rev-parse", "--git-dir"]).pipe(
|
|
161
|
+
Effect.map(
|
|
162
|
+
(gitDir) =>
|
|
163
|
+
existsSync(`${gitDir}/rebase-merge`) || existsSync(`${gitDir}/rebase-apply`),
|
|
164
|
+
),
|
|
110
165
|
Effect.catch(() => Effect.succeed(false)),
|
|
111
166
|
),
|
|
112
167
|
|
|
113
|
-
|
|
168
|
+
commitAmend: (options) => {
|
|
169
|
+
const args = ["commit", "--amend"];
|
|
170
|
+
if (options?.edit !== true) args.push("--no-edit");
|
|
171
|
+
return run(args).pipe(Effect.asVoid);
|
|
172
|
+
},
|
|
114
173
|
|
|
115
174
|
fetch: (remote) => run(["fetch", remote ?? "origin"]).pipe(Effect.asVoid),
|
|
175
|
+
|
|
176
|
+
deleteRemoteBranch: (branch) =>
|
|
177
|
+
run(["push", "origin", "--delete", branch]).pipe(Effect.asVoid),
|
|
116
178
|
};
|
|
117
179
|
});
|
|
118
180
|
|
|
@@ -125,15 +187,18 @@ export class GitService extends ServiceMap.Service<
|
|
|
125
187
|
deleteBranch: () => Effect.void,
|
|
126
188
|
checkout: () => Effect.void,
|
|
127
189
|
rebase: () => Effect.void,
|
|
190
|
+
rebaseOnto: () => Effect.void,
|
|
191
|
+
rebaseAbort: () => Effect.void,
|
|
128
192
|
push: () => Effect.void,
|
|
129
193
|
log: () => Effect.succeed(""),
|
|
130
|
-
mergeBase: () => Effect.succeed("abc123"),
|
|
131
194
|
isClean: () => Effect.succeed(true),
|
|
132
195
|
revParse: () => Effect.succeed("abc123"),
|
|
133
|
-
diff: () => Effect.succeed(""),
|
|
134
196
|
isAncestor: () => Effect.succeed(true),
|
|
135
|
-
|
|
197
|
+
mergeBase: () => Effect.succeed("abc123"),
|
|
198
|
+
isRebaseInProgress: () => Effect.succeed(false),
|
|
199
|
+
commitAmend: () => Effect.void,
|
|
136
200
|
fetch: () => Effect.void,
|
|
201
|
+
deleteRemoteBranch: () => Effect.void,
|
|
137
202
|
...impl,
|
|
138
203
|
});
|
|
139
204
|
}
|
package/src/services/GitHub.ts
CHANGED
|
@@ -6,6 +6,7 @@ const GhPrResponse = Schema.Struct({
|
|
|
6
6
|
url: Schema.String,
|
|
7
7
|
state: Schema.String,
|
|
8
8
|
baseRefName: Schema.String,
|
|
9
|
+
body: Schema.NullOr(Schema.String),
|
|
9
10
|
});
|
|
10
11
|
|
|
11
12
|
export class GitHubService extends ServiceMap.Service<
|
|
@@ -27,7 +28,7 @@ export class GitHubService extends ServiceMap.Service<
|
|
|
27
28
|
readonly getPR: (
|
|
28
29
|
branch: string,
|
|
29
30
|
) => Effect.Effect<
|
|
30
|
-
{ number: number; url: string; state: string; base: string } | null,
|
|
31
|
+
{ number: number; url: string; state: string; base: string; body: string | null } | null,
|
|
31
32
|
GitHubError
|
|
32
33
|
>;
|
|
33
34
|
readonly isGhInstalled: () => Effect.Effect<boolean>;
|
|
@@ -35,13 +36,40 @@ export class GitHubService extends ServiceMap.Service<
|
|
|
35
36
|
>()("@cvr/stacked/services/GitHub/GitHubService") {
|
|
36
37
|
static layer: Layer.Layer<GitHubService> = Layer.sync(GitHubService, () => {
|
|
37
38
|
const run = Effect.fn("gh.run")(function* (args: readonly string[]) {
|
|
38
|
-
const proc =
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
const proc = yield* Effect.sync(() =>
|
|
40
|
+
Bun.spawn(["gh", ...args], {
|
|
41
|
+
stdout: "pipe",
|
|
42
|
+
stderr: "pipe",
|
|
43
|
+
}),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const exitCode = yield* Effect.tryPromise({
|
|
47
|
+
try: () => proc.exited,
|
|
48
|
+
catch: (e) =>
|
|
49
|
+
new GitHubError({ message: `Process failed: ${e}`, command: `gh ${args.join(" ")}` }),
|
|
50
|
+
}).pipe(
|
|
51
|
+
Effect.onInterrupt(() =>
|
|
52
|
+
Effect.sync(() => {
|
|
53
|
+
proc.kill();
|
|
54
|
+
}),
|
|
55
|
+
),
|
|
56
|
+
);
|
|
57
|
+
const stdout = yield* Effect.tryPromise({
|
|
58
|
+
try: () => new Response(proc.stdout).text(),
|
|
59
|
+
catch: (e) =>
|
|
60
|
+
new GitHubError({
|
|
61
|
+
message: `Failed to read stdout: ${e}`,
|
|
62
|
+
command: `gh ${args.join(" ")}`,
|
|
63
|
+
}),
|
|
64
|
+
});
|
|
65
|
+
const stderr = yield* Effect.tryPromise({
|
|
66
|
+
try: () => new Response(proc.stderr).text(),
|
|
67
|
+
catch: (e) =>
|
|
68
|
+
new GitHubError({
|
|
69
|
+
message: `Failed to read stderr: ${e}`,
|
|
70
|
+
command: `gh ${args.join(" ")}`,
|
|
71
|
+
}),
|
|
41
72
|
});
|
|
42
|
-
const exitCode = yield* Effect.promise(() => proc.exited);
|
|
43
|
-
const stdout = yield* Effect.promise(() => new Response(proc.stdout).text());
|
|
44
|
-
const stderr = yield* Effect.promise(() => new Response(proc.stderr).text());
|
|
45
73
|
|
|
46
74
|
if (exitCode !== 0) {
|
|
47
75
|
return yield* new GitHubError({
|
|
@@ -87,31 +115,41 @@ export class GitHubService extends ServiceMap.Service<
|
|
|
87
115
|
"view",
|
|
88
116
|
branch,
|
|
89
117
|
"--json",
|
|
90
|
-
"number,url,state,baseRefName",
|
|
91
|
-
]).pipe(Effect.
|
|
118
|
+
"number,url,state,baseRefName,body",
|
|
119
|
+
]).pipe(Effect.catchTag("GitHubError", () => Effect.succeed(null)));
|
|
92
120
|
|
|
93
121
|
if (result === null) return null;
|
|
94
122
|
const data = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(GhPrResponse))(
|
|
95
123
|
result,
|
|
96
|
-
).pipe(Effect.
|
|
124
|
+
).pipe(Effect.catchTag("SchemaError", () => Effect.succeed(null)));
|
|
97
125
|
if (data === null) return null;
|
|
98
126
|
return {
|
|
99
127
|
number: data.number,
|
|
100
128
|
url: data.url,
|
|
101
129
|
state: data.state,
|
|
102
130
|
base: data.baseRefName,
|
|
131
|
+
body: data.body,
|
|
103
132
|
};
|
|
104
133
|
}),
|
|
105
134
|
|
|
106
135
|
isGhInstalled: () =>
|
|
107
|
-
Effect.
|
|
108
|
-
try
|
|
109
|
-
Bun.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
})
|
|
136
|
+
Effect.try({
|
|
137
|
+
try: () =>
|
|
138
|
+
Bun.spawn(["gh", "--version"], {
|
|
139
|
+
stdout: "ignore",
|
|
140
|
+
stderr: "ignore",
|
|
141
|
+
}),
|
|
142
|
+
catch: () => null,
|
|
143
|
+
}).pipe(
|
|
144
|
+
Effect.andThen((proc) => {
|
|
145
|
+
if (proc === null) return Effect.succeed(false);
|
|
146
|
+
return Effect.tryPromise({
|
|
147
|
+
try: () => proc.exited,
|
|
148
|
+
catch: () => -1,
|
|
149
|
+
}).pipe(Effect.map((code) => code === 0));
|
|
150
|
+
}),
|
|
151
|
+
Effect.catch(() => Effect.succeed(false)),
|
|
152
|
+
),
|
|
115
153
|
};
|
|
116
154
|
});
|
|
117
155
|
|