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