runcastle 1.2.2 → 1.2.4

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
@@ -25,6 +25,7 @@ __export(exports_paths, {
25
25
  projectWorktreesDir: () => projectWorktreesDir,
26
26
  prodDataDir: () => prodDataDir,
27
27
  logsDir: () => logsDir,
28
+ installIdPath: () => installIdPath,
28
29
  featureDocsRel: () => featureDocsRel,
29
30
  envPath: () => envPath,
30
31
  devDataDir: () => devDataDir,
@@ -63,6 +64,9 @@ function configPath() {
63
64
  function envPath() {
64
65
  return join(dataDir(), ".env");
65
66
  }
67
+ function installIdPath() {
68
+ return join(dataDir(), "install-id");
69
+ }
66
70
  function logsDir() {
67
71
  return join(dataDir(), "logs");
68
72
  }
@@ -245,7 +249,7 @@ var init_ids = () => {};
245
249
 
246
250
  // ../core/src/schemas.ts
247
251
  import * as z2 from "zod";
248
- var Phase, TicketStatus, SessionKind, RunStatus, FeatureStatus, SessionStatus, TicketInput, Ticket, WaypointType, WaypointStatus, WaypointDisposition, WaypointInput, Waypoint, TestNoteStatus, TestNote, PREPARED_KEYS, PreparedKey, DRIVE_LOOP_KEYS, FindingSource, PROJECT_NAME_MAX = 80, ProjectName, Project, ProjectFinding, Feature, SessionRow, Run, SettingSource, SettingScope, SettingField, SettingsView, SettingsUpdateInput, EventRow;
252
+ var Phase, TicketStatus, SessionKind, SessionPurpose, MergeBranchPair, RunStatus, FeatureStatus, SessionStatus, TicketInput, Ticket, WaypointType, WaypointStatus, WaypointDisposition, WaypointInput, Waypoint, TestNoteStatus, TestNote, PREPARED_KEYS, PreparedKey, DRIVE_LOOP_KEYS, FindingSource, PROJECT_NAME_MAX = 80, ProjectName, Project, ProjectFinding, Feature, SessionRow, Run, SettingSource, SettingScope, SettingField, SettingsView, SettingsUpdateInput, EventRow;
249
253
  var init_schemas = __esm(() => {
250
254
  Phase = z2.enum([
251
255
  "ideation",
@@ -265,6 +269,11 @@ var init_schemas = __esm(() => {
265
269
  "prepare",
266
270
  "project"
267
271
  ]);
272
+ SessionPurpose = z2.enum(["resolve-conflict"]);
273
+ MergeBranchPair = z2.object({
274
+ mergeFrom: z2.string(),
275
+ mergeInto: z2.string()
276
+ });
268
277
  RunStatus = z2.enum(["running", "succeeded", "failed", "cancelled"]);
269
278
  FeatureStatus = z2.enum(["active", "shipped", "archived"]);
270
279
  SessionStatus = z2.enum(["launching", "live", "ended"]);
@@ -381,6 +390,8 @@ var init_schemas = __esm(() => {
381
390
  featureId: z2.string().optional(),
382
391
  projectId: z2.string().optional(),
383
392
  kind: SessionKind,
393
+ purpose: SessionPurpose.optional(),
394
+ purposeData: MergeBranchPair.optional(),
384
395
  ccSessionId: z2.string().optional(),
385
396
  transcriptPath: z2.string().optional(),
386
397
  status: SessionStatus,
@@ -619,6 +630,8 @@ var init_db_schema = __esm(() => {
619
630
  projectId: text("project_id"),
620
631
  lap: integer("lap").notNull().default(1),
621
632
  kind: text("kind").notNull().$type(),
633
+ purpose: text("purpose").$type(),
634
+ purposeData: text("purpose_data", { mode: "json" }).$type(),
622
635
  ccSessionId: text("cc_session_id"),
623
636
  transcriptPath: text("transcript_path"),
624
637
  status: text("status").notNull().$type(),
@@ -913,6 +926,23 @@ var init_resolve_executable = __esm(() => {
913
926
  };
914
927
  });
915
928
 
929
+ // src/pty/kill-tree.ts
930
+ import { execFile } from "child_process";
931
+ import { promisify } from "util";
932
+ async function killProcessTree(pid) {
933
+ try {
934
+ if (process.platform === "win32") {
935
+ await execFileAsync("taskkill", ["/pid", String(pid), "/T", "/F"], { windowsHide: true });
936
+ } else {
937
+ process.kill(-pid, "SIGTERM");
938
+ }
939
+ } catch {}
940
+ }
941
+ var execFileAsync;
942
+ var init_kill_tree = __esm(() => {
943
+ execFileAsync = promisify(execFile);
944
+ });
945
+
916
946
  // src/pty/pty-sidecar.ts
917
947
  import { spawn } from "child_process";
918
948
  import { createRequire } from "module";
@@ -1094,6 +1124,9 @@ ${why.replace(/\n/g, `\r
1094
1124
  }
1095
1125
  }, 500);
1096
1126
  },
1127
+ killTree() {
1128
+ return killProcessTree(process.platform === "win32" ? child.pid ?? pid : pid);
1129
+ },
1097
1130
  get pid() {
1098
1131
  return pid;
1099
1132
  },
@@ -1106,6 +1139,7 @@ var HOST_PATH;
1106
1139
  var init_pty_sidecar = __esm(() => {
1107
1140
  init_asset_paths();
1108
1141
  init_resolve_executable();
1142
+ init_kill_tree();
1109
1143
  HOST_PATH = resolveAsset(ASSET_ENV.ptyHost, fileURLToPath(new URL("./pty-host.cjs", import.meta.url)));
1110
1144
  });
1111
1145
 
@@ -1192,6 +1226,9 @@ function createNativePtySession(cmd, args, opts) {
1192
1226
  proc.kill();
1193
1227
  } catch {}
1194
1228
  },
1229
+ killTree() {
1230
+ return killProcessTree(proc.pid);
1231
+ },
1195
1232
  get pid() {
1196
1233
  return proc.pid;
1197
1234
  },
@@ -1203,6 +1240,7 @@ function createNativePtySession(cmd, args, opts) {
1203
1240
  var cached = null, BACKEND_LOGGED;
1204
1241
  var init_pty = __esm(() => {
1205
1242
  init_resolve_executable();
1243
+ init_kill_tree();
1206
1244
  init_pty_sidecar();
1207
1245
  BACKEND_LOGGED = Symbol.for("runcastle.pty.backend.logged");
1208
1246
  });
@@ -1540,6 +1578,8 @@ function rowToSession(row) {
1540
1578
  featureId: row.featureId ?? undefined,
1541
1579
  projectId: row.projectId ?? undefined,
1542
1580
  kind: row.kind,
1581
+ purpose: row.purpose ?? undefined,
1582
+ purposeData: row.purposeData ?? undefined,
1543
1583
  ccSessionId: row.ccSessionId ?? undefined,
1544
1584
  transcriptPath: row.transcriptPath ?? undefined,
1545
1585
  status: row.status,
@@ -1795,7 +1835,6 @@ var init_waypoints = __esm(() => {
1795
1835
  });
1796
1836
 
1797
1837
  // src/pty/dev-pane.ts
1798
- import { execFileSync } from "child_process";
1799
1838
  function drivePaneId(ownerId) {
1800
1839
  return `${DRIVE_PREFIX}${ownerId}`;
1801
1840
  }
@@ -1855,24 +1894,24 @@ function devPaneLive(paneId) {
1855
1894
  const entry = ptyRegistry().get(paneId);
1856
1895
  return !!entry && !entry.exited;
1857
1896
  }
1858
- function killProcessTree(pid) {
1859
- try {
1860
- if (process.platform === "win32") {
1861
- execFileSync("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore" });
1862
- } else {
1863
- process.kill(-pid, "SIGTERM");
1864
- }
1865
- } catch {}
1897
+ function killTreeBounded(pty) {
1898
+ return new Promise((resolve4) => {
1899
+ const timer = setTimeout(resolve4, KILL_TREE_TIMEOUT_MS);
1900
+ pty.killTree().catch(() => {}).then(() => {
1901
+ clearTimeout(timer);
1902
+ resolve4();
1903
+ });
1904
+ });
1866
1905
  }
1867
- function stopDevPane(paneId) {
1906
+ async function stopDevPane(paneId) {
1868
1907
  const reg = ptyRegistry();
1869
1908
  const entry = reg.get(paneId);
1870
1909
  if (entry && !entry.exited)
1871
- killProcessTree(entry.pty.pid);
1910
+ await killTreeBounded(entry.pty);
1872
1911
  reg.kill(paneId);
1873
1912
  reg.remove(paneId);
1874
1913
  }
1875
- var DRIVE_PREFIX = "drive:", SNIFF_WINDOW = 8192;
1914
+ var DRIVE_PREFIX = "drive:", SNIFF_WINDOW = 8192, KILL_TREE_TIMEOUT_MS = 5000;
1876
1915
  var init_dev_pane = __esm(() => {
1877
1916
  init_events();
1878
1917
  init_registry();
@@ -2801,7 +2840,7 @@ async function testDrive(ctx, project, feature, action) {
2801
2840
  const detachedWorktree2 = testDriveState.detachedWorktree;
2802
2841
  const devPaneId = testDriveState.devPaneId;
2803
2842
  if (devPaneId)
2804
- stopDevPane(devPaneId);
2843
+ await stopDevPane(devPaneId);
2805
2844
  const teardown = await runDriveHookStep(ctx, scope, project.repoPath, "teardown", project.driveStopCommand, driveEnvFor(ctx, project, { slug: feature.slug, branch }, scope).env);
2806
2845
  const carriedChanges = await dirtyPaths(g);
2807
2846
  await g.checkout(previousBranch2);
@@ -2941,7 +2980,7 @@ async function startDryRun(ctx, project) {
2941
2980
  async function stopDryRun(ctx, project, state) {
2942
2981
  const scope = { projectId: project.id };
2943
2982
  if (state.devPaneId)
2944
- stopDevPane(state.devPaneId);
2983
+ await stopDevPane(state.devPaneId);
2945
2984
  const teardown = await runDriveHookStep(ctx, scope, project.repoPath, "teardown", project.driveStopCommand, state.env);
2946
2985
  state.observed.teardownOk = teardown?.result.ok;
2947
2986
  testDriveState = undefined;
@@ -3156,10 +3195,24 @@ async function conflictedFiles(g) {
3156
3195
  return [];
3157
3196
  }
3158
3197
  }
3198
+ async function mergeInProgressAt(worktreePath) {
3199
+ try {
3200
+ return await mergeInProgress(git(worktreePath));
3201
+ } catch {
3202
+ return false;
3203
+ }
3204
+ }
3205
+ async function isAncestor(repoPath, ancestor, descendant) {
3206
+ try {
3207
+ const out = await git(repoPath).raw(["rev-list", "--count", ancestor, `^${descendant}`]);
3208
+ return out.trim() === "0";
3209
+ } catch {
3210
+ return false;
3211
+ }
3212
+ }
3159
3213
  async function mergeInProgress(g) {
3160
3214
  try {
3161
- await g.raw(["rev-parse", "--verify", "--quiet", "MERGE_HEAD"]);
3162
- return true;
3215
+ return (await g.raw(["rev-parse", "--verify", "--quiet", "MERGE_HEAD"])).trim() !== "";
3163
3216
  } catch {
3164
3217
  return false;
3165
3218
  }
@@ -3189,6 +3242,8 @@ function createSessionRow(ctx, input) {
3189
3242
  projectId: input.projectId ?? null,
3190
3243
  lap: input.featureId ? getFeatureRow(ctx, input.featureId).lap : 1,
3191
3244
  kind: input.kind,
3245
+ purpose: input.purpose ?? null,
3246
+ purposeData: input.purposeData ?? null,
3192
3247
  ccSessionId: null,
3193
3248
  transcriptPath: null,
3194
3249
  status: "launching",
@@ -3436,7 +3491,7 @@ function landProjectSession(ctx, session) {
3436
3491
  data: { sessionId: session.id, branch: PROJECT_BRANCH, ...data }
3437
3492
  });
3438
3493
  };
3439
- landProjectBranch(project).then((res) => {
3494
+ const landing = landProjectBranch(project).then((res) => {
3440
3495
  if (!res)
3441
3496
  return;
3442
3497
  if (res.landed) {
@@ -3449,6 +3504,8 @@ function landProjectSession(ctx, session) {
3449
3504
  }).catch((e) => {
3450
3505
  report("project.land_failed", `could not land the project session's work onto ${project.mainBranch}: ${e instanceof Error ? e.message : String(e)} \u2014 ${kept}`, { error: e instanceof Error ? e.message : String(e) });
3451
3506
  });
3507
+ inFlightLandings.add(landing);
3508
+ landing.finally(() => inFlightLandings.delete(landing));
3452
3509
  }
3453
3510
  function markSessionEnded(ctx, id) {
3454
3511
  const existing = getSessionRow(ctx, id);
@@ -3477,7 +3534,7 @@ function mostRecentLiveSession(ctx) {
3477
3534
  const row = ctx.db.select().from(sessions).where(eq5(sessions.status, "live")).orderBy(desc3(sql2`rowid`)).limit(1).get();
3478
3535
  return row ? rowToSession(row) : null;
3479
3536
  }
3480
- var KICKOFF_DELAY_MS = 1500, KICKOFF_SUBMIT_DELAY_MS = 350, KICKOFF_CONFIRM_MS = 12000, KICKOFF_MAX_ATTEMPTS = 3, CLEAR_INPUT = "\x15", SESSION_READY_TIMEOUT_MS = 25000, CONVERGE_KICKOFF_LINE, KICKOFF_LINES, RESUME_KICKOFF_PREFIX, pendingKickoffOverrides, deliveries, MATCH_PREFIX = 40, landedProjectSessions;
3537
+ var KICKOFF_DELAY_MS = 1500, KICKOFF_SUBMIT_DELAY_MS = 350, KICKOFF_CONFIRM_MS = 12000, KICKOFF_MAX_ATTEMPTS = 3, CLEAR_INPUT = "\x15", SESSION_READY_TIMEOUT_MS = 25000, CONVERGE_KICKOFF_LINE, KICKOFF_LINES, RESUME_KICKOFF_PREFIX, pendingKickoffOverrides, deliveries, MATCH_PREFIX = 40, landedProjectSessions, inFlightLandings;
3481
3538
  var init_sessions = __esm(() => {
3482
3539
  init_src();
3483
3540
  init_schema();
@@ -3500,6 +3557,7 @@ var init_sessions = __esm(() => {
3500
3557
  pendingKickoffOverrides = new Map;
3501
3558
  deliveries = new Map;
3502
3559
  landedProjectSessions = new Set;
3560
+ inFlightLandings = new Set;
3503
3561
  });
3504
3562
 
3505
3563
  // src/launcher/reconcile.ts
@@ -6630,6 +6688,8 @@ function guardsEdits(kind) {
6630
6688
  function evaluateEditGuard(input) {
6631
6689
  if (!guardsEdits(input.kind))
6632
6690
  return null;
6691
+ if (input.purpose === "resolve-conflict" && input.mergeInProgress)
6692
+ return null;
6633
6693
  if (!input.toolName || !EDIT_TOOLS.includes(input.toolName))
6634
6694
  return null;
6635
6695
  if (!input.filePath)
@@ -6806,22 +6866,40 @@ function handleUserPrompt(ctx, sessionId, feature, payload) {
6806
6866
  }
6807
6867
  };
6808
6868
  }
6809
- function handleSessionEnd(ctx, sessionId, feature) {
6810
- markSessionEnded(ctx, sessionId);
6811
- releaseForSession(ctx, sessionId);
6869
+ async function handleSessionEnd(ctx, session, feature) {
6870
+ markSessionEnded(ctx, session.id);
6871
+ releaseForSession(ctx, session.id);
6812
6872
  emit(ctx, feature.id, {
6813
6873
  type: "session.ended",
6814
6874
  message: "session ended",
6815
- data: { sessionId }
6875
+ data: { sessionId: session.id }
6816
6876
  });
6877
+ await noteResolvedMerge(ctx, session, feature);
6817
6878
  return {};
6818
6879
  }
6819
- function handlePreToolUse(session, feature, payload) {
6880
+ async function noteResolvedMerge(ctx, session, feature) {
6881
+ const pair = session.purpose === "resolve-conflict" ? session.purposeData : undefined;
6882
+ if (!pair)
6883
+ return;
6884
+ try {
6885
+ if (!await isAncestor(session.worktreePath, pair.mergeFrom, pair.mergeInto))
6886
+ return;
6887
+ emit(ctx, feature.id, {
6888
+ type: "merge.resolved",
6889
+ message: `merge conflict resolved \u2014 ${pair.mergeFrom} is in ${pair.mergeInto}`,
6890
+ data: { sessionId: session.id, ...pair }
6891
+ });
6892
+ } catch {}
6893
+ }
6894
+ async function handlePreToolUse(session, feature, payload) {
6820
6895
  const toolName = typeof payload?.tool_name === "string" ? payload.tool_name : undefined;
6821
6896
  const toolInput = payload?.tool_input ?? {};
6822
6897
  const path = typeof toolInput.file_path === "string" ? toolInput.file_path : typeof toolInput.notebook_path === "string" ? toolInput.notebook_path : undefined;
6898
+ const mergeInProgress2 = session.purpose === "resolve-conflict" ? await mergeInProgressAt(session.worktreePath) : false;
6823
6899
  const denial = evaluateEditGuard({
6824
6900
  kind: session.kind,
6901
+ purpose: session.purpose,
6902
+ mergeInProgress: mergeInProgress2,
6825
6903
  worktreePath: session.worktreePath,
6826
6904
  toolName,
6827
6905
  filePath: path,
@@ -6850,6 +6928,7 @@ var init_hooks = __esm(() => {
6850
6928
  init_edit_guard();
6851
6929
  init_sessions();
6852
6930
  init_events();
6931
+ init_git();
6853
6932
  init_prep();
6854
6933
  init_repo();
6855
6934
  init_tickets();
@@ -6887,7 +6966,7 @@ var init_hooks = __esm(() => {
6887
6966
  case "session-end":
6888
6967
  return c.json(handleProjectScopedSessionEnd(ctx, session));
6889
6968
  case "pre-tool":
6890
- return c.json(handlePreToolUse(session, undefined, body.payload));
6969
+ return c.json(await handlePreToolUse(session, undefined, body.payload));
6891
6970
  default:
6892
6971
  return c.json({});
6893
6972
  }
@@ -6901,9 +6980,9 @@ var init_hooks = __esm(() => {
6901
6980
  case "user-prompt":
6902
6981
  return c.json(handleUserPrompt(ctx, sessionId, feature, body.payload));
6903
6982
  case "session-end":
6904
- return c.json(handleSessionEnd(ctx, sessionId, feature));
6983
+ return c.json(await handleSessionEnd(ctx, session, feature));
6905
6984
  case "pre-tool":
6906
- return c.json(handlePreToolUse(session, feature, body.payload));
6985
+ return c.json(await handlePreToolUse(session, feature, body.payload));
6907
6986
  default:
6908
6987
  return c.json({});
6909
6988
  }
@@ -7045,6 +7124,166 @@ var init_web = __esm(() => {
7045
7124
  };
7046
7125
  });
7047
7126
 
7127
+ // src/version.ts
7128
+ import { readFileSync as readFileSync6 } from "fs";
7129
+ import { dirname as dirname5, join as join12 } from "path";
7130
+ import { fileURLToPath as fileURLToPath4 } from "url";
7131
+ function runcastleVersion() {
7132
+ if (cached2)
7133
+ return cached2;
7134
+ const here = dirname5(fileURLToPath4(import.meta.url));
7135
+ for (const pkgPath of [join12(here, "..", "package.json"), join12(here, "package.json")]) {
7136
+ try {
7137
+ const pkg = JSON.parse(readFileSync6(pkgPath, "utf8"));
7138
+ if (typeof pkg.version === "string") {
7139
+ cached2 = pkg.version;
7140
+ return cached2;
7141
+ }
7142
+ } catch {}
7143
+ }
7144
+ cached2 = UNKNOWN_VERSION;
7145
+ return cached2;
7146
+ }
7147
+ var cached2, UNKNOWN_VERSION = "0.0.0";
7148
+ var init_version = () => {};
7149
+
7150
+ // src/services/install-id.ts
7151
+ import { randomUUID } from "crypto";
7152
+ import { existsSync as existsSync13, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "fs";
7153
+ import { dirname as dirname6 } from "path";
7154
+ function getInstallId() {
7155
+ const file = installIdPath();
7156
+ try {
7157
+ if (existsSync13(file)) {
7158
+ const existing = readFileSync7(file, "utf8").trim();
7159
+ if (UUID_RE.test(existing))
7160
+ return existing;
7161
+ }
7162
+ const id = randomUUID();
7163
+ mkdirSync6(dirname6(file), { recursive: true });
7164
+ writeFileSync2(file, `${id}
7165
+ `);
7166
+ return id;
7167
+ } catch {
7168
+ return randomUUID();
7169
+ }
7170
+ }
7171
+ var UUID_RE;
7172
+ var init_install_id = __esm(() => {
7173
+ init_paths();
7174
+ UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
7175
+ });
7176
+
7177
+ // src/services/update-check.ts
7178
+ function parse(version) {
7179
+ const cleaned = version.trim().replace(/^v/, "");
7180
+ const [core = "", pre = ""] = cleaned.split("-", 2);
7181
+ const release2 = core.split(".").map((n) => Number.parseInt(n, 10) || 0);
7182
+ return { release: release2, pre: pre ? pre.split(".") : [] };
7183
+ }
7184
+ function compareSemver(a, b) {
7185
+ const pa = parse(a);
7186
+ const pb = parse(b);
7187
+ const len = Math.max(pa.release.length, pb.release.length);
7188
+ for (let i = 0;i < len; i++) {
7189
+ const diff = (pa.release[i] ?? 0) - (pb.release[i] ?? 0);
7190
+ if (diff !== 0)
7191
+ return diff;
7192
+ }
7193
+ if (pa.pre.length === 0 && pb.pre.length === 0)
7194
+ return 0;
7195
+ if (pa.pre.length === 0)
7196
+ return 1;
7197
+ if (pb.pre.length === 0)
7198
+ return -1;
7199
+ const preLen = Math.max(pa.pre.length, pb.pre.length);
7200
+ for (let i = 0;i < preLen; i++) {
7201
+ const x = pa.pre[i];
7202
+ const y = pb.pre[i];
7203
+ if (x === undefined)
7204
+ return -1;
7205
+ if (y === undefined)
7206
+ return 1;
7207
+ const nx = Number(x);
7208
+ const ny = Number(y);
7209
+ const bothNumeric = !Number.isNaN(nx) && !Number.isNaN(ny);
7210
+ const cmp = bothNumeric ? nx - ny : x < y ? -1 : x > y ? 1 : 0;
7211
+ if (cmp !== 0)
7212
+ return cmp;
7213
+ }
7214
+ return 0;
7215
+ }
7216
+ function doNotTrack(env) {
7217
+ const raw = env.DO_NOT_TRACK;
7218
+ return raw !== undefined && raw !== "" && raw !== "0";
7219
+ }
7220
+ async function latestFromNpm(fetchImpl) {
7221
+ try {
7222
+ const res = await fetchImpl(NPM_LATEST_URL);
7223
+ if (!res.ok)
7224
+ return null;
7225
+ const body = await res.json();
7226
+ return typeof body.version === "string" ? body.version : null;
7227
+ } catch {
7228
+ return null;
7229
+ }
7230
+ }
7231
+ async function latestFromPing(fetchImpl, current2) {
7232
+ try {
7233
+ const res = await fetchImpl(PING_URL, {
7234
+ method: "POST",
7235
+ headers: { "content-type": "application/json" },
7236
+ body: JSON.stringify({
7237
+ installId: getInstallId(),
7238
+ version: current2,
7239
+ platform: process.platform
7240
+ })
7241
+ });
7242
+ if (!res.ok)
7243
+ return null;
7244
+ const body = await res.json();
7245
+ return typeof body.latest === "string" ? body.latest : null;
7246
+ } catch {
7247
+ return null;
7248
+ }
7249
+ }
7250
+ async function checkForUpdate(opts) {
7251
+ const { current: current2 } = opts;
7252
+ const fetchImpl = opts.fetchImpl ?? fetch;
7253
+ const env = opts.env ?? process.env;
7254
+ if (current2 === UNKNOWN_VERSION)
7255
+ return NO_UPDATE(current2);
7256
+ const latest = doNotTrack(env) ? await latestFromNpm(fetchImpl) : await latestFromPing(fetchImpl, current2) ?? await latestFromNpm(fetchImpl);
7257
+ if (!latest)
7258
+ return NO_UPDATE(current2);
7259
+ return {
7260
+ current: current2,
7261
+ latest,
7262
+ updateAvailable: compareSemver(latest, current2) > 0,
7263
+ command: UPDATE_COMMAND
7264
+ };
7265
+ }
7266
+ async function getUpdateInfo(current2, fetchImpl) {
7267
+ if (cache)
7268
+ return cache;
7269
+ const info = await checkForUpdate({ current: current2, fetchImpl });
7270
+ if (info.latest !== null)
7271
+ cache = info;
7272
+ return info;
7273
+ }
7274
+ var PACKAGE_NAME = "runcastle", UPDATE_COMMAND, PING_URL = "https://ping.runcastle.dev/ping", NPM_LATEST_URL, NO_UPDATE = (current2) => ({
7275
+ current: current2,
7276
+ latest: null,
7277
+ updateAvailable: false,
7278
+ command: UPDATE_COMMAND
7279
+ }), cache;
7280
+ var init_update_check = __esm(() => {
7281
+ init_version();
7282
+ init_install_id();
7283
+ UPDATE_COMMAND = `bun add -g ${PACKAGE_NAME}@latest`;
7284
+ NPM_LATEST_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
7285
+ });
7286
+
7048
7287
  // src/trpc/context.ts
7049
7288
  import { initTRPC } from "@trpc/server";
7050
7289
  var t, errorMapping, router, publicProcedure, createCallerFactory;
@@ -7093,11 +7332,11 @@ var init_events2 = __esm(() => {
7093
7332
  });
7094
7333
 
7095
7334
  // src/launcher/artifacts.ts
7096
- import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync2 } from "fs";
7097
- import { fileURLToPath as fileURLToPath4 } from "url";
7098
- import { join as join12 } from "path";
7335
+ import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync3 } from "fs";
7336
+ import { fileURLToPath as fileURLToPath5 } from "url";
7337
+ import { join as join13 } from "path";
7099
7338
  function hookClientPath() {
7100
- return resolveAsset(ASSET_ENV.hookClient, fileURLToPath4(new URL("./hook-client.ts", import.meta.url)));
7339
+ return resolveAsset(ASSET_ENV.hookClient, fileURLToPath5(new URL("./hook-client.ts", import.meta.url)));
7101
7340
  }
7102
7341
  function serverUrlFor(config2) {
7103
7342
  return `http://localhost:${config2.serverPort}`;
@@ -7519,16 +7758,16 @@ function renderMcpConfig(session, config2) {
7519
7758
  async function writeSessionArtifacts(input) {
7520
7759
  const { session, feature, config: config2, waypoint, prepare, projectBrief, lap } = input;
7521
7760
  const dir = sessionDir(session.id);
7522
- mkdirSync6(dir, { recursive: true });
7523
- const systemPromptPath = join12(dir, "system-prompt.md");
7524
- const settingsPath = join12(dir, "settings.json");
7525
- const mcpConfigPath = join12(dir, "mcp.json");
7761
+ mkdirSync7(dir, { recursive: true });
7762
+ const systemPromptPath = join13(dir, "system-prompt.md");
7763
+ const settingsPath = join13(dir, "settings.json");
7764
+ const mcpConfigPath = join13(dir, "mcp.json");
7526
7765
  const systemPrompt = feature ? renderSystemPrompt(feature, session.kind, waypoint, lap) : prepare ? renderPreparePrompt(prepare) : projectBrief ? renderProjectPrompt(projectBrief) : (() => {
7527
7766
  throw new Error(`session ${session.id} has no feature and no project-session brief`);
7528
7767
  })();
7529
- writeFileSync2(systemPromptPath, systemPrompt, "utf8");
7530
- writeFileSync2(settingsPath, JSON.stringify(renderSettings(hookClientPath(), session.kind), null, 2), "utf8");
7531
- writeFileSync2(mcpConfigPath, JSON.stringify(renderMcpConfig(session, config2), null, 2), "utf8");
7768
+ writeFileSync3(systemPromptPath, systemPrompt, "utf8");
7769
+ writeFileSync3(settingsPath, JSON.stringify(renderSettings(hookClientPath(), session.kind), null, 2), "utf8");
7770
+ writeFileSync3(mcpConfigPath, JSON.stringify(renderMcpConfig(session, config2), null, 2), "utf8");
7532
7771
  return { systemPromptPath, settingsPath, mcpConfigPath };
7533
7772
  }
7534
7773
  var RUNCASTLE_MCP_ALLOW_RULES, SESSION_BASH_READ_RULES, SESSION_BASH_WRITE_RULES, SESSION_BASH_ALLOW_RULES, SESSION_START_SOURCES;
@@ -7572,9 +7811,9 @@ var init_artifacts = __esm(() => {
7572
7811
  });
7573
7812
 
7574
7813
  // src/launcher/launcher.ts
7575
- import { existsSync as existsSync13 } from "fs";
7576
- import { dirname as dirname5, join as join13, resolve as resolve9 } from "path";
7577
- import { fileURLToPath as fileURLToPath5 } from "url";
7814
+ import { existsSync as existsSync14 } from "fs";
7815
+ import { dirname as dirname7, join as join14, resolve as resolve9 } from "path";
7816
+ import { fileURLToPath as fileURLToPath6 } from "url";
7578
7817
  import { and as and9, eq as eq11 } from "drizzle-orm";
7579
7818
  function ptyExitMessage(exitCode) {
7580
7819
  const label = typeof exitCode === "number" ? String(exitCode) : "unknown";
@@ -7606,23 +7845,23 @@ function resolveClaudeExecutable() {
7606
7845
  function claudeSpawnTarget(claudeArgs) {
7607
7846
  return spawnTargetFor(resolveClaudeExecutable(), claudeArgs);
7608
7847
  }
7609
- function resolvePluginDir(fromDir = dirname5(fileURLToPath5(import.meta.url))) {
7610
- const rel = join13("packages", "skills", "packs", "runcastle");
7848
+ function resolvePluginDir(fromDir = dirname7(fileURLToPath6(import.meta.url))) {
7849
+ const rel = join14("packages", "skills", "packs", "runcastle");
7611
7850
  const override = process.env[SKILLS_DIR_ENV];
7612
7851
  if (override) {
7613
- const dir2 = join13(resolve9(override), "packs", "runcastle");
7614
- if (existsSync13(dir2))
7852
+ const dir2 = join14(resolve9(override), "packs", "runcastle");
7853
+ if (existsSync14(dir2))
7615
7854
  return dir2;
7616
7855
  throw new Error(`${SKILLS_DIR_ENV}=${override} has no plugin dir at ${dir2}`);
7617
7856
  }
7618
7857
  const searched = [];
7619
7858
  let dir = fromDir;
7620
7859
  for (let i = 0;i < 8; i += 1) {
7621
- const candidate = join13(dir, rel);
7860
+ const candidate = join14(dir, rel);
7622
7861
  searched.push(candidate);
7623
- if (existsSync13(candidate))
7862
+ if (existsSync14(candidate))
7624
7863
  return candidate;
7625
- const parent = dirname5(dir);
7864
+ const parent = dirname7(dir);
7626
7865
  if (parent === dir)
7627
7866
  break;
7628
7867
  dir = parent;
@@ -7691,6 +7930,8 @@ async function launchSession(ctx, input, opts = {}) {
7691
7930
  const session = createSessionRow(ctx, {
7692
7931
  featureId: feature.id,
7693
7932
  kind: input.kind,
7933
+ purpose: input.purpose,
7934
+ purposeData: input.purposeData,
7694
7935
  worktreePath
7695
7936
  });
7696
7937
  const plan = planKickoff({ kind: input.kind, lap: feature.lap, kickoffLine: input.kickoffLine });
@@ -8138,7 +8379,9 @@ var init_feature = __esm(() => {
8138
8379
  launchSession: publicProcedure.input(z6.object({
8139
8380
  featureId: z6.string(),
8140
8381
  kind: SessionKind,
8141
- kickoffLine: z6.string().min(1).optional()
8382
+ kickoffLine: z6.string().min(1).optional(),
8383
+ purpose: SessionPurpose.optional(),
8384
+ purposeData: MergeBranchPair.optional()
8142
8385
  })).mutation(({ ctx, input }) => launchSession(ctx, input)),
8143
8386
  workWaypoint: publicProcedure.input(z6.object({ featureId: z6.string(), waypointId: z6.string(), endLive: z6.boolean().optional() })).mutation(({ ctx, input }) => workWaypoint(ctx, input)),
8144
8387
  converge: publicProcedure.input(z6.object({ featureId: z6.string(), overrideReason: z6.string().min(1).optional() })).mutation(({ ctx, input }) => converge(ctx, { featureId: input.featureId, overrideReason: input.overrideReason })),
@@ -8189,9 +8432,9 @@ var init_feature = __esm(() => {
8189
8432
  });
8190
8433
 
8191
8434
  // src/services/fsbrowse.ts
8192
- import { existsSync as existsSync14, readdirSync as readdirSync5, statSync as statSync2 } from "fs";
8435
+ import { existsSync as existsSync15, readdirSync as readdirSync5, statSync as statSync2 } from "fs";
8193
8436
  import { homedir as homedir2 } from "os";
8194
- import { basename, dirname as dirname6, isAbsolute as isAbsolute2, join as join14, resolve as resolve10, sep as sep3 } from "path";
8437
+ import { basename, dirname as dirname8, isAbsolute as isAbsolute2, join as join15, resolve as resolve10, sep as sep3 } from "path";
8195
8438
  function expandPath(input, home = homedir2()) {
8196
8439
  const trimmed = input.trim();
8197
8440
  if (trimmed === "")
@@ -8200,12 +8443,12 @@ function expandPath(input, home = homedir2()) {
8200
8443
  if (out === "~")
8201
8444
  out = home;
8202
8445
  else if (out.startsWith("~/") || out.startsWith("~\\"))
8203
- out = join14(home, out.slice(2));
8446
+ out = join15(home, out.slice(2));
8204
8447
  return resolve10(out);
8205
8448
  }
8206
8449
  function looksLikeRepo(dir) {
8207
8450
  try {
8208
- return existsSync14(join14(dir, ".git"));
8451
+ return existsSync15(join15(dir, ".git"));
8209
8452
  } catch {
8210
8453
  return false;
8211
8454
  }
@@ -8216,20 +8459,20 @@ function listRoots(home = homedir2()) {
8216
8459
  for (let code = 67;code <= 90; code++) {
8217
8460
  const drive = `${String.fromCharCode(code)}:${sep3}`;
8218
8461
  try {
8219
- if (existsSync14(drive))
8462
+ if (existsSync15(drive))
8220
8463
  roots.push({ label: drive.replace(/\\$/, ""), path: drive, kind: "drive" });
8221
8464
  } catch {}
8222
8465
  }
8223
8466
  } else {
8224
8467
  roots.push({ label: "/", path: "/", kind: "drive" });
8225
8468
  }
8226
- if (existsSync14(home))
8469
+ if (existsSync15(home))
8227
8470
  roots.push({ label: "Home", path: home, kind: "home" });
8228
8471
  const seen = new Set(roots.map(pathKey));
8229
8472
  for (const name of COMMON_DIRS) {
8230
- const candidate = join14(home, name);
8473
+ const candidate = join15(home, name);
8231
8474
  const key = pathKey({ path: candidate });
8232
- if (seen.has(key) || !existsSync14(candidate))
8475
+ if (seen.has(key) || !existsSync15(candidate))
8233
8476
  continue;
8234
8477
  seen.add(key);
8235
8478
  roots.push({ label: name, path: candidate, kind: "common" });
@@ -8244,7 +8487,7 @@ function crumbsFor(dir) {
8244
8487
  let cur = dir;
8245
8488
  for (;; ) {
8246
8489
  out.unshift({ name: basename(cur) || cur, path: cur });
8247
- const parent = dirname6(cur);
8490
+ const parent = dirname8(cur);
8248
8491
  if (parent === cur)
8249
8492
  break;
8250
8493
  cur = parent;
@@ -8252,7 +8495,7 @@ function crumbsFor(dir) {
8252
8495
  return out;
8253
8496
  }
8254
8497
  function parentOf(dir) {
8255
- const parent = dirname6(dir);
8498
+ const parent = dirname8(dir);
8256
8499
  return parent === dir ? null : parent;
8257
8500
  }
8258
8501
  function browseDir(input, showHidden = false, home = homedir2()) {
@@ -8260,13 +8503,13 @@ function browseDir(input, showHidden = false, home = homedir2()) {
8260
8503
  if (!isAbsolute2(target)) {
8261
8504
  throw new InvalidInputError(`path is not absolute: ${target}`);
8262
8505
  }
8263
- if (!existsSync14(target)) {
8506
+ if (!existsSync15(target)) {
8264
8507
  throw new InvalidInputError(`path does not exist: ${target}`);
8265
8508
  }
8266
8509
  let dir = target;
8267
8510
  try {
8268
8511
  if (!statSync2(target).isDirectory())
8269
- dir = dirname6(target);
8512
+ dir = dirname8(target);
8270
8513
  } catch (e) {
8271
8514
  throw new InvalidInputError(`cannot read path: ${target} (${errMsg4(e)})`);
8272
8515
  }
@@ -8288,7 +8531,7 @@ function browseDir(input, showHidden = false, home = homedir2()) {
8288
8531
  continue;
8289
8532
  if (!showHidden && name.startsWith("."))
8290
8533
  continue;
8291
- const path = join14(dir, name);
8534
+ const path = join15(dir, name);
8292
8535
  const isSymlink = dirent.isSymbolicLink();
8293
8536
  if (isSymlink && !showHidden)
8294
8537
  continue;
@@ -8325,8 +8568,8 @@ var init_fsbrowse = __esm(() => {
8325
8568
  });
8326
8569
 
8327
8570
  // src/services/projects.ts
8328
- import { existsSync as existsSync15 } from "fs";
8329
- import { basename as basename2, join as join15 } from "path";
8571
+ import { existsSync as existsSync16 } from "fs";
8572
+ import { basename as basename2, join as join16 } from "path";
8330
8573
  import { eq as eq12, isNull } from "drizzle-orm";
8331
8574
  function listProjects(ctx) {
8332
8575
  return ctx.db.select().from(projects).where(isNull(projects.closedAt)).all().map(rowToProject);
@@ -8407,7 +8650,7 @@ async function assertRepoTolerant(ctx, repoPath) {
8407
8650
  } catch (e) {
8408
8651
  if (!isNotImplemented(e))
8409
8652
  throw e;
8410
- if (!existsSync15(join15(repoPath, ".git"))) {
8653
+ if (!existsSync16(join16(repoPath, ".git"))) {
8411
8654
  throw new InvalidInputError(`not a git repository: ${repoPath}`);
8412
8655
  }
8413
8656
  }
@@ -8480,15 +8723,15 @@ var init_run = __esm(() => {
8480
8723
  });
8481
8724
 
8482
8725
  // src/services/settings.ts
8483
- import { existsSync as existsSync16, mkdirSync as mkdirSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
8484
- import { dirname as dirname7 } from "path";
8726
+ import { existsSync as existsSync17, mkdirSync as mkdirSync8, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
8727
+ import { dirname as dirname9 } from "path";
8485
8728
  import * as z9 from "zod";
8486
8729
  import { eq as eq13 } from "drizzle-orm";
8487
8730
  function readRawConfig(configFile) {
8488
- if (!existsSync16(configFile))
8731
+ if (!existsSync17(configFile))
8489
8732
  return {};
8490
8733
  try {
8491
- const parsed = JSON.parse(readFileSync6(configFile, "utf8"));
8734
+ const parsed = JSON.parse(readFileSync8(configFile, "utf8"));
8492
8735
  if (typeof parsed !== "object" || parsed === null)
8493
8736
  return {};
8494
8737
  return foldLegacyModelConfig(parsed);
@@ -8627,8 +8870,8 @@ function updateSettings(ctx, input, io = {}) {
8627
8870
  function writeGlobal(configFile, configKey, value) {
8628
8871
  const raw = readRawConfig(configFile);
8629
8872
  raw[configKey] = value;
8630
- mkdirSync7(dirname7(configFile), { recursive: true });
8631
- writeFileSync3(configFile, `${JSON.stringify(raw, null, 2)}
8873
+ mkdirSync8(dirname9(configFile), { recursive: true });
8874
+ writeFileSync4(configFile, `${JSON.stringify(raw, null, 2)}
8632
8875
  `);
8633
8876
  }
8634
8877
  function updateStepModel(ctx, input, configFile, io) {
@@ -8666,8 +8909,8 @@ function writeStepModel(configFile, step, value) {
8666
8909
  else
8667
8910
  stepModels[step] = value;
8668
8911
  raw.stepModels = stepModels;
8669
- mkdirSync7(dirname7(configFile), { recursive: true });
8670
- writeFileSync3(configFile, `${JSON.stringify(raw, null, 2)}
8912
+ mkdirSync8(dirname9(configFile), { recursive: true });
8913
+ writeFileSync4(configFile, `${JSON.stringify(raw, null, 2)}
8671
8914
  `);
8672
8915
  }
8673
8916
  function field(view, key) {
@@ -9044,9 +9287,9 @@ var init_system_exec = __esm(() => {
9044
9287
  });
9045
9288
 
9046
9289
  // src/services/setup.ts
9047
- import { copyFileSync, existsSync as existsSync17, mkdirSync as mkdirSync8, readFileSync as readFileSync7, readdirSync as readdirSync6, writeFileSync as writeFileSync4 } from "fs";
9048
- import { dirname as dirname8, join as join16 } from "path";
9049
- import { fileURLToPath as fileURLToPath6 } from "url";
9290
+ import { copyFileSync, existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as readFileSync9, readdirSync as readdirSync6, writeFileSync as writeFileSync5 } from "fs";
9291
+ import { dirname as dirname10, join as join17 } from "path";
9292
+ import { fileURLToPath as fileURLToPath7 } from "url";
9050
9293
  async function writeGitIdentity(exec, identity) {
9051
9294
  const name = identity.name.trim();
9052
9295
  const email = identity.email.trim();
@@ -9121,16 +9364,16 @@ async function resolveRuntime(exec, preferred) {
9121
9364
  function resolveSandcastleBin() {
9122
9365
  try {
9123
9366
  const mainUrl = import.meta.resolve("@ai-hero/sandcastle");
9124
- let dir = dirname8(fileURLToPath6(mainUrl));
9367
+ let dir = dirname10(fileURLToPath7(mainUrl));
9125
9368
  for (let hops = 0;hops < 6; hops++) {
9126
- const manifestPath = join16(dir, "package.json");
9127
- if (existsSync17(manifestPath)) {
9128
- const manifest = JSON.parse(readFileSync7(manifestPath, "utf8"));
9369
+ const manifestPath = join17(dir, "package.json");
9370
+ if (existsSync18(manifestPath)) {
9371
+ const manifest = JSON.parse(readFileSync9(manifestPath, "utf8"));
9129
9372
  if (manifest.name === "@ai-hero/sandcastle" && manifest.bin?.sandcastle) {
9130
- return join16(dir, manifest.bin.sandcastle);
9373
+ return join17(dir, manifest.bin.sandcastle);
9131
9374
  }
9132
9375
  }
9133
- const parent = dirname8(dir);
9376
+ const parent = dirname10(dir);
9134
9377
  if (parent === dir)
9135
9378
  break;
9136
9379
  dir = parent;
@@ -9152,21 +9395,21 @@ function terminalSpec(kind, opts) {
9152
9395
  };
9153
9396
  }
9154
9397
  function sandcastleTemplateDir() {
9155
- return resolveAsset(ASSET_ENV.sandcastleTemplate, fileURLToPath6(new URL("../assets/sandcastle", import.meta.url)));
9398
+ return resolveAsset(ASSET_ENV.sandcastleTemplate, fileURLToPath7(new URL("../assets/sandcastle", import.meta.url)));
9156
9399
  }
9157
9400
  function scaffoldSandcastleConfig(templateDir, targetDir) {
9158
- const dir = join16(targetDir, ".sandcastle");
9159
- if (existsSync17(dir))
9401
+ const dir = join17(targetDir, ".sandcastle");
9402
+ if (existsSync18(dir))
9160
9403
  return { scaffolded: false, dir };
9161
- mkdirSync8(dir, { recursive: true });
9404
+ mkdirSync9(dir, { recursive: true });
9162
9405
  for (const name of readdirSync6(templateDir)) {
9163
- copyFileSync(join16(templateDir, name), join16(dir, name));
9406
+ copyFileSync(join17(templateDir, name), join17(dir, name));
9164
9407
  }
9165
9408
  return { scaffolded: true, dir };
9166
9409
  }
9167
9410
  function prepareSandboxBuildContext() {
9168
9411
  const target = sandboxBuildDir();
9169
- mkdirSync8(target, { recursive: true });
9412
+ mkdirSync9(target, { recursive: true });
9170
9413
  scaffoldSandcastleConfig(sandcastleTemplateDir(), target);
9171
9414
  return target;
9172
9415
  }
@@ -9202,10 +9445,10 @@ function createTokenVerifier(exec) {
9202
9445
  function fileAfkTokenIo(exec) {
9203
9446
  const path = envPath();
9204
9447
  return {
9205
- read: () => existsSync17(path) ? readFileSync7(path, "utf8") : "",
9448
+ read: () => existsSync18(path) ? readFileSync9(path, "utf8") : "",
9206
9449
  write: (content) => {
9207
- mkdirSync8(dirname8(path), { recursive: true });
9208
- writeFileSync4(path, content, "utf8");
9450
+ mkdirSync9(dirname10(path), { recursive: true });
9451
+ writeFileSync5(path, content, "utf8");
9209
9452
  },
9210
9453
  verify: createTokenVerifier(exec)
9211
9454
  };
@@ -9258,110 +9501,6 @@ var init_setup2 = __esm(() => {
9258
9501
  });
9259
9502
  });
9260
9503
 
9261
- // src/version.ts
9262
- import { readFileSync as readFileSync8 } from "fs";
9263
- import { dirname as dirname9, join as join17 } from "path";
9264
- import { fileURLToPath as fileURLToPath7 } from "url";
9265
- function runcastleVersion() {
9266
- if (cached2)
9267
- return cached2;
9268
- const here = dirname9(fileURLToPath7(import.meta.url));
9269
- for (const pkgPath of [join17(here, "..", "package.json"), join17(here, "package.json")]) {
9270
- try {
9271
- const pkg = JSON.parse(readFileSync8(pkgPath, "utf8"));
9272
- if (typeof pkg.version === "string") {
9273
- cached2 = pkg.version;
9274
- return cached2;
9275
- }
9276
- } catch {}
9277
- }
9278
- cached2 = UNKNOWN_VERSION;
9279
- return cached2;
9280
- }
9281
- var cached2, UNKNOWN_VERSION = "0.0.0";
9282
- var init_version = () => {};
9283
-
9284
- // src/services/update-check.ts
9285
- function parse(version) {
9286
- const cleaned = version.trim().replace(/^v/, "");
9287
- const [core = "", pre = ""] = cleaned.split("-", 2);
9288
- const release2 = core.split(".").map((n) => Number.parseInt(n, 10) || 0);
9289
- return { release: release2, pre: pre ? pre.split(".") : [] };
9290
- }
9291
- function compareSemver(a, b) {
9292
- const pa = parse(a);
9293
- const pb = parse(b);
9294
- const len = Math.max(pa.release.length, pb.release.length);
9295
- for (let i = 0;i < len; i++) {
9296
- const diff = (pa.release[i] ?? 0) - (pb.release[i] ?? 0);
9297
- if (diff !== 0)
9298
- return diff;
9299
- }
9300
- if (pa.pre.length === 0 && pb.pre.length === 0)
9301
- return 0;
9302
- if (pa.pre.length === 0)
9303
- return 1;
9304
- if (pb.pre.length === 0)
9305
- return -1;
9306
- const preLen = Math.max(pa.pre.length, pb.pre.length);
9307
- for (let i = 0;i < preLen; i++) {
9308
- const x = pa.pre[i];
9309
- const y = pb.pre[i];
9310
- if (x === undefined)
9311
- return -1;
9312
- if (y === undefined)
9313
- return 1;
9314
- const nx = Number(x);
9315
- const ny = Number(y);
9316
- const bothNumeric = !Number.isNaN(nx) && !Number.isNaN(ny);
9317
- const cmp = bothNumeric ? nx - ny : x < y ? -1 : x > y ? 1 : 0;
9318
- if (cmp !== 0)
9319
- return cmp;
9320
- }
9321
- return 0;
9322
- }
9323
- async function checkForUpdate(opts) {
9324
- const { current: current2 } = opts;
9325
- const fetchImpl = opts.fetchImpl ?? fetch;
9326
- if (current2 === UNKNOWN_VERSION)
9327
- return NO_UPDATE(current2);
9328
- try {
9329
- const res = await fetchImpl(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`);
9330
- if (!res.ok)
9331
- return NO_UPDATE(current2);
9332
- const body = await res.json();
9333
- const latest = typeof body.version === "string" ? body.version : null;
9334
- if (!latest)
9335
- return NO_UPDATE(current2);
9336
- return {
9337
- current: current2,
9338
- latest,
9339
- updateAvailable: compareSemver(latest, current2) > 0,
9340
- command: UPDATE_COMMAND
9341
- };
9342
- } catch {
9343
- return NO_UPDATE(current2);
9344
- }
9345
- }
9346
- async function getUpdateInfo(current2, fetchImpl) {
9347
- if (cache)
9348
- return cache;
9349
- const info = await checkForUpdate({ current: current2, fetchImpl });
9350
- if (info.latest !== null)
9351
- cache = info;
9352
- return info;
9353
- }
9354
- var PACKAGE_NAME = "runcastle", UPDATE_COMMAND, NO_UPDATE = (current2) => ({
9355
- current: current2,
9356
- latest: null,
9357
- updateAvailable: false,
9358
- command: UPDATE_COMMAND
9359
- }), cache;
9360
- var init_update_check = __esm(() => {
9361
- init_version();
9362
- UPDATE_COMMAND = `bun add -g ${PACKAGE_NAME}@latest`;
9363
- });
9364
-
9365
9504
  // src/trpc/routers/system.ts
9366
9505
  var systemRouter;
9367
9506
  var init_system = __esm(() => {
@@ -9375,8 +9514,8 @@ var init_system = __esm(() => {
9375
9514
  });
9376
9515
 
9377
9516
  // src/services/test-notes.ts
9378
- import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync5 } from "fs";
9379
- import { dirname as dirname10 } from "path";
9517
+ import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync6 } from "fs";
9518
+ import { dirname as dirname11 } from "path";
9380
9519
  import { asc as asc4, eq as eq14 } from "drizzle-orm";
9381
9520
  function rowToNote(row) {
9382
9521
  return {
@@ -9521,8 +9660,8 @@ function renderTestNotes(ctx, feature) {
9521
9660
  lines.push(noteLine(ctx, note));
9522
9661
  }
9523
9662
  const path = featureDocPath(projectForFeature(ctx, feature), feature, "test-notes.md");
9524
- mkdirSync9(dirname10(path), { recursive: true });
9525
- writeFileSync5(path, `${lines.join(`
9663
+ mkdirSync10(dirname11(path), { recursive: true });
9664
+ writeFileSync6(path, `${lines.join(`
9526
9665
  `)}
9527
9666
  `, "utf8");
9528
9667
  }
@@ -9721,6 +9860,7 @@ async function startServer() {
9721
9860
  websocket: terminalWebSocket
9722
9861
  });
9723
9862
  console.log(`runcastle${process.env.RUNCASTLE_DEV ? " [dev]" : ""} server listening on ` + `http://localhost:${config2.serverPort} \u2014 data dir: ${dataDir()}`);
9863
+ getUpdateInfo(runcastleVersion()).catch(() => {});
9724
9864
  const SHUTDOWN_KEY = Symbol.for("runcastle.shutdown.wired");
9725
9865
  const g = globalThis;
9726
9866
  if (!g[SHUTDOWN_KEY]) {
@@ -9746,7 +9886,9 @@ var init_src2 = __esm(() => {
9746
9886
  init_hooks();
9747
9887
  init_stream();
9748
9888
  init_web();
9889
+ init_update_check();
9749
9890
  init_router();
9891
+ init_version();
9750
9892
  init_reconcile_runs();
9751
9893
  });
9752
9894
  init_src2();