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/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: (newValue) => all[name] = () => newValue
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
- await g.raw(["branch", branch, project.mainBranch]);
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(project.mainBranch);
2248
+ await g.checkout(target);
2197
2249
  try {
2198
2250
  await g.merge(["--no-ff", branch]);
2199
- return { ok: true };
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
- return { ok: false, conflict: true };
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 { branchReady } = await ensureFeatureBranch(project, slug);
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 createFeatureBranch(project, slug);
3153
- return { branchReady: true };
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 ${project.mainBranch}`);
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.1",
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",