runcastle 1.0.18 → 1.0.19

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
@@ -5430,9 +5430,143 @@ var init_feature = __esm(() => {
5430
5430
  });
5431
5431
  });
5432
5432
 
5433
+ // src/services/fsbrowse.ts
5434
+ import { existsSync as existsSync14, readdirSync as readdirSync5, statSync as statSync2 } from "fs";
5435
+ import { homedir as homedir2 } from "os";
5436
+ import { basename, dirname as dirname6, isAbsolute, join as join14, resolve as resolve8, sep as sep3 } from "path";
5437
+ function expandPath(input, home = homedir2()) {
5438
+ const trimmed = input.trim();
5439
+ if (trimmed === "")
5440
+ return trimmed;
5441
+ let out = trimmed;
5442
+ if (out === "~")
5443
+ out = home;
5444
+ else if (out.startsWith("~/") || out.startsWith("~\\"))
5445
+ out = join14(home, out.slice(2));
5446
+ return resolve8(out);
5447
+ }
5448
+ function looksLikeRepo(dir) {
5449
+ try {
5450
+ return existsSync14(join14(dir, ".git"));
5451
+ } catch {
5452
+ return false;
5453
+ }
5454
+ }
5455
+ function listRoots(home = homedir2()) {
5456
+ const roots = [];
5457
+ if (process.platform === "win32") {
5458
+ for (let code = 67;code <= 90; code++) {
5459
+ const drive = `${String.fromCharCode(code)}:${sep3}`;
5460
+ try {
5461
+ if (existsSync14(drive))
5462
+ roots.push({ label: drive.replace(/\\$/, ""), path: drive, kind: "drive" });
5463
+ } catch {}
5464
+ }
5465
+ } else {
5466
+ roots.push({ label: "/", path: "/", kind: "drive" });
5467
+ }
5468
+ if (existsSync14(home))
5469
+ roots.push({ label: "Home", path: home, kind: "home" });
5470
+ const seen = new Set(roots.map(pathKey));
5471
+ for (const name of COMMON_DIRS) {
5472
+ const candidate = join14(home, name);
5473
+ const key = pathKey({ path: candidate });
5474
+ if (seen.has(key) || !existsSync14(candidate))
5475
+ continue;
5476
+ seen.add(key);
5477
+ roots.push({ label: name, path: candidate, kind: "common" });
5478
+ }
5479
+ return roots;
5480
+ }
5481
+ function pathKey(r) {
5482
+ return process.platform === "win32" ? r.path.toLowerCase() : r.path;
5483
+ }
5484
+ function crumbsFor(dir) {
5485
+ const out = [];
5486
+ let cur = dir;
5487
+ for (;; ) {
5488
+ out.unshift({ name: basename(cur) || cur, path: cur });
5489
+ const parent = dirname6(cur);
5490
+ if (parent === cur)
5491
+ break;
5492
+ cur = parent;
5493
+ }
5494
+ return out;
5495
+ }
5496
+ function parentOf(dir) {
5497
+ const parent = dirname6(dir);
5498
+ return parent === dir ? null : parent;
5499
+ }
5500
+ function browseDir(input, showHidden = false, home = homedir2()) {
5501
+ const target = input && input.trim() !== "" ? expandPath(input, home) : home;
5502
+ if (!isAbsolute(target)) {
5503
+ throw new InvalidInputError(`path is not absolute: ${target}`);
5504
+ }
5505
+ if (!existsSync14(target)) {
5506
+ throw new InvalidInputError(`path does not exist: ${target}`);
5507
+ }
5508
+ let dir = target;
5509
+ try {
5510
+ if (!statSync2(target).isDirectory())
5511
+ dir = dirname6(target);
5512
+ } catch (e) {
5513
+ throw new InvalidInputError(`cannot read path: ${target} (${errMsg2(e)})`);
5514
+ }
5515
+ let dirents;
5516
+ try {
5517
+ dirents = readdirSync5(dir, { withFileTypes: true, encoding: "utf8" });
5518
+ } catch (e) {
5519
+ throw new InvalidInputError(`cannot read directory: ${dir} (${errMsg2(e)})`);
5520
+ }
5521
+ const entries = [];
5522
+ let truncated = false;
5523
+ for (const dirent of dirents) {
5524
+ if (entries.length >= MAX_ENTRIES) {
5525
+ truncated = true;
5526
+ break;
5527
+ }
5528
+ const name = dirent.name;
5529
+ if (SKIP_ALWAYS.has(name))
5530
+ continue;
5531
+ if (!showHidden && name.startsWith("."))
5532
+ continue;
5533
+ const path = join14(dir, name);
5534
+ const isSymlink = dirent.isSymbolicLink();
5535
+ let isDirectory = dirent.isDirectory();
5536
+ if (isSymlink) {
5537
+ try {
5538
+ isDirectory = statSync2(path).isDirectory();
5539
+ } catch {
5540
+ continue;
5541
+ }
5542
+ }
5543
+ if (!isDirectory)
5544
+ continue;
5545
+ entries.push({ name, path, isRepo: looksLikeRepo(path), isSymlink });
5546
+ }
5547
+ entries.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: "base" }));
5548
+ return {
5549
+ dir,
5550
+ parent: parentOf(dir),
5551
+ crumbs: crumbsFor(dir),
5552
+ entries,
5553
+ isRepo: looksLikeRepo(dir),
5554
+ truncated
5555
+ };
5556
+ }
5557
+ function errMsg2(e) {
5558
+ return e instanceof Error ? e.message : String(e);
5559
+ }
5560
+ var SKIP_ALWAYS, MAX_ENTRIES = 1000, COMMON_DIRS;
5561
+ var init_fsbrowse = __esm(() => {
5562
+ init_errors();
5563
+ SKIP_ALWAYS = new Set(["node_modules", "$RECYCLE.BIN", "System Volume Information"]);
5564
+ COMMON_DIRS = ["Projects", "projects", "code", "Code", "src", "dev", "repos", "git"];
5565
+ });
5566
+
5433
5567
  // src/services/projects.ts
