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