runcastle 1.0.1 → 1.0.5
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/bin/runcastle.js +84 -19
- package/drizzle/0006_living_typhoid_mary.sql +1 -0
- package/drizzle/meta/0006_snapshot.json +603 -0
- package/drizzle/meta/_journal.json +8 -1
- package/index.js +84 -19
- package/package.json +11 -1
- package/web/assets/{index-YBuT7tTz.js → index-CRhFvrAu.js} +2 -2
- package/web/assets/{index-CKZ5Z7vc.css → index-ClNOSIVg.css} +1 -1
- package/web/index.html +2 -2
package/index.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
var __defProp = Object.defineProperty;
|
|
3
|
+
var __returnValue = (v) => v;
|
|
4
|
+
function __exportSetter(name, newValue) {
|
|
5
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
6
|
+
}
|
|
3
7
|
var __export = (target, all) => {
|
|
4
8
|
for (var name in all)
|
|
5
9
|
__defProp(target, name, {
|
|
6
10
|
get: all[name],
|
|
7
11
|
enumerable: true,
|
|
8
12
|
configurable: true,
|
|
9
|
-
set: (
|
|
13
|
+
set: __exportSetter.bind(all, name)
|
|
10
14
|
});
|
|
11
15
|
};
|
|
12
16
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
@@ -225,6 +229,7 @@ var init_schemas = __esm(() => {
|
|
|
225
229
|
mapped: z2.boolean(),
|
|
226
230
|
phase: Phase,
|
|
227
231
|
branch: z2.string(),
|
|
232
|
+
baseBranch: z2.string().optional(),
|
|
228
233
|
status: FeatureStatus,
|
|
229
234
|
createdAt: z2.number()
|
|
230
235
|
});
|
|
@@ -438,6 +443,7 @@ var init_db_schema = __esm(() => {
|
|
|
438
443
|
mapped: integer("mapped", { mode: "boolean" }).notNull().default(false),
|
|
439
444
|
phase: text("phase").notNull().$type(),
|
|
440
445
|
branch: text("branch").notNull(),
|
|
446
|
+
baseBranch: text("base_branch"),
|
|
441
447
|
status: text("status").notNull().$type(),
|
|
442
448
|
createdAt: integer("created_at").notNull()
|
|
443
449
|
});
|
|
@@ -1164,6 +1170,7 @@ function rowToFeature(row) {
|
|
|
1164
1170
|
mapped: row.mapped,
|
|
1165
1171
|
phase: row.phase,
|
|
1166
1172
|
branch: row.branch,
|
|
1173
|
+
baseBranch: row.baseBranch ?? undefined,
|
|
1167
1174
|
status: row.status,
|
|
1168
1175
|
createdAt: row.createdAt
|
|
1169
1176
|
};
|
|
@@ -1900,20 +1907,59 @@ async function detectMainBranch(repoPath) {
|
|
|
1900
1907
|
} catch {}
|
|
1901
1908
|
return "main";
|
|
1902
1909
|
}
|
|
1903
|
-
async function createFeatureBranch(project, slug) {
|
|
1910
|
+
async function createFeatureBranch(project, slug, base) {
|
|
1904
1911
|
const branch = featureBranch(slug);
|
|
1912
|
+
const from = base?.trim() || project.mainBranch;
|
|
1905
1913
|
const g = git(project.repoPath);
|
|
1906
1914
|
const branches = await g.branchLocal();
|
|
1907
1915
|
if (branches.all.includes(branch))
|
|
1908
1916
|
return branch;
|
|
1909
|
-
|
|
1917
|
+
if (!branches.all.includes(from)) {
|
|
1918
|
+
throw new Error(`base branch "${from}" does not exist in ${project.repoPath}`);
|
|
1919
|
+
}
|
|
1920
|
+
await g.raw(["branch", branch, from]);
|
|
1910
1921
|
return branch;
|
|
1911
1922
|
}
|
|
1923
|
+
async function listBranches(project) {
|
|
1924
|
+
const g = git(project.repoPath);
|
|
1925
|
+
const local = await g.branchLocal();
|
|
1926
|
+
const branches = local.all.filter((name) => !name.startsWith("feature/"));
|
|
1927
|
+
const localSet = new Set(local.all);
|
|
1928
|
+
let remoteBranches = [];
|
|
1929
|
+
try {
|
|
1930
|
+
const remote = await g.branch(["-r"]);
|
|
1931
|
+
remoteBranches = remote.all.filter((r) => !r.includes(" -> ") && !r.endsWith("/HEAD")).filter((r) => {
|
|
1932
|
+
const short = r.replace(/^[^/]+\//, "");
|
|
1933
|
+
return !short.startsWith("feature/") && !localSet.has(short);
|
|
1934
|
+
});
|
|
1935
|
+
} catch {}
|
|
1936
|
+
return { current: local.current, mainBranch: project.mainBranch, branches, remoteBranches };
|
|
1937
|
+
}
|
|
1938
|
+
async function resolveBaseBranch(project, base) {
|
|
1939
|
+
const g = git(project.repoPath);
|
|
1940
|
+
const local = await g.branchLocal();
|
|
1941
|
+
if (local.all.includes(base))
|
|
1942
|
+
return base;
|
|
1943
|
+
const remotes = (await g.getRemotes()).map((r) => r.name);
|
|
1944
|
+
for (const rem of remotes) {
|
|
1945
|
+
const prefix = `${rem}/`;
|
|
1946
|
+
if (!base.startsWith(prefix))
|
|
1947
|
+
continue;
|
|
1948
|
+
const name = base.slice(prefix.length);
|
|
1949
|
+
if (!name || name.endsWith("/HEAD"))
|
|
1950
|
+
break;
|
|
1951
|
+
if (local.all.includes(name))
|
|
1952
|
+
return name;
|
|
1953
|
+
await g.raw(["branch", "--track", name, base]);
|
|
1954
|
+
return name;
|
|
1955
|
+
}
|
|
1956
|
+
throw new Error(`base branch "${base}" does not exist in ${project.repoPath}`);
|
|
1957
|
+
}
|
|
1912
1958
|
async function ensureTalkWorktree(project, feature) {
|
|
1913
1959
|
const worktreePath = worktreeDir(project.id, feature.slug);
|
|
1914
1960
|
const branch = featureBranch(feature.slug);
|
|
1915
1961
|
const g = git(project.repoPath);
|
|
1916
|
-
await ensureBranchExists(g, branch, project.mainBranch);
|
|
1962
|
+
await ensureBranchExists(g, branch, feature.baseBranch ?? project.mainBranch);
|
|
1917
1963
|
if (await worktreeIsValid(g, worktreePath, branch))
|
|
1918
1964
|
return worktreePath;
|
|
1919
1965
|
mkdirSync2(dirname(worktreePath), { recursive: true });
|
|
@@ -2184,6 +2230,7 @@ async function testDrive(ctx, project, feature, action) {
|
|
|
2184
2230
|
return { ok: true, branch };
|
|
2185
2231
|
}
|
|
2186
2232
|
async function mergeFeature(project, feature) {
|
|
2233
|
+
const target = feature.baseBranch ?? project.mainBranch;
|
|
2187
2234
|
if (testDriveState) {
|
|
2188
2235
|
throw new GateError("Cannot merge while a test drive is active \u2014 stop it first");
|
|
2189
2236
|
}
|
|
@@ -2192,19 +2239,33 @@ async function mergeFeature(project, feature) {
|
|
|
2192
2239
|
if (porcelain !== "") {
|
|
2193
2240
|
throw new GateError("Cannot merge \u2014 working tree has uncommitted changes");
|
|
2194
2241
|
}
|
|
2242
|
+
const branches = await g.branchLocal();
|
|
2243
|
+
if (!branches.all.includes(target)) {
|
|
2244
|
+
throw new GateError(`Cannot merge \u2014 base branch "${target}" no longer exists`);
|
|
2245
|
+
}
|
|
2246
|
+
const previous = (await g.revparse(["--abbrev-ref", "HEAD"])).trim();
|
|
2195
2247
|
const branch = featureBranch(feature.slug);
|
|
2196
|
-
await g.checkout(
|
|
2248
|
+
await g.checkout(target);
|
|
2197
2249
|
try {
|
|
2198
2250
|
await g.merge(["--no-ff", branch]);
|
|
2199
|
-
|
|
2251
|
+
await restoreBranch(g, previous, target);
|
|
2252
|
+
return { ok: true, target };
|
|
2200
2253
|
} catch (e) {
|
|
2201
2254
|
if (await mergeInProgress(g)) {
|
|
2202
2255
|
await g.raw(["merge", "--abort"]);
|
|
2203
|
-
|
|
2256
|
+
await restoreBranch(g, previous, target);
|
|
2257
|
+
return { ok: false, conflict: true, target };
|
|
2204
2258
|
}
|
|
2205
2259
|
throw e instanceof Error ? e : new Error(errMsg(e));
|
|
2206
2260
|
}
|
|
2207
2261
|
}
|
|
2262
|
+
async function restoreBranch(g, previous, target) {
|
|
2263
|
+
if (previous === target || previous === "HEAD" || !previous)
|
|
2264
|
+
return;
|
|
2265
|
+
try {
|
|
2266
|
+
await g.checkout(previous);
|
|
2267
|
+
} catch {}
|
|
2268
|
+
}
|
|
2208
2269
|
async function mergeInProgress(g) {
|
|
2209
2270
|
try {
|
|
2210
2271
|
await g.raw(["rev-parse", "--verify", "--quiet", "MERGE_HEAD"]);
|
|
@@ -3118,7 +3179,8 @@ async function createFeature(ctx, input) {
|
|
|
3118
3179
|
const project = requireProjectById(ctx, input.projectId);
|
|
3119
3180
|
const slug = uniqueSlug(ctx, project.id, input.title);
|
|
3120
3181
|
const branch = `feature/${slug}`;
|
|
3121
|
-
const
|
|
3182
|
+
const requestedBase = input.baseBranch?.trim() || project.mainBranch;
|
|
3183
|
+
const { branchReady, baseBranch } = await ensureFeatureBranch(project, slug, requestedBase);
|
|
3122
3184
|
const row = {
|
|
3123
3185
|
id: newId("feat"),
|
|
3124
3186
|
projectId: project.id,
|
|
@@ -3129,6 +3191,7 @@ async function createFeature(ctx, input) {
|
|
|
3129
3191
|
mapped: input.mapped ?? false,
|
|
3130
3192
|
phase: "ideation",
|
|
3131
3193
|
branch,
|
|
3194
|
+
baseBranch,
|
|
3132
3195
|
status: "active",
|
|
3133
3196
|
createdAt: Date.now()
|
|
3134
3197
|
};
|
|
@@ -3136,8 +3199,8 @@ async function createFeature(ctx, input) {
|
|
|
3136
3199
|
const feature = rowToFeature(inserted);
|
|
3137
3200
|
emit(ctx, feature.id, {
|
|
3138
3201
|
type: "feature.created",
|
|
3139
|
-
message: branchReady ? `feature.created (${branch})` : "feature.created (branch pending)",
|
|
3140
|
-
data: { slug, branch, branchReady }
|
|
3202
|
+
message: branchReady ? `feature.created (${branch} \u2190 ${baseBranch})` : "feature.created (branch pending)",
|
|
3203
|
+
data: { slug, branch, baseBranch, branchReady }
|
|
3141
3204
|
});
|
|
3142
3205
|
scaffoldDocs(ctx, feature);
|
|
3143
3206
|
if (branchReady) {
|
|
@@ -3147,13 +3210,14 @@ async function createFeature(ctx, input) {
|
|
|
3147
3210
|
}
|
|
3148
3211
|
return feature;
|
|
3149
3212
|
}
|
|
3150
|
-
async function ensureFeatureBranch(project, slug) {
|
|
3213
|
+
async function ensureFeatureBranch(project, slug, requestedBase) {
|
|
3151
3214
|
try {
|
|
3152
|
-
await
|
|
3153
|
-
|
|
3215
|
+
const baseBranch = await resolveBaseBranch(project, requestedBase);
|
|
3216
|
+
await createFeatureBranch(project, slug, baseBranch);
|
|
3217
|
+
return { branchReady: true, baseBranch };
|
|
3154
3218
|
} catch (e) {
|
|
3155
3219
|
if (isNotImplemented(e))
|
|
3156
|
-
return { branchReady: false };
|
|
3220
|
+
return { branchReady: false, baseBranch: requestedBase };
|
|
3157
3221
|
throw e;
|
|
3158
3222
|
}
|
|
3159
3223
|
}
|
|
@@ -4382,7 +4446,8 @@ var init_feature = __esm(() => {
|
|
|
4382
4446
|
title: z6.string().min(1),
|
|
4383
4447
|
oneLiner: z6.string(),
|
|
4384
4448
|
size: FeatureSize,
|
|
4385
|
-
mapped: z6.boolean().optional()
|
|
4449
|
+
mapped: z6.boolean().optional(),
|
|
4450
|
+
baseBranch: z6.string().optional()
|
|
4386
4451
|
})).mutation(({ ctx, input }) => createFeature(ctx, input)),
|
|
4387
4452
|
list: publicProcedure.input(z6.object({ projectId: z6.string() })).query(({ ctx, input }) => list(ctx, input.projectId)),
|
|
4388
4453
|
get: publicProcedure.input(z6.object({ id: z6.string() })).query(({ ctx, input }) => getFeatureFull(ctx, input.id)),
|
|
@@ -4407,7 +4472,7 @@ var init_feature = __esm(() => {
|
|
|
4407
4472
|
}
|
|
4408
4473
|
const res = await mergeFeature(project, feature);
|
|
4409
4474
|
if (res.ok) {
|
|
4410
|
-
setPhase(ctx, input.featureId, "shipped", "feature.shipped", `merged to ${
|
|
4475
|
+
setPhase(ctx, input.featureId, "shipped", "feature.shipped", `merged to ${res.target}`);
|
|
4411
4476
|
setFeatureStatus(ctx, input.featureId, "shipped");
|
|
4412
4477
|
} else {
|
|
4413
4478
|
emit(ctx, input.featureId, {
|
|
@@ -4513,10 +4578,13 @@ var init_projects = __esm(() => {
|
|
|
4513
4578
|
import * as z7 from "zod";
|
|
4514
4579
|
var projectRouter;
|
|
4515
4580
|
var init_project = __esm(() => {
|
|
4581
|
+
init_git();
|
|
4516
4582
|
init_projects();
|
|
4583
|
+
init_repo();
|
|
4517
4584
|
init_context();
|
|
4518
4585
|
projectRouter = router({
|
|
4519
4586
|
list: publicProcedure.query(({ ctx }) => listProjects(ctx)),
|
|
4587
|
+
branches: publicProcedure.input(z7.object({ projectId: z7.string() })).query(({ ctx, input }) => listBranches(requireProjectById(ctx, input.projectId))),
|
|
4520
4588
|
open: publicProcedure.input(z7.object({ repoPath: z7.string().min(1) })).mutation(({ ctx, input }) => openProject(ctx, input.repoPath)),
|
|
4521
4589
|
close: publicProcedure.input(z7.object({ projectId: z7.string() })).mutation(({ ctx, input }) => closeProject(ctx, input.projectId)),
|
|
4522
4590
|
rename: publicProcedure.input(z7.object({ projectId: z7.string(), name: z7.string().min(1) })).mutation(({ ctx, input }) => renameProject(ctx, input.projectId, input.name))
|
|
@@ -5441,9 +5509,6 @@ var init_src2 = __esm(() => {
|
|
|
5441
5509
|
init_web();
|
|
5442
5510
|
init_router();
|
|
5443
5511
|
init_reconcile_runs();
|
|
5444
|
-
if (import.meta.main) {
|
|
5445
|
-
startServer();
|
|
5446
|
-
}
|
|
5447
5512
|
});
|
|
5448
5513
|
init_src2();
|
|
5449
5514
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "runcastle",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
|
+
"description": "Burn tickets into shipped features with Claude Code.",
|
|
5
|
+
"license": "MIT",
|
|
4
6
|
"type": "module",
|
|
5
7
|
"bin": {
|
|
6
8
|
"runcastle": "./bin/runcastle.js"
|
|
@@ -8,6 +10,14 @@
|
|
|
8
10
|
"engines": {
|
|
9
11
|
"bun": ">=1.3.14"
|
|
10
12
|
},
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/MuathZahir/runcastle.git"
|
|
16
|
+
},
|
|
17
|
+
"homepage": "https://github.com/MuathZahir/runcastle#readme",
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/MuathZahir/runcastle/issues"
|
|
20
|
+
},
|
|
11
21
|
"files": [
|
|
12
22
|
"index.js",
|
|
13
23
|
"bin",
|