5434
- import { existsSync as existsSync14 } from "fs";
5435
- import { basename, join as join14 } from "path";
5568
+ import { existsSync as existsSync15 } from "fs";
5569
+ import { basename as basename2, join as join15 } from "path";
5436
5570
  import { eq as eq9, isNull } from "drizzle-orm";
5437
5571
  function listProjects(ctx) {
5438
5572
  return ctx.db.select().from(projects).where(isNull(projects.closedAt)).all().map(rowToProject);
@@ -5448,14 +5582,15 @@ function warnIfTranslatedMount(ctx, projectId, repoPath) {
5448
5582
  message: `${repoPath} is a Windows drive mounted into Linux (/mnt) \u2014 git and burns pay a per-file translation tax here; clone the repo into the Linux filesystem (e.g. ~/projects) for native speed`
5449
5583
  });
5450
5584
  }
5451
- async function openProject(ctx, repoPath) {
5585
+ async function openProject(ctx, rawPath) {
5586
+ const repoPath = expandPath(rawPath);
5452
5587
  await assertRepoTolerant(ctx, repoPath);
5453
5588
  const mainBranch = await detectMainBranchTolerant(ctx, repoPath);
5454
- const name = basename(repoPath) || repoPath;
5455
- const existing = getProjectByRepoPath(ctx, repoPath);
5589
+ const name = basename2(repoPath) || repoPath;
5590
+ const existing = getProjectByRepoPath(ctx, repoPath) ?? findProjectByCanonPath(ctx, repoPath);
5456
5591
  if (existing) {
5457
- ctx.db.update(projects).set({ mainBranch, closedAt: null }).where(eq9(projects.id, existing.id)).run();
5458
- const reopened = { ...existing, mainBranch };
5592
+ ctx.db.update(projects).set({ mainBranch, repoPath, closedAt: null }).where(eq9(projects.id, existing.id)).run();
5593
+ const reopened = { ...existing, mainBranch, repoPath };
5459
5594
  emitProject(ctx, existing.id, {
5460
5595
  type: "project.opened",
5461
5596
  message: `project ${existing.name} re-opened at ${repoPath} (${mainBranch})`
@@ -5502,13 +5637,17 @@ function renameProject(ctx, projectId, name) {
5502
5637
  });
5503
5638
  return { ...project, name };
5504
5639
  }
5640
+ function findProjectByCanonPath(ctx, repoPath) {
5641
+ const key = canon(repoPath);
5642
+ return allProjects(ctx).find((p) => canon(p.repoPath) === key) ?? null;
5643
+ }
5505
5644
  async function assertRepoTolerant(ctx, repoPath) {
5506
5645
  try {
5507
5646
  await assertRepo(repoPath);
5508
5647
  } catch (e) {
5509
5648
  if (!isNotImplemented(e))
5510
5649
  throw e;
5511
- if (!existsSync14(join14(repoPath, ".git"))) {
5650
+ if (!existsSync15(join15(repoPath, ".git"))) {
5512
5651
  throw new InvalidInputError(`not a git repository: ${repoPath}`);
5513
5652
  }
5514
5653
  }
@@ -5527,6 +5666,7 @@ var init_projects = __esm(() => {
5527
5666
  init_schema();
5528
5667
  init_errors();
5529
5668
  init_events();
5669
+ init_fsbrowse();
5530
5670
  init_git();
5531
5671
  init_repo();
5532
5672
  });
@@ -5535,12 +5675,15 @@ var init_projects = __esm(() => {
5535
5675
  import * as z7 from "zod";
5536
5676
  var projectRouter;
5537
5677
  var init_project = __esm(() => {
5678
+ init_fsbrowse();
5538
5679
  init_git();
5539
5680
  init_projects();
5540
5681
  init_repo();
5541
5682
  init_context();
5542
5683
  projectRouter = router({
5543
5684
  list: publicProcedure.query(({ ctx }) => listProjects(ctx)),
5685
+ roots: publicProcedure.query(() => listRoots()),
5686
+ browse: publicProcedure.input(z7.object({ dir: z7.string().optional(), showHidden: z7.boolean().optional() }).optional()).query(({ input }) => browseDir(input?.dir, input?.showHidden ?? false)),
5544
5687
  branches: publicProcedure.input(z7.object({ projectId: z7.string() })).query(({ ctx, input }) => listBranches(requireProjectById(ctx, input.projectId))),
5545
5688
  open: publicProcedure.input(z7.object({ repoPath: z7.string().min(1) })).mutation(({ ctx, input }) => openProject(ctx, input.repoPath)),
5546
5689
  close: publicProcedure.input(z7.object({ projectId: z7.string() })).mutation(({ ctx, input }) => closeProject(ctx, input.projectId)),
@@ -5567,12 +5710,12 @@ var init_run = __esm(() => {
5567
5710
  });
5568
5711
 
5569
5712
  // src/services/settings.ts
5570
- import { existsSync as existsSync15, mkdirSync as mkdirSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
5571
- import { dirname as dirname6 } from "path";
5713
+ import { existsSync as existsSync16, mkdirSync as mkdirSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
5714
+ import { dirname as dirname7 } from "path";
5572
5715
  import * as z9 from "zod";
5573
5716
  import { eq as eq10 } from "drizzle-orm";
5574
5717
  function readRawConfig(configFile) {
5575
- if (!existsSync15(configFile))
5718
+ if (!existsSync16(configFile))
5576
5719
  return {};
5577
5720
  try {
5578
5721
  const parsed = JSON.parse(readFileSync6(configFile, "utf8"));
@@ -5694,7 +5837,7 @@ function updateSettings(ctx, input, io = {}) {
5694
5837
  function writeGlobal(configFile, configKey, value) {
5695
5838
  const raw = readRawConfig(configFile);
5696
5839
  raw[configKey] = value;
5697
- mkdirSync7(dirname6(configFile), { recursive: true });
5840
+ mkdirSync7(dirname7(configFile), { recursive: true });
5698
5841
  writeFileSync3(configFile, `${JSON.stringify(raw, null, 2)}
5699
5842
  `);
5700
5843
  }
@@ -5733,7 +5876,7 @@ function writeStepModel(configFile, step, value) {
5733
5876
  else
5734
5877
  stepModels[step] = value;
5735
5878
  raw.stepModels = stepModels;
5736
- mkdirSync7(dirname6(configFile), { recursive: true });
5879
+ mkdirSync7(dirname7(configFile), { recursive: true });
5737
5880
  writeFileSync3(configFile, `${JSON.stringify(raw, null, 2)}
5738
5881
  `);
5739
5882
  }
@@ -6015,7 +6158,7 @@ var init_doctor = __esm(() => {
6015
6158
  // src/doctor/system-exec.ts
6016
6159
  import { spawn as spawn2 } from "child_process";
6017
6160
  function createSystemExec(opts = {}) {
6018
- return (command, args) => new Promise((resolve8) => {
6161
+ return (command, args) => new Promise((resolve9) => {
6019
6162
  const resolved = resolveExecutable(command);
6020
6163
  const isShim = /\.(cmd|bat)$/i.test(resolved);
6021
6164
  const file = isShim ? process.env.ComSpec ?? "cmd.exe" : resolved;
@@ -6030,10 +6173,10 @@ function createSystemExec(opts = {}) {
6030
6173
  stderr += d.toString();
6031
6174
  });
6032
6175
  child.on("error", (err) => {
6033
- resolve8({ ok: false, code: null, stdout, stderr: stderr || String(err) });
6176
+ resolve9({ ok: false, code: null, stdout, stderr: stderr || String(err) });
6034
6177
  });
6035
6178
  child.on("close", (code) => {
6036
- resolve8({ ok: true, code: code ?? 0, stdout, stderr });
6179
+ resolve9({ ok: true, code: code ?? 0, stdout, stderr });
6037
6180
  });
6038
6181
  });
6039
6182
  }
@@ -6042,8 +6185,8 @@ var init_system_exec = __esm(() => {
6042
6185
  });
6043
6186
 
6044
6187
  // src/services/setup.ts
6045
- import { copyFileSync, existsSync as existsSync16, mkdirSync as mkdirSync8, readFileSync as readFileSync7, readdirSync as readdirSync5, writeFileSync as writeFileSync4 } from "fs";
6046
- import { dirname as dirname7, join as join15 } from "path";
6188
+ import { copyFileSync, existsSync as existsSync17, mkdirSync as mkdirSync8, readFileSync as readFileSync7, readdirSync as readdirSync6, writeFileSync as writeFileSync4 } from "fs";
6189
+ import { dirname as dirname8, join as join16 } from "path";
6047
6190
  import { fileURLToPath as fileURLToPath6 } from "url";
6048
6191
  async function writeGitIdentity(exec, identity) {
6049
6192
  const name = identity.name.trim();
@@ -6119,16 +6262,16 @@ async function resolveRuntime(exec, preferred) {
6119
6262
  function resolveSandcastleBin() {
6120
6263
  try {
6121
6264
  const mainUrl = import.meta.resolve("@ai-hero/sandcastle");
6122
- let dir = dirname7(fileURLToPath6(mainUrl));
6265
+ let dir = dirname8(fileURLToPath6(mainUrl));
6123
6266
  for (let hops = 0;hops < 6; hops++) {
6124
- const manifestPath = join15(dir, "package.json");
6125
- if (existsSync16(manifestPath)) {
6267
+ const manifestPath = join16(dir, "package.json");
6268
+ if (existsSync17(manifestPath)) {
6126
6269
  const manifest = JSON.parse(readFileSync7(manifestPath, "utf8"));
6127
6270
  if (manifest.name === "@ai-hero/sandcastle" && manifest.bin?.sandcastle) {
6128
- return join15(dir, manifest.bin.sandcastle);
6271
+ return join16(dir, manifest.bin.sandcastle);
6129
6272
  }
6130
6273
  }
6131
- const parent = dirname7(dir);
6274
+ const parent = dirname8(dir);
6132
6275
  if (parent === dir)
6133
6276
  break;
6134
6277
  dir = parent;
@@ -6153,12 +6296,12 @@ function sandcastleTemplateDir() {
6153
6296
  return resolveAsset(ASSET_ENV.sandcastleTemplate, fileURLToPath6(new URL("../assets/sandcastle", import.meta.url)));
6154
6297
  }
6155
6298
  function scaffoldSandcastleConfig(templateDir, targetDir) {
6156
- const dir = join15(targetDir, ".sandcastle");
6157
- if (existsSync16(dir))
6299
+ const dir = join16(targetDir, ".sandcastle");
6300
+ if (existsSync17(dir))
6158
6301
  return { scaffolded: false, dir };
6159
6302
  mkdirSync8(dir, { recursive: true });
6160
- for (const name of readdirSync5(templateDir)) {
6161
- copyFileSync(join15(templateDir, name), join15(dir, name));
6303
+ for (const name of readdirSync6(templateDir)) {
6304
+ copyFileSync(join16(templateDir, name), join16(dir, name));
6162
6305
  }
6163
6306
  return { scaffolded: true, dir };
6164
6307
  }
@@ -6183,9 +6326,9 @@ function createTokenVerifier(exec) {
6183
6326
  function fileAfkTokenIo(exec) {
6184
6327
  const path = envPath();
6185
6328
  return {
6186
- read: () => existsSync16(path) ? readFileSync7(path, "utf8") : "",
6329
+ read: () => existsSync17(path) ? readFileSync7(path, "utf8") : "",
6187
6330
  write: (content) => {
6188
- mkdirSync8(dirname7(path), { recursive: true });
6331
+ mkdirSync8(dirname8(path), { recursive: true });
6189
6332
  writeFileSync4(path, content, "utf8");
6190
6333
  },
6191
6334
  verify: createTokenVerifier(exec)
@@ -6322,13 +6465,13 @@ var init_update_check = __esm(() => {
6322
6465
 
6323
6466
  // src/version.ts
6324
6467
  import { readFileSync as readFileSync8 } from "fs";
6325
- import { dirname as dirname8, join as join16 } from "path";
6468
+ import { dirname as dirname9, join as join17 } from "path";
6326
6469
  import { fileURLToPath as fileURLToPath7 } from "url";
6327
6470
  function runcastleVersion() {
6328
6471
  if (cached2)
6329
6472
  return cached2;
6330
- const here = dirname8(fileURLToPath7(import.meta.url));
6331
- for (const pkgPath of [join16(here, "..", "package.json"), join16(here, "package.json")]) {
6473
+ const here = dirname9(fileURLToPath7(import.meta.url));
6474
+ for (const pkgPath of [join17(here, "..", "package.json"), join17(here, "package.json")]) {
6332
6475
  try {
6333
6476
  const pkg = JSON.parse(readFileSync8(pkgPath, "utf8"));
6334
6477
  if (typeof pkg.version === "string") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runcastle",
3
- "version": "1.0.18",
3
+ "version": "1.0.19",
4
4
  "description": "Burn tickets into shipped features with Claude Code.",
5
5
  "license": "MIT",
6
6
  "type": "module",