@cvr/stacked 0.1.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 +81 -0
- package/bin/stacked +0 -0
- package/package.json +46 -0
- package/scripts/build.ts +53 -0
- package/skills/stacked/SKILL.md +181 -0
- package/src/commands/adopt.ts +39 -0
- package/src/commands/bottom.ts +29 -0
- package/src/commands/checkout.ts +15 -0
- package/src/commands/create.ts +46 -0
- package/src/commands/delete.ts +55 -0
- package/src/commands/index.ts +34 -0
- package/src/commands/list.ts +50 -0
- package/src/commands/log.ts +34 -0
- package/src/commands/restack.ts +40 -0
- package/src/commands/submit.ts +68 -0
- package/src/commands/sync.ts +42 -0
- package/src/commands/top.ts +29 -0
- package/src/commands/trunk.ts +21 -0
- package/src/errors/index.ts +15 -0
- package/src/main.ts +22 -0
- package/src/services/Git.ts +127 -0
- package/src/services/GitHub.ts +126 -0
- package/src/services/Stack.ts +299 -0
- package/tsconfig.json +64 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { Effect, Layer, Ref, Schema, ServiceMap } from "effect";
|
|
2
|
+
import type { GitError } from "../errors/index.js";
|
|
3
|
+
import { StackError } from "../errors/index.js";
|
|
4
|
+
import { GitService } from "./Git.js";
|
|
5
|
+
|
|
6
|
+
export const StackSchema = Schema.Struct({
|
|
7
|
+
branches: Schema.Array(Schema.String),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export const StackFileSchema = Schema.Struct({
|
|
11
|
+
version: Schema.Literal(1),
|
|
12
|
+
trunk: Schema.String,
|
|
13
|
+
stacks: Schema.Record(Schema.String, StackSchema),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export type Stack = typeof StackSchema.Type;
|
|
17
|
+
export type StackFile = typeof StackFileSchema.Type;
|
|
18
|
+
|
|
19
|
+
const emptyStackFile: StackFile = { version: 1, trunk: "main", stacks: {} };
|
|
20
|
+
|
|
21
|
+
export class StackService extends ServiceMap.Service<
|
|
22
|
+
StackService,
|
|
23
|
+
{
|
|
24
|
+
readonly load: () => Effect.Effect<StackFile, StackError>;
|
|
25
|
+
readonly save: (data: StackFile) => Effect.Effect<void, StackError>;
|
|
26
|
+
readonly currentStack: () => Effect.Effect<
|
|
27
|
+
{ name: string; stack: Stack } | null,
|
|
28
|
+
StackError | GitError
|
|
29
|
+
>;
|
|
30
|
+
readonly addBranch: (
|
|
31
|
+
stackName: string,
|
|
32
|
+
branch: string,
|
|
33
|
+
after?: string,
|
|
34
|
+
) => Effect.Effect<void, StackError>;
|
|
35
|
+
readonly removeBranch: (stackName: string, branch: string) => Effect.Effect<void, StackError>;
|
|
36
|
+
readonly createStack: (name: string, branches: string[]) => Effect.Effect<void, StackError>;
|
|
37
|
+
readonly getTrunk: () => Effect.Effect<string, StackError>;
|
|
38
|
+
readonly setTrunk: (name: string) => Effect.Effect<void, StackError>;
|
|
39
|
+
readonly parentOf: (branch: string) => Effect.Effect<string, StackError>;
|
|
40
|
+
readonly childrenOf: (branch: string) => Effect.Effect<string[], StackError>;
|
|
41
|
+
}
|
|
42
|
+
>()("@cvr/stacked/services/Stack/StackService") {
|
|
43
|
+
static layer: Layer.Layer<StackService, never, GitService> = Layer.effect(
|
|
44
|
+
StackService,
|
|
45
|
+
Effect.gen(function* () {
|
|
46
|
+
const git = yield* GitService;
|
|
47
|
+
|
|
48
|
+
const stackFilePath = Effect.fn("stackFilePath")(function* () {
|
|
49
|
+
const gitDir = yield* git
|
|
50
|
+
.revParse("--git-dir")
|
|
51
|
+
.pipe(
|
|
52
|
+
Effect.mapError(
|
|
53
|
+
(e) => new StackError({ message: `Not a git repository: ${e.message}` }),
|
|
54
|
+
),
|
|
55
|
+
);
|
|
56
|
+
return `${gitDir}/stacked.json`;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const StackFileJson = Schema.fromJsonString(StackFileSchema);
|
|
60
|
+
const decodeStackFile = Schema.decodeUnknownEffect(StackFileJson);
|
|
61
|
+
const encodeStackFile = Schema.encodeEffect(StackFileJson);
|
|
62
|
+
|
|
63
|
+
const load = Effect.fn("StackService.load")(function* () {
|
|
64
|
+
const path = yield* stackFilePath();
|
|
65
|
+
const file = Bun.file(path);
|
|
66
|
+
const exists = yield* Effect.promise(() => file.exists());
|
|
67
|
+
if (!exists) return emptyStackFile;
|
|
68
|
+
const text = yield* Effect.promise(() => file.text());
|
|
69
|
+
return yield* decodeStackFile(text).pipe(
|
|
70
|
+
Effect.catch(() => Effect.succeed(emptyStackFile)),
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const save = Effect.fn("StackService.save")(function* (data: StackFile) {
|
|
75
|
+
const path = yield* stackFilePath();
|
|
76
|
+
const text = yield* encodeStackFile(data).pipe(
|
|
77
|
+
Effect.mapError(() => new StackError({ message: `Failed to encode stack data` })),
|
|
78
|
+
);
|
|
79
|
+
yield* Effect.promise(() => Bun.write(path, text + "\n")).pipe(
|
|
80
|
+
Effect.mapError(() => new StackError({ message: `Failed to write ${path}` })),
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const findBranchStack = (data: StackFile, branch: string) => {
|
|
85
|
+
for (const [name, stack] of Object.entries(data.stacks)) {
|
|
86
|
+
if (stack.branches.includes(branch)) {
|
|
87
|
+
return { name, stack };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
load: () => load(),
|
|
95
|
+
save: (data) => save(data),
|
|
96
|
+
|
|
97
|
+
currentStack: Effect.fn("StackService.currentStack")(function* () {
|
|
98
|
+
const branch = yield* git.currentBranch();
|
|
99
|
+
const data = yield* load();
|
|
100
|
+
return findBranchStack(data, branch);
|
|
101
|
+
}),
|
|
102
|
+
|
|
103
|
+
addBranch: Effect.fn("StackService.addBranch")(function* (
|
|
104
|
+
stackName: string,
|
|
105
|
+
branch: string,
|
|
106
|
+
after?: string,
|
|
107
|
+
) {
|
|
108
|
+
const data = yield* load();
|
|
109
|
+
const stack = data.stacks[stackName];
|
|
110
|
+
if (stack === undefined) {
|
|
111
|
+
return yield* new StackError({ message: `Stack "${stackName}" not found` });
|
|
112
|
+
}
|
|
113
|
+
const branches = [...stack.branches];
|
|
114
|
+
if (after !== undefined) {
|
|
115
|
+
const idx = branches.indexOf(after);
|
|
116
|
+
if (idx === -1) {
|
|
117
|
+
return yield* new StackError({
|
|
118
|
+
message: `Branch "${after}" not in stack "${stackName}"`,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
branches.splice(idx + 1, 0, branch);
|
|
122
|
+
} else {
|
|
123
|
+
branches.push(branch);
|
|
124
|
+
}
|
|
125
|
+
yield* save({
|
|
126
|
+
...data,
|
|
127
|
+
stacks: { ...data.stacks, [stackName]: { branches } },
|
|
128
|
+
});
|
|
129
|
+
}),
|
|
130
|
+
|
|
131
|
+
removeBranch: Effect.fn("StackService.removeBranch")(function* (
|
|
132
|
+
stackName: string,
|
|
133
|
+
branch: string,
|
|
134
|
+
) {
|
|
135
|
+
const data = yield* load();
|
|
136
|
+
const stack = data.stacks[stackName];
|
|
137
|
+
if (stack === undefined) {
|
|
138
|
+
return yield* new StackError({ message: `Stack "${stackName}" not found` });
|
|
139
|
+
}
|
|
140
|
+
const branches = stack.branches.filter((b) => b !== branch);
|
|
141
|
+
if (branches.length === 0) {
|
|
142
|
+
const { [stackName]: _, ...rest } = data.stacks;
|
|
143
|
+
yield* save({ ...data, stacks: rest });
|
|
144
|
+
} else {
|
|
145
|
+
yield* save({
|
|
146
|
+
...data,
|
|
147
|
+
stacks: { ...data.stacks, [stackName]: { branches } },
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}),
|
|
151
|
+
|
|
152
|
+
createStack: Effect.fn("StackService.createStack")(function* (
|
|
153
|
+
name: string,
|
|
154
|
+
branches: string[],
|
|
155
|
+
) {
|
|
156
|
+
const data = yield* load();
|
|
157
|
+
if (data.stacks[name] !== undefined) {
|
|
158
|
+
return yield* new StackError({ message: `Stack "${name}" already exists` });
|
|
159
|
+
}
|
|
160
|
+
yield* save({
|
|
161
|
+
...data,
|
|
162
|
+
stacks: { ...data.stacks, [name]: { branches } },
|
|
163
|
+
});
|
|
164
|
+
}),
|
|
165
|
+
|
|
166
|
+
getTrunk: Effect.fn("StackService.getTrunk")(function* () {
|
|
167
|
+
const data = yield* load();
|
|
168
|
+
return data.trunk;
|
|
169
|
+
}),
|
|
170
|
+
|
|
171
|
+
setTrunk: Effect.fn("StackService.setTrunk")(function* (name: string) {
|
|
172
|
+
const data = yield* load();
|
|
173
|
+
yield* save({ ...data, trunk: name });
|
|
174
|
+
}),
|
|
175
|
+
|
|
176
|
+
parentOf: Effect.fn("StackService.parentOf")(function* (branch: string) {
|
|
177
|
+
const data = yield* load();
|
|
178
|
+
for (const stack of Object.values(data.stacks)) {
|
|
179
|
+
const idx = stack.branches.indexOf(branch);
|
|
180
|
+
if (idx === 0) return data.trunk;
|
|
181
|
+
if (idx > 0) return stack.branches[idx - 1] ?? data.trunk;
|
|
182
|
+
}
|
|
183
|
+
return yield* new StackError({ message: `Branch "${branch}" not found in any stack` });
|
|
184
|
+
}),
|
|
185
|
+
|
|
186
|
+
childrenOf: Effect.fn("StackService.childrenOf")(function* (branch: string) {
|
|
187
|
+
const data = yield* load();
|
|
188
|
+
const children: string[] = [];
|
|
189
|
+
for (const stack of Object.values(data.stacks)) {
|
|
190
|
+
const idx = stack.branches.indexOf(branch);
|
|
191
|
+
const child = stack.branches[idx + 1];
|
|
192
|
+
if (idx !== -1 && child !== undefined) {
|
|
193
|
+
children.push(child);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return children;
|
|
197
|
+
}),
|
|
198
|
+
};
|
|
199
|
+
}),
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
static layerTest = (data?: StackFile) => {
|
|
203
|
+
const initial = data ?? emptyStackFile;
|
|
204
|
+
return Layer.effect(
|
|
205
|
+
StackService,
|
|
206
|
+
Effect.gen(function* () {
|
|
207
|
+
const ref = yield* Ref.make<StackFile>(initial);
|
|
208
|
+
|
|
209
|
+
const findBranchStack = (d: StackFile, branch: string) => {
|
|
210
|
+
for (const [name, stack] of Object.entries(d.stacks)) {
|
|
211
|
+
if (stack.branches.includes(branch)) {
|
|
212
|
+
return { name, stack };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return null;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
load: () => Ref.get(ref),
|
|
220
|
+
save: (d) => Ref.set(ref, d),
|
|
221
|
+
|
|
222
|
+
currentStack: Effect.fn("test.currentStack")(function* () {
|
|
223
|
+
const d = yield* Ref.get(ref);
|
|
224
|
+
return findBranchStack(d, "test-branch");
|
|
225
|
+
}),
|
|
226
|
+
|
|
227
|
+
addBranch: Effect.fn("test.addBranch")(function* (
|
|
228
|
+
stackName: string,
|
|
229
|
+
branch: string,
|
|
230
|
+
after?: string,
|
|
231
|
+
) {
|
|
232
|
+
yield* Ref.update(ref, (d) => {
|
|
233
|
+
const stack = d.stacks[stackName];
|
|
234
|
+
if (stack === undefined) return d;
|
|
235
|
+
const branches = [...stack.branches];
|
|
236
|
+
if (after !== undefined) {
|
|
237
|
+
const idx = branches.indexOf(after);
|
|
238
|
+
if (idx !== -1) branches.splice(idx + 1, 0, branch);
|
|
239
|
+
else branches.push(branch);
|
|
240
|
+
} else {
|
|
241
|
+
branches.push(branch);
|
|
242
|
+
}
|
|
243
|
+
return { ...d, stacks: { ...d.stacks, [stackName]: { branches } } };
|
|
244
|
+
});
|
|
245
|
+
}),
|
|
246
|
+
|
|
247
|
+
removeBranch: Effect.fn("test.removeBranch")(function* (
|
|
248
|
+
stackName: string,
|
|
249
|
+
branch: string,
|
|
250
|
+
) {
|
|
251
|
+
yield* Ref.update(ref, (d) => {
|
|
252
|
+
const stack = d.stacks[stackName];
|
|
253
|
+
if (stack === undefined) return d;
|
|
254
|
+
const branches = stack.branches.filter((b) => b !== branch);
|
|
255
|
+
if (branches.length === 0) {
|
|
256
|
+
const { [stackName]: _, ...rest } = d.stacks;
|
|
257
|
+
return { ...d, stacks: rest };
|
|
258
|
+
}
|
|
259
|
+
return { ...d, stacks: { ...d.stacks, [stackName]: { branches } } };
|
|
260
|
+
});
|
|
261
|
+
}),
|
|
262
|
+
|
|
263
|
+
createStack: Effect.fn("test.createStack")(function* (name: string, branches: string[]) {
|
|
264
|
+
yield* Ref.update(ref, (d) => ({
|
|
265
|
+
...d,
|
|
266
|
+
stacks: { ...d.stacks, [name]: { branches } },
|
|
267
|
+
}));
|
|
268
|
+
}),
|
|
269
|
+
|
|
270
|
+
getTrunk: () => Ref.get(ref).pipe(Effect.map((d) => d.trunk)),
|
|
271
|
+
setTrunk: (name: string) => Ref.update(ref, (d) => ({ ...d, trunk: name })),
|
|
272
|
+
|
|
273
|
+
parentOf: Effect.fn("test.parentOf")(function* (branch: string) {
|
|
274
|
+
const d = yield* Ref.get(ref);
|
|
275
|
+
for (const stack of Object.values(d.stacks)) {
|
|
276
|
+
const idx = stack.branches.indexOf(branch);
|
|
277
|
+
if (idx === 0) return d.trunk;
|
|
278
|
+
if (idx > 0) return stack.branches[idx - 1] ?? d.trunk;
|
|
279
|
+
}
|
|
280
|
+
return yield* new StackError({ message: `Branch "${branch}" not found in any stack` });
|
|
281
|
+
}),
|
|
282
|
+
|
|
283
|
+
childrenOf: Effect.fn("test.childrenOf")(function* (branch: string) {
|
|
284
|
+
const d = yield* Ref.get(ref);
|
|
285
|
+
const children: string[] = [];
|
|
286
|
+
for (const stack of Object.values(d.stacks)) {
|
|
287
|
+
const idx = stack.branches.indexOf(branch);
|
|
288
|
+
const child = stack.branches[idx + 1];
|
|
289
|
+
if (idx !== -1 && child !== undefined) {
|
|
290
|
+
children.push(child);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return children;
|
|
294
|
+
}),
|
|
295
|
+
};
|
|
296
|
+
}),
|
|
297
|
+
);
|
|
298
|
+
};
|
|
299
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"strict": true,
|
|
4
|
+
"noUncheckedIndexedAccess": true,
|
|
5
|
+
"noFallthroughCasesInSwitch": true,
|
|
6
|
+
"noImplicitOverride": true,
|
|
7
|
+
"noPropertyAccessFromIndexSignature": true,
|
|
8
|
+
"target": "ESNext",
|
|
9
|
+
"module": "ESNext",
|
|
10
|
+
"moduleResolution": "bundler",
|
|
11
|
+
"moduleDetection": "force",
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"noEmit": true,
|
|
15
|
+
"plugins": [
|
|
16
|
+
{
|
|
17
|
+
"name": "@effect/language-service",
|
|
18
|
+
"diagnostics": true,
|
|
19
|
+
"diagnosticsName": true,
|
|
20
|
+
"diagnosticSeverity": {
|
|
21
|
+
"anyUnknownInErrorContext": "error",
|
|
22
|
+
"deterministicKeys": "warning",
|
|
23
|
+
"importFromBarrel": "warning",
|
|
24
|
+
"instanceOfSchema": "warning",
|
|
25
|
+
"missedPipeableOpportunity": "warning",
|
|
26
|
+
"missingEffectServiceDependency": "warning",
|
|
27
|
+
"schemaUnionOfLiterals": "warning",
|
|
28
|
+
"strictBooleanExpressions": "warning",
|
|
29
|
+
"strictEffectProvide": "warning",
|
|
30
|
+
"catchAllToMapError": "warning",
|
|
31
|
+
"catchUnfailableEffect": "warning",
|
|
32
|
+
"effectFnOpportunity": "warning",
|
|
33
|
+
"effectMapVoid": "warning",
|
|
34
|
+
"effectSucceedWithVoid": "warning",
|
|
35
|
+
"leakingRequirements": "warning",
|
|
36
|
+
"preferSchemaOverJson": "warning",
|
|
37
|
+
"redundantSchemaTagIdentifier": "warning",
|
|
38
|
+
"returnEffectInGen": "warning",
|
|
39
|
+
"runEffectInsideEffect": "error",
|
|
40
|
+
"schemaStructWithTag": "warning",
|
|
41
|
+
"schemaSyncInEffect": "warning",
|
|
42
|
+
"tryCatchInEffectGen": "warning",
|
|
43
|
+
"unnecessaryEffectGen": "warning",
|
|
44
|
+
"unnecessaryFailYieldableError": "warning",
|
|
45
|
+
"unnecessaryPipe": "warning",
|
|
46
|
+
"unnecessaryPipeChain": "warning"
|
|
47
|
+
},
|
|
48
|
+
"keyPatterns": [
|
|
49
|
+
{
|
|
50
|
+
"target": "service",
|
|
51
|
+
"pattern": "default",
|
|
52
|
+
"skipLeadingPath": ["src/"]
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"target": "error",
|
|
56
|
+
"pattern": "default",
|
|
57
|
+
"skipLeadingPath": ["src/"]
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
]
|
|
62
|
+
},
|
|
63
|
+
"include": ["src", "tests", "scripts"]
|
|
64
|
+
}
|