@staff0rd/assist 0.748.2 → 0.750.0

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/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import { Command } from "commander";
6
6
  // package.json
7
7
  var package_default = {
8
8
  name: "@staff0rd/assist",
9
- version: "0.748.2",
9
+ version: "0.750.0",
10
10
  type: "module",
11
11
  main: "dist/index.js",
12
12
  bin: {
@@ -12903,8 +12903,8 @@ var seqConfigHelp = [
12903
12903
  var nodesConfigHelp = [
12904
12904
  {
12905
12905
  key: "sessions.links",
12906
- setter: "assist sessions nodes link <name> http://127.0.0.1:<port> (or --ssh <alias> --port <port>)",
12907
- note: "linked nodes whose sessions merge into this node's web UI, each direct (url) or over an ssh tunnel (ssh alias, peer port, localPort); each name must match the peer's sessions.nodeName"
12906
+ setter: "assist sessions nodes link <name> http://127.0.0.1:<port> (or --tailscale <host> --port <port>, or --ssh <alias> --port <port>)",
12907
+ note: "linked nodes whose sessions merge into this node's web UI, each direct (url, including a Tailscale <host>.<tailnet>.ts.net https url) or over an ssh tunnel (ssh alias, peer port, localPort); each name must match the peer's sessions.nodeName"
12908
12908
  },
12909
12909
  {
12910
12910
  key: "sessions.linkVersionCheck",
@@ -13593,8 +13593,8 @@ function subsequenceScore(text18, query) {
13593
13593
  function scoreFilePath(path106, query) {
13594
13594
  const needle = query.trim().toLowerCase();
13595
13595
  if (!needle) return 0;
13596
- const basename31 = path106.slice(path106.lastIndexOf("/") + 1);
13597
- const inBasename = subsequenceScore(basename31, needle);
13596
+ const basename32 = path106.slice(path106.lastIndexOf("/") + 1);
13597
+ const inBasename = subsequenceScore(basename32, needle);
13598
13598
  if (inBasename !== null) return BASENAME_WEIGHT + inBasename;
13599
13599
  return subsequenceScore(path106, needle);
13600
13600
  }
@@ -20685,7 +20685,7 @@ async function criteriaExtension() {
20685
20685
  // src/commands/criteriaExtension/signCriteriaExtension.ts
20686
20686
  import { rm as rm3 } from "fs/promises";
20687
20687
  import { homedir as homedir19, tmpdir as tmpdir4 } from "os";
20688
- import { join as join51 } from "path";
20688
+ import { join as join52 } from "path";
20689
20689
  import chalk139 from "chalk";
20690
20690
 
20691
20691
  // src/commands/criteriaExtension/copySignedXpi.ts
@@ -20747,7 +20747,7 @@ function signPreflightProblem(source) {
20747
20747
 
20748
20748
  // src/commands/criteriaExtension/stageCriteriaExtension.ts
20749
20749
  import { cp as cp3, mkdir as mkdir4, readFile as readFile4, rm as rm2, writeFile as writeFile4 } from "fs/promises";
20750
- import { join as join50 } from "path";
20750
+ import { basename as basename13, join as join50 } from "path";
20751
20751
 
20752
20752
  // src/commands/criteriaExtension/stampManifestVersion.ts
20753
20753
  function stampManifestVersion(manifest, version2) {
@@ -20760,7 +20760,10 @@ function stampManifestVersion(manifest, version2) {
20760
20760
  async function stageCriteriaExtension(source, staging, version2) {
20761
20761
  await rm2(staging, { recursive: true, force: true });
20762
20762
  await mkdir4(staging, { recursive: true });
20763
- await cp3(source, staging, { recursive: true });
20763
+ await cp3(source, staging, {
20764
+ recursive: true,
20765
+ filter: (path106) => basename13(path106) !== "updates.json"
20766
+ });
20764
20767
  const manifest = join50(staging, "manifest.json");
20765
20768
  const stamped = stampManifestVersion(
20766
20769
  await readFile4(manifest, "utf8"),
@@ -20769,9 +20772,44 @@ async function stageCriteriaExtension(source, staging, version2) {
20769
20772
  await writeFile4(manifest, stamped);
20770
20773
  }
20771
20774
 
20775
+ // src/commands/criteriaExtension/writeUpdateManifest.ts
20776
+ import { readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
20777
+ import { join as join51 } from "path";
20778
+
20779
+ // src/commands/criteriaExtension/buildUpdateManifest.ts
20780
+ var RELEASE_DOWNLOAD_URL = "https://github.com/staff0rd/assist/releases/download";
20781
+ function buildUpdateManifest(manifest, version2) {
20782
+ const { id } = JSON.parse(manifest).browser_specific_settings.gecko;
20783
+ const updates = {
20784
+ addons: {
20785
+ [id]: {
20786
+ updates: [
20787
+ {
20788
+ version: version2,
20789
+ update_link: `${RELEASE_DOWNLOAD_URL}/v${version2}/criteria-extension.xpi`
20790
+ }
20791
+ ]
20792
+ }
20793
+ }
20794
+ };
20795
+ return `${JSON.stringify(updates, null, " ")}
20796
+ `;
20797
+ }
20798
+
20799
+ // src/commands/criteriaExtension/writeUpdateManifest.ts
20800
+ async function writeUpdateManifest(source, dir, version2) {
20801
+ await writeFile5(
20802
+ join51(dir, "updates.json"),
20803
+ buildUpdateManifest(
20804
+ await readFile5(join51(source, "manifest.json"), "utf8"),
20805
+ version2
20806
+ )
20807
+ );
20808
+ }
20809
+
20772
20810
  // src/commands/criteriaExtension/signCriteriaExtension.ts
20773
- var ARTIFACTS_DIR = join51(homedir19(), ".assist", "criteria-extension");
20774
- var STAGING_DIR = join51(tmpdir4(), "assist-criteria-extension");
20811
+ var ARTIFACTS_DIR = join52(homedir19(), ".assist", "criteria-extension");
20812
+ var STAGING_DIR = join52(tmpdir4(), "assist-criteria-extension");
20775
20813
  async function signCriteriaExtension() {
20776
20814
  const source = criteriaExtensionDir();
20777
20815
  const problem = signPreflightProblem(source);
@@ -20804,6 +20842,7 @@ async function signCriteriaExtension() {
20804
20842
  process.exitCode = 1;
20805
20843
  return;
20806
20844
  }
20845
+ await writeUpdateManifest(source, ARTIFACTS_DIR, version2);
20807
20846
  const path106 = await signedAddonInstallPath(
20808
20847
  await copySignedXpi(xpi, ARTIFACTS_DIR)
20809
20848
  );
@@ -20894,12 +20933,12 @@ import { unlinkSync as unlinkSync9, writeFileSync as writeFileSync26 } from "fs"
20894
20933
  import chalk141 from "chalk";
20895
20934
 
20896
20935
  // src/commands/dbMigration/getMigrationPinPath.ts
20897
- import { join as join52 } from "path";
20936
+ import { join as join53 } from "path";
20898
20937
  function getMigrationPinPath(pin2) {
20899
- return join52(getRestrictedDir(), `db-migration-pin-${pin2}.json`);
20938
+ return join53(getRestrictedDir(), `db-migration-pin-${pin2}.json`);
20900
20939
  }
20901
20940
  function getMigrationApprovalPath(migrationId) {
20902
- return join52(getRestrictedDir(), `db-migration-approval-${migrationId}.json`);
20941
+ return join53(getRestrictedDir(), `db-migration-approval-${migrationId}.json`);
20903
20942
  }
20904
20943
 
20905
20944
  // src/commands/dbMigration/readMigrationPinState.ts
@@ -21033,14 +21072,14 @@ function registerDeploy(program2) {
21033
21072
 
21034
21073
  // src/commands/devlog/list.ts
21035
21074
  import { execFileSync as execFileSync5 } from "child_process";
21036
- import { basename as basename14 } from "path";
21075
+ import { basename as basename15 } from "path";
21037
21076
 
21038
21077
  // src/commands/devlog/loadBlogSkipDays.ts
21039
21078
  import { homedir as homedir20 } from "os";
21040
- import { join as join53 } from "path";
21041
- var BLOG_REPO_ROOT = join53(homedir20(), "git/blog");
21079
+ import { join as join54 } from "path";
21080
+ var BLOG_REPO_ROOT = join54(homedir20(), "git/blog");
21042
21081
  function loadBlogSkipDays(repoName) {
21043
- const config = loadRawYaml(join53(BLOG_REPO_ROOT, "assist.yml"));
21082
+ const config = loadRawYaml(join54(BLOG_REPO_ROOT, "assist.yml"));
21044
21083
  const devlog = config.devlog;
21045
21084
  const skip2 = devlog?.skip;
21046
21085
  return new Set(skip2?.[repoName]);
@@ -21052,13 +21091,13 @@ import chalk144 from "chalk";
21052
21091
 
21053
21092
  // src/shared/getRepoName.ts
21054
21093
  import { existsSync as existsSync50, readFileSync as readFileSync33 } from "fs";
21055
- import { basename as basename13, join as join54 } from "path";
21094
+ import { basename as basename14, join as join55 } from "path";
21056
21095
  function getRepoName() {
21057
21096
  const config = loadConfig();
21058
21097
  if (config.devlog?.name) {
21059
21098
  return config.devlog.name;
21060
21099
  }
21061
- const packageJsonPath = join54(process.cwd(), "package.json");
21100
+ const packageJsonPath = join55(process.cwd(), "package.json");
21062
21101
  if (existsSync50(packageJsonPath)) {
21063
21102
  try {
21064
21103
  const content = readFileSync33(packageJsonPath, "utf8");
@@ -21069,13 +21108,13 @@ function getRepoName() {
21069
21108
  } catch {
21070
21109
  }
21071
21110
  }
21072
- return basename13(process.cwd());
21111
+ return basename14(process.cwd());
21073
21112
  }
21074
21113
 
21075
21114
  // src/commands/devlog/loadDevlogEntries.ts
21076
21115
  import { readdirSync as readdirSync6, readFileSync as readFileSync34 } from "fs";
21077
- import { join as join55 } from "path";
21078
- var DEVLOG_DIR = join55(BLOG_REPO_ROOT, "src/content/devlog");
21116
+ import { join as join56 } from "path";
21117
+ var DEVLOG_DIR = join56(BLOG_REPO_ROOT, "src/content/devlog");
21079
21118
  function extractFrontmatter(content) {
21080
21119
  const fm = content.match(/^---\n([\s\S]*?)\n---/);
21081
21120
  return fm?.[1] ?? null;
@@ -21103,7 +21142,7 @@ function readDevlogFiles(callback) {
21103
21142
  try {
21104
21143
  const files = readdirSync6(DEVLOG_DIR).filter((f) => f.endsWith(".md"));
21105
21144
  for (const file of files) {
21106
- const content = readFileSync34(join55(DEVLOG_DIR, file), "utf8");
21145
+ const content = readFileSync34(join56(DEVLOG_DIR, file), "utf8");
21107
21146
  const parsed = parseFrontmatter(content, file);
21108
21147
  if (parsed) callback(parsed);
21109
21148
  }
@@ -21205,7 +21244,7 @@ function list2(options2) {
21205
21244
  const config = loadConfig();
21206
21245
  const days = options2.days ?? 30;
21207
21246
  const ignore3 = options2.ignore ?? config.devlog?.ignore ?? [];
21208
- const repoName = basename14(process.cwd());
21247
+ const repoName = basename15(process.cwd());
21209
21248
  const skipDays = loadBlogSkipDays(repoName);
21210
21249
  const devlogEntries = loadDevlogEntries(repoName);
21211
21250
  const args = ["log"];
@@ -21491,11 +21530,11 @@ function repos(options2) {
21491
21530
 
21492
21531
  // src/commands/devlog/skip.ts
21493
21532
  import { writeFileSync as writeFileSync29 } from "fs";
21494
- import { join as join56 } from "path";
21533
+ import { join as join57 } from "path";
21495
21534
  import chalk149 from "chalk";
21496
21535
  import { stringify as stringifyYaml3 } from "yaml";
21497
21536
  function getBlogConfigPath() {
21498
- return join56(BLOG_REPO_ROOT, "assist.yml");
21537
+ return join57(BLOG_REPO_ROOT, "assist.yml");
21499
21538
  }
21500
21539
  function skip(date) {
21501
21540
  if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
@@ -21557,7 +21596,7 @@ function registerDevlog(program2) {
21557
21596
 
21558
21597
  // src/commands/dotnet/checkBuildLocks.ts
21559
21598
  import { closeSync as closeSync4, openSync as openSync5, readdirSync as readdirSync7 } from "fs";
21560
- import { join as join57 } from "path";
21599
+ import { join as join58 } from "path";
21561
21600
  import chalk151 from "chalk";
21562
21601
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "packages"]);
21563
21602
  function isLockedDll(debugDir) {
@@ -21569,7 +21608,7 @@ function isLockedDll(debugDir) {
21569
21608
  }
21570
21609
  for (const file of files) {
21571
21610
  if (!file.toLowerCase().endsWith(".dll")) continue;
21572
- const dllPath = join57(debugDir, file);
21611
+ const dllPath = join58(debugDir, file);
21573
21612
  try {
21574
21613
  const fd = openSync5(dllPath, "r+");
21575
21614
  closeSync4(fd);
@@ -21587,13 +21626,13 @@ function findFirstLockedDll(dir) {
21587
21626
  return null;
21588
21627
  }
21589
21628
  if (entries.includes("bin")) {
21590
- const locked = isLockedDll(join57(dir, "bin", "Debug"));
21629
+ const locked = isLockedDll(join58(dir, "bin", "Debug"));
21591
21630
  if (locked) return locked;
21592
21631
  }
21593
21632
  for (const entry of entries) {
21594
21633
  if (SKIP_DIRS.has(entry) || entry === "bin" || entry.startsWith("."))
21595
21634
  continue;
21596
- const found = findFirstLockedDll(join57(dir, entry));
21635
+ const found = findFirstLockedDll(join58(dir, entry));
21597
21636
  if (found) return found;
21598
21637
  }
21599
21638
  return null;
@@ -21940,11 +21979,11 @@ import chalk157 from "chalk";
21940
21979
 
21941
21980
  // src/commands/dotnet/findSolution.ts
21942
21981
  import { readdirSync as readdirSync9 } from "fs";
21943
- import { dirname as dirname27, join as join58 } from "path";
21982
+ import { dirname as dirname27, join as join59 } from "path";
21944
21983
  import chalk156 from "chalk";
21945
21984
  function findSlnInDir(dir) {
21946
21985
  try {
21947
- return readdirSync9(dir).filter((f) => f.endsWith(".sln")).map((f) => join58(dir, f));
21986
+ return readdirSync9(dir).filter((f) => f.endsWith(".sln")).map((f) => join59(dir, f));
21948
21987
  } catch {
21949
21988
  return [];
21950
21989
  }
@@ -22939,7 +22978,7 @@ function aggregateCommitters(authorLists) {
22939
22978
  import { spawnSync as spawnSync6 } from "child_process";
22940
22979
  import { unlinkSync as unlinkSync12, writeFileSync as writeFileSync30 } from "fs";
22941
22980
  import { tmpdir as tmpdir6 } from "os";
22942
- import { join as join59 } from "path";
22981
+ import { join as join60 } from "path";
22943
22982
  function buildArgs2(queryFile, vars) {
22944
22983
  const args = ["api", "graphql", "-F", `query=@${queryFile}`];
22945
22984
  for (const [key, value] of Object.entries(vars)) {
@@ -22949,7 +22988,7 @@ function buildArgs2(queryFile, vars) {
22949
22988
  return args;
22950
22989
  }
22951
22990
  function runGhGraphql(mutation, vars) {
22952
- const queryFile = join59(tmpdir6(), `gh-query-${Date.now()}.graphql`);
22991
+ const queryFile = join60(tmpdir6(), `gh-query-${Date.now()}.graphql`);
22953
22992
  writeFileSync30(queryFile, mutation);
22954
22993
  try {
22955
22994
  const result = spawnSync6("gh", buildArgs2(queryFile, vars), {
@@ -24229,14 +24268,14 @@ function fetchIssue2(number, repo) {
24229
24268
  import { existsSync as existsSync55, readFileSync as readFileSync38 } from "fs";
24230
24269
 
24231
24270
  // src/commands/github/issue/issueWorkingFile.ts
24232
- import { join as join60 } from "path";
24271
+ import { join as join61 } from "path";
24233
24272
  function issueWorkingFile(slug2, number) {
24234
24273
  const [owner = "unknown", repo = "unknown"] = slug2.split("/");
24235
- const dir = join60(getStoreDir(), "github-issues", owner, repo);
24274
+ const dir = join61(getStoreDir(), "github-issues", owner, repo);
24236
24275
  return {
24237
24276
  dir,
24238
- bodyPath: join60(dir, `${number}.md`),
24239
- metaPath: join60(dir, `${number}.json`)
24277
+ bodyPath: join61(dir, `${number}.md`),
24278
+ metaPath: join61(dir, `${number}.json`)
24240
24279
  };
24241
24280
  }
24242
24281
 
@@ -24663,18 +24702,18 @@ import {
24663
24702
  rmSync as rmSync3,
24664
24703
  statSync as statSync9
24665
24704
  } from "fs";
24666
- import { basename as basename15, join as join63 } from "path";
24705
+ import { basename as basename16, join as join64 } from "path";
24667
24706
 
24668
24707
  // src/commands/handover/getHandoverPath.ts
24669
- import { join as join61 } from "path";
24708
+ import { join as join62 } from "path";
24670
24709
  function getHandoverPath(cwd = process.cwd()) {
24671
- return join61(cwd, ".assist", "HANDOVER.md");
24710
+ return join62(cwd, ".assist", "HANDOVER.md");
24672
24711
  }
24673
24712
 
24674
24713
  // src/commands/handover/getHandoversDir.ts
24675
- import { join as join62 } from "path";
24714
+ import { join as join63 } from "path";
24676
24715
  function getHandoversDir(cwd = process.cwd()) {
24677
- return join62(cwd, ".assist", "handovers");
24716
+ return join63(cwd, ".assist", "handovers");
24678
24717
  }
24679
24718
 
24680
24719
  // src/commands/handover/parseArchiveTimestamp.ts
@@ -24715,7 +24754,7 @@ function collectMarkdown(dir) {
24715
24754
  if (!existsSync56(dir)) return [];
24716
24755
  const out = [];
24717
24756
  for (const entry of readdirSync10(dir, { withFileTypes: true })) {
24718
- const full = join63(dir, entry.name);
24757
+ const full = join64(dir, entry.name);
24719
24758
  if (entry.isDirectory()) out.push(...collectMarkdown(full));
24720
24759
  else if (entry.isFile() && entry.name.endsWith(".md")) out.push(full);
24721
24760
  }
@@ -24734,7 +24773,7 @@ async function migrateFile(orm, origin, file, createdAt) {
24734
24773
  async function migrateDiskHandovers(orm, origin, cwd = process.cwd()) {
24735
24774
  let migrated = 0;
24736
24775
  for (const file of collectMarkdown(getHandoversDir(cwd))) {
24737
- const createdAt = parseArchiveTimestamp(basename15(file)) ?? statSync9(file).mtime;
24776
+ const createdAt = parseArchiveTimestamp(basename16(file)) ?? statSync9(file).mtime;
24738
24777
  await migrateFile(orm, origin, file, createdAt);
24739
24778
  migrated++;
24740
24779
  }
@@ -25097,17 +25136,17 @@ function canonicalTreePath(path106) {
25097
25136
 
25098
25137
  // src/commands/sessions/daemon/worktree/createWorktree.ts
25099
25138
  import { existsSync as existsSync57 } from "fs";
25100
- import { basename as basename17, dirname as dirname28 } from "path";
25139
+ import { basename as basename18, dirname as dirname28 } from "path";
25101
25140
 
25102
25141
  // src/commands/sessions/daemon/worktree/planAllocation.ts
25103
- import { basename as basename16, join as join64 } from "path";
25142
+ import { basename as basename17, join as join65 } from "path";
25104
25143
  function planAllocation(clone, boundTreeRoots2) {
25105
25144
  return boundTreeRoots2.has(clone) ? "spill" : "primary";
25106
25145
  }
25107
25146
  function nextWorktreePath(clone, base, isTaken) {
25108
- const name = basename16(clone);
25147
+ const name = basename17(clone);
25109
25148
  for (let n = 2; n < 1e3; n++) {
25110
- const candidate = join64(base, `${name}-${n}`);
25149
+ const candidate = join65(base, `${name}-${n}`);
25111
25150
  if (!isTaken(candidate)) return candidate;
25112
25151
  }
25113
25152
  throw new Error(`no free worktree suffix for ${clone}`);
@@ -25143,7 +25182,7 @@ function createWorktree(clone, strategy, boundTreeRoots2, preferredPath) {
25143
25182
  const base = strategy.root ? expandTilde2(strategy.root) : dirname28(clone);
25144
25183
  const registered = new Set(listWorktreePaths(clone));
25145
25184
  const branches = new Set(listLocalBranches(clone));
25146
- const isTaken = (candidate) => registered.has(candidate) || existsSync57(candidate) || boundTreeRoots2.has(candidate) || branches.has(basename17(candidate));
25185
+ const isTaken = (candidate) => registered.has(candidate) || existsSync57(candidate) || boundTreeRoots2.has(candidate) || branches.has(basename18(candidate));
25147
25186
  const path106 = preferredPath && !isTaken(preferredPath) ? preferredPath : nextWorktreePath(clone, base, isTaken);
25148
25187
  const start3 = worktreeStartPoint(clone, strategy.trunk);
25149
25188
  gitSync(clone, [
@@ -25151,13 +25190,13 @@ function createWorktree(clone, strategy, boundTreeRoots2, preferredPath) {
25151
25190
  "add",
25152
25191
  start3.track ? "--track" : "--no-track",
25153
25192
  "-b",
25154
- basename17(path106),
25193
+ basename18(path106),
25155
25194
  path106,
25156
25195
  start3.ref
25157
25196
  ]);
25158
25197
  recordWorktree(path106, clone, getCurrentOrigin(clone));
25159
25198
  daemonLog(
25160
- start3.track ? `worktree allocated ${path106} (branch ${basename17(path106)} tracking ${start3.ref}) for clone ${clone}` : `worktree allocated ${path106} (branch ${basename17(path106)} off ${start3.ref}, no mainline tracking) for clone ${clone}`
25199
+ start3.track ? `worktree allocated ${path106} (branch ${basename18(path106)} tracking ${start3.ref}) for clone ${clone}` : `worktree allocated ${path106} (branch ${basename18(path106)} off ${start3.ref}, no mainline tracking) for clone ${clone}`
25161
25200
  );
25162
25201
  return path106;
25163
25202
  }
@@ -25459,19 +25498,19 @@ function persistedTreeRoots() {
25459
25498
 
25460
25499
  // src/commands/sessions/daemon/worktree/seedWorktree.ts
25461
25500
  import { copyFileSync, existsSync as existsSync60, mkdirSync as mkdirSync17 } from "fs";
25462
- import { dirname as dirname29, join as join66 } from "path";
25501
+ import { dirname as dirname29, join as join67 } from "path";
25463
25502
 
25464
25503
  // src/commands/sessions/daemon/worktree/runInstall.ts
25465
25504
  import { resolve as resolve16 } from "path";
25466
25505
 
25467
25506
  // src/commands/sessions/daemon/worktree/detectInstallCommand.ts
25468
25507
  import { existsSync as existsSync59 } from "fs";
25469
- import { join as join65 } from "path";
25508
+ import { join as join66 } from "path";
25470
25509
  function detectInstallCommand(dir) {
25471
- if (!existsSync59(join65(dir, "package.json"))) return null;
25472
- if (existsSync59(join65(dir, "pnpm-lock.yaml"))) return "pnpm install";
25473
- if (existsSync59(join65(dir, "yarn.lock"))) return "yarn install";
25474
- if (existsSync59(join65(dir, "bun.lockb"))) return "bun install";
25510
+ if (!existsSync59(join66(dir, "package.json"))) return null;
25511
+ if (existsSync59(join66(dir, "pnpm-lock.yaml"))) return "pnpm install";
25512
+ if (existsSync59(join66(dir, "yarn.lock"))) return "yarn install";
25513
+ if (existsSync59(join66(dir, "bun.lockb"))) return "bun install";
25475
25514
  return "npm install";
25476
25515
  }
25477
25516
 
@@ -25633,9 +25672,9 @@ function seedWorktree(worktreePath, clone, onSeeded = () => {
25633
25672
  }
25634
25673
  function copyConfigFiles(worktreePath, clone, copy) {
25635
25674
  for (const rel of copy) {
25636
- const src = join66(clone, rel);
25675
+ const src = join67(clone, rel);
25637
25676
  if (!existsSync60(src)) continue;
25638
- const dest = join66(worktreePath, rel);
25677
+ const dest = join67(worktreePath, rel);
25639
25678
  try {
25640
25679
  mkdirSync17(dirname29(dest), { recursive: true });
25641
25680
  copyFileSync(src, dest);
@@ -25945,7 +25984,7 @@ import chalk173 from "chalk";
25945
25984
 
25946
25985
  // src/commands/mermaid/exportFile.ts
25947
25986
  import { readFileSync as readFileSync40, writeFileSync as writeFileSync32 } from "fs";
25948
- import { basename as basename18, extname as extname2, resolve as resolve17 } from "path";
25987
+ import { basename as basename19, extname as extname2, resolve as resolve17 } from "path";
25949
25988
  import chalk172 from "chalk";
25950
25989
 
25951
25990
  // src/commands/mermaid/renderBlock.ts
@@ -25972,7 +26011,7 @@ async function renderBlock(krokiUrl, source) {
25972
26011
  async function exportFile(file, outDir, krokiUrl, onlyIndex) {
25973
26012
  const content = readFileSync40(file, "utf8");
25974
26013
  const blocks = extractMermaidBlocks(content);
25975
- const stem = basename18(file, extname2(file));
26014
+ const stem = basename19(file, extname2(file));
25976
26015
  if (onlyIndex !== void 0) {
25977
26016
  if (onlyIndex < 1 || onlyIndex > blocks.length) {
25978
26017
  console.error(
@@ -26667,17 +26706,17 @@ function createNetcapHandler(options2) {
26667
26706
  }
26668
26707
 
26669
26708
  // src/commands/netcap/prepareExtensionForLoad.ts
26670
- import { cp as cp4, readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
26709
+ import { cp as cp4, readFile as readFile6, writeFile as writeFile6 } from "fs/promises";
26671
26710
  import { networkInterfaces } from "os";
26672
- import { join as join68 } from "path";
26711
+ import { join as join69 } from "path";
26673
26712
  import chalk176 from "chalk";
26674
26713
 
26675
26714
  // src/commands/netcap/netcapExtensionDir.ts
26676
- import { dirname as dirname31, join as join67 } from "path";
26715
+ import { dirname as dirname31, join as join68 } from "path";
26677
26716
  import { fileURLToPath as fileURLToPath7 } from "url";
26678
26717
  var moduleDir2 = dirname31(fileURLToPath7(import.meta.url));
26679
26718
  function netcapExtensionDir() {
26680
- return join67(moduleDir2, "commands", "netcap", "netcap-extension");
26719
+ return join68(moduleDir2, "commands", "netcap", "netcap-extension");
26681
26720
  }
26682
26721
 
26683
26722
  // src/commands/netcap/prepareExtensionForLoad.ts
@@ -26692,9 +26731,9 @@ function lanIPv4() {
26692
26731
  return void 0;
26693
26732
  }
26694
26733
  async function configureBackground(dir, host, port, filter) {
26695
- const file = join68(dir, "background.js");
26696
- const source = await readFile5(file, "utf8");
26697
- await writeFile5(
26734
+ const file = join69(dir, "background.js");
26735
+ const source = await readFile6(file, "utf8");
26736
+ await writeFile6(
26698
26737
  file,
26699
26738
  source.replace(
26700
26739
  /const RECEIVER = "[^"]*";/,
@@ -26732,20 +26771,20 @@ async function prepareExtensionForLoad(port, filter = "") {
26732
26771
  }
26733
26772
 
26734
26773
  // src/commands/netcap/resolveNetcapOutPath.ts
26735
- import { isAbsolute as isAbsolute4, join as join70, resolve as resolve20 } from "path";
26774
+ import { isAbsolute as isAbsolute4, join as join71, resolve as resolve20 } from "path";
26736
26775
 
26737
26776
  // src/commands/netcap/defaultCapturePath.ts
26738
26777
  import { homedir as homedir21 } from "os";
26739
- import { join as join69 } from "path";
26778
+ import { join as join70 } from "path";
26740
26779
  function defaultCapturePath() {
26741
- return join69(homedir21(), ".assist", "netcap", "capture.jsonl");
26780
+ return join70(homedir21(), ".assist", "netcap", "capture.jsonl");
26742
26781
  }
26743
26782
 
26744
26783
  // src/commands/netcap/resolveNetcapOutPath.ts
26745
26784
  function resolveNetcapOutPath(out) {
26746
26785
  if (!out) return defaultCapturePath();
26747
26786
  const dir = isAbsolute4(out) ? out : resolve20(process.cwd(), out);
26748
- return join70(dir, "capture.jsonl");
26787
+ return join71(dir, "capture.jsonl");
26749
26788
  }
26750
26789
 
26751
26790
  // src/commands/netcap/netcap.ts
@@ -26792,7 +26831,7 @@ netcap stopped \u2014 captured ${count8} ${count8 === 1 ? "entry" : "entries"} t
26792
26831
 
26793
26832
  // src/commands/netcap/netcapExtract.ts
26794
26833
  import { writeFileSync as writeFileSync34 } from "fs";
26795
- import { join as join71 } from "path";
26834
+ import { join as join72 } from "path";
26796
26835
  import chalk178 from "chalk";
26797
26836
 
26798
26837
  // src/commands/netcap/extractPostsFromCapture.ts
@@ -27237,7 +27276,7 @@ function extractPostsFromCapture(captureFile) {
27237
27276
  function netcapExtract(file) {
27238
27277
  const captureFile = file ?? defaultCapturePath();
27239
27278
  const posts = extractPostsFromCapture(captureFile);
27240
- const outFile = join71(captureFile, "..", "posts.json");
27279
+ const outFile = join72(captureFile, "..", "posts.json");
27241
27280
  writeFileSync34(outFile, `${JSON.stringify(posts, null, 2)}
27242
27281
  `);
27243
27282
  console.log(
@@ -27600,7 +27639,7 @@ import { execSync as execSync41 } from "child_process";
27600
27639
  import { execSync as execSync40 } from "child_process";
27601
27640
  import { unlinkSync as unlinkSync14, writeFileSync as writeFileSync35 } from "fs";
27602
27641
  import { tmpdir as tmpdir7 } from "os";
27603
- import { join as join73 } from "path";
27642
+ import { join as join74 } from "path";
27604
27643
 
27605
27644
  // src/commands/prs/loadCommentsCache.ts
27606
27645
  import { existsSync as existsSync62, readFileSync as readFileSync44, unlinkSync as unlinkSync13 } from "fs";
@@ -27608,9 +27647,9 @@ import { parse as parse3 } from "yaml";
27608
27647
 
27609
27648
  // src/commands/prs/commentsCachePath.ts
27610
27649
  import { homedir as homedir22 } from "os";
27611
- import { join as join72 } from "path";
27650
+ import { join as join73 } from "path";
27612
27651
  function commentsCachePath(org, repo, prNumber) {
27613
- return join72(
27652
+ return join73(
27614
27653
  homedir22(),
27615
27654
  ".assist",
27616
27655
  "pr-comments",
@@ -27659,7 +27698,7 @@ function replyToComment(org, repo, prNumber, commentId, message3) {
27659
27698
  // src/commands/prs/resolveCommentWithReply.ts
27660
27699
  function resolveThread(threadId) {
27661
27700
  const mutation = `mutation($threadId: ID!) { resolveReviewThread(input: {threadId: $threadId}) { thread { isResolved } } }`;
27662
- const queryFile = join73(tmpdir7(), `gh-mutation-${Date.now()}.graphql`);
27701
+ const queryFile = join74(tmpdir7(), `gh-mutation-${Date.now()}.graphql`);
27663
27702
  writeFileSync35(queryFile, mutation);
27664
27703
  try {
27665
27704
  execSync40(
@@ -27744,10 +27783,10 @@ function fixed(commentId, sha) {
27744
27783
  import { execSync as execSync42 } from "child_process";
27745
27784
  import { unlinkSync as unlinkSync15, writeFileSync as writeFileSync36 } from "fs";
27746
27785
  import { tmpdir as tmpdir8 } from "os";
27747
- import { join as join74 } from "path";
27786
+ import { join as join75 } from "path";
27748
27787
  var THREAD_QUERY = `query($owner: String!, $repo: String!, $prNumber: Int!) { repository(owner: $owner, name: $repo) { pullRequest(number: $prNumber) { reviewThreads(first: 100) { nodes { id isResolved comments(first: 100) { nodes { databaseId } } } } } } }`;
27749
27788
  function fetchThreadIds(org, repo, prNumber) {
27750
- const queryFile = join74(tmpdir8(), `gh-query-${Date.now()}.graphql`);
27789
+ const queryFile = join75(tmpdir8(), `gh-query-${Date.now()}.graphql`);
27751
27790
  writeFileSync36(queryFile, THREAD_QUERY);
27752
27791
  try {
27753
27792
  const result = execSync42(
@@ -27822,10 +27861,10 @@ import { stringify as stringify3 } from "yaml";
27822
27861
 
27823
27862
  // src/commands/prs/removeStaleCommentsCaches.ts
27824
27863
  import { readdirSync as readdirSync12, unlinkSync as unlinkSync16 } from "fs";
27825
- import { join as join75 } from "path";
27864
+ import { join as join76 } from "path";
27826
27865
  var STALE_PATTERN = /^pr-\d+-comments\.yaml$/;
27827
27866
  function removeStaleCommentsCaches(cwd = process.cwd()) {
27828
- const dir = join75(cwd, ".assist");
27867
+ const dir = join76(cwd, ".assist");
27829
27868
  let entries;
27830
27869
  try {
27831
27870
  entries = readdirSync12(dir);
@@ -27833,7 +27872,7 @@ function removeStaleCommentsCaches(cwd = process.cwd()) {
27833
27872
  return;
27834
27873
  }
27835
27874
  for (const entry of entries.filter((e) => STALE_PATTERN.test(e))) {
27836
- unlinkSync16(join75(dir, entry));
27875
+ unlinkSync16(join76(dir, entry));
27837
27876
  }
27838
27877
  }
27839
27878
 
@@ -30455,7 +30494,7 @@ async function notify() {
30455
30494
  import { execSync as execSync52 } from "child_process";
30456
30495
  import { existsSync as existsSync63, mkdirSync as mkdirSync21, unlinkSync as unlinkSync17, writeFileSync as writeFileSync38 } from "fs";
30457
30496
  import { tmpdir as tmpdir9 } from "os";
30458
- import { join as join76, resolve as resolve21 } from "path";
30497
+ import { join as join77, resolve as resolve21 } from "path";
30459
30498
  import chalk203 from "chalk";
30460
30499
 
30461
30500
  // src/commands/screenshot/captureWindowPs1.ts
@@ -30592,7 +30631,7 @@ function buildOutputPath(outputDir, processName) {
30592
30631
  return resolve21(outputDir, `${processName}-${timestamp6}.png`);
30593
30632
  }
30594
30633
  function runPowerShellScript(processName, outputPath) {
30595
- const scriptPath = join76(tmpdir9(), `assist-screenshot-${Date.now()}.ps1`);
30634
+ const scriptPath = join77(tmpdir9(), `assist-screenshot-${Date.now()}.ps1`);
30596
30635
  writeFileSync38(scriptPath, captureWindowPs1, "utf8");
30597
30636
  try {
30598
30637
  execSync52(
@@ -30707,9 +30746,9 @@ function buildLimitsSegment(rateLimits) {
30707
30746
 
30708
30747
  // src/commands/readGitBranch.ts
30709
30748
  import { readFileSync as readFileSync47, statSync as statSync10 } from "fs";
30710
- import { isAbsolute as isAbsolute5, join as join77, resolve as resolve22 } from "path";
30749
+ import { isAbsolute as isAbsolute5, join as join78, resolve as resolve22 } from "path";
30711
30750
  function resolveGitDir(cwd) {
30712
- const dotGit = join77(cwd, ".git");
30751
+ const dotGit = join78(cwd, ".git");
30713
30752
  let stat4;
30714
30753
  try {
30715
30754
  stat4 = statSync10(dotGit);
@@ -30739,7 +30778,7 @@ function readGitBranch(cwd) {
30739
30778
  }
30740
30779
  let head;
30741
30780
  try {
30742
- head = readFileSync47(join77(gitDir, "HEAD"), "utf8");
30781
+ head = readFileSync47(join78(gitDir, "HEAD"), "utf8");
30743
30782
  } catch {
30744
30783
  return null;
30745
30784
  }
@@ -33362,7 +33401,7 @@ function groupFilesByDirectory(files) {
33362
33401
  }
33363
33402
 
33364
33403
  // src/commands/review/highLevel/buildHighLevelTree.ts
33365
- function basename19(path106) {
33404
+ function basename20(path106) {
33366
33405
  return path106.slice(path106.lastIndexOf("/") + 1);
33367
33406
  }
33368
33407
  function sum(nodes, key) {
@@ -33378,7 +33417,7 @@ function nodesOf(at2, prefix2) {
33378
33417
  const files = at2.files.map(
33379
33418
  (file) => ({
33380
33419
  kind: "file",
33381
- name: basename19(file.path),
33420
+ name: basename20(file.path),
33382
33421
  ...file
33383
33422
  })
33384
33423
  );
@@ -33698,12 +33737,12 @@ function fetchHighLevelPr(prNumber, repo) {
33698
33737
  import { existsSync as existsSync64, readFileSync as readFileSync48 } from "fs";
33699
33738
 
33700
33739
  // src/commands/review/highLevel/highLevelReviewPath.ts
33701
- import { join as join78 } from "path";
33740
+ import { join as join79 } from "path";
33702
33741
  function slug(value) {
33703
33742
  return value.replace(/[^\w.-]+/g, "-");
33704
33743
  }
33705
33744
  function highLevelReviewPath(repo, headRef, headSha) {
33706
- return join78(
33745
+ return join79(
33707
33746
  getStoreDir(),
33708
33747
  "high-level-reviews",
33709
33748
  slug(repo),
@@ -34779,7 +34818,7 @@ import { format } from "util";
34779
34818
 
34780
34819
  // src/commands/review/createReviewLogSink.ts
34781
34820
  import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync23 } from "fs";
34782
- import { join as join79 } from "path";
34821
+ import { join as join80 } from "path";
34783
34822
 
34784
34823
  // src/shared/stripAnsi.ts
34785
34824
  var ANSI = new RegExp(
@@ -34807,7 +34846,7 @@ function createReviewLogSink() {
34807
34846
  },
34808
34847
  attach(reviewDir) {
34809
34848
  mkdirSync23(reviewDir, { recursive: true });
34810
- logPath2 = join79(reviewDir, LOG_FILE);
34849
+ logPath2 = join80(reviewDir, LOG_FILE);
34811
34850
  const lines2 = [
34812
34851
  "",
34813
34852
  `=== ${(/* @__PURE__ */ new Date()).toISOString()} ===`,
@@ -35889,21 +35928,21 @@ ${annotateDiffWithLineNumbers(context.diff.trimEnd())}
35889
35928
 
35890
35929
  // src/commands/review/buildReviewPaths.ts
35891
35930
  import { homedir as homedir23 } from "os";
35892
- import { basename as basename20, join as join80 } from "path";
35931
+ import { basename as basename21, join as join81 } from "path";
35893
35932
  function buildReviewPaths(repoRoot2, key) {
35894
- const reviewDir = join80(
35933
+ const reviewDir = join81(
35895
35934
  homedir23(),
35896
35935
  ".assist",
35897
35936
  "reviews",
35898
- basename20(repoRoot2),
35937
+ basename21(repoRoot2),
35899
35938
  key
35900
35939
  );
35901
35940
  return {
35902
35941
  reviewDir,
35903
- requestPath: join80(reviewDir, "request.md"),
35904
- claudePath: join80(reviewDir, "claude.md"),
35905
- codexPath: join80(reviewDir, "codex.md"),
35906
- synthesisPath: join80(reviewDir, "synthesis.md")
35942
+ requestPath: join81(reviewDir, "request.md"),
35943
+ claudePath: join81(reviewDir, "claude.md"),
35944
+ codexPath: join81(reviewDir, "codex.md"),
35945
+ synthesisPath: join81(reviewDir, "synthesis.md")
35907
35946
  };
35908
35947
  }
35909
35948
 
@@ -36884,12 +36923,12 @@ async function reviewProposedSlackMessage(target, body, workingPath) {
36884
36923
  }
36885
36924
 
36886
36925
  // src/commands/slack/slackWorkingFile.ts
36887
- import { join as join81 } from "path";
36926
+ import { join as join82 } from "path";
36888
36927
  function slackWorkingFile(channel, partIndex) {
36889
36928
  const slug2 = channel.replace(/^[#@]/, "").toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "channel";
36890
- const dir = join81(getStoreDir(), "slack");
36929
+ const dir = join82(getStoreDir(), "slack");
36891
36930
  const name = partIndex === void 0 ? slug2 : `${slug2}-${partIndex}`;
36892
- return { dir, bodyPath: join81(dir, `${name}.md`) };
36931
+ return { dir, bodyPath: join82(dir, `${name}.md`) };
36893
36932
  }
36894
36933
 
36895
36934
  // src/commands/slack/previewSlackThreadParts.ts
@@ -38069,20 +38108,20 @@ async function configure() {
38069
38108
 
38070
38109
  // src/commands/transcript/list.ts
38071
38110
  import { existsSync as existsSync76, readdirSync as readdirSync20, statSync as statSync12 } from "fs";
38072
- import { join as join93 } from "path";
38111
+ import { join as join94 } from "path";
38073
38112
  function list3() {
38074
38113
  const { vttDir } = getTranscriptConfig();
38075
38114
  if (!existsSync76(vttDir)) return;
38076
38115
  for (const entry of readdirSync20(vttDir)) {
38077
38116
  if (!entry.endsWith(".vtt")) continue;
38078
- if (statSync12(join93(vttDir, entry)).isDirectory()) continue;
38117
+ if (statSync12(join94(vttDir, entry)).isDirectory()) continue;
38079
38118
  console.log(entry);
38080
38119
  }
38081
38120
  }
38082
38121
 
38083
38122
  // src/commands/transcript/move.ts
38084
38123
  import { existsSync as existsSync77, mkdirSync as mkdirSync32, renameSync as renameSync2, writeFileSync as writeFileSync50 } from "fs";
38085
- import { basename as basename24, join as join94 } from "path";
38124
+ import { basename as basename25, join as join95 } from "path";
38086
38125
 
38087
38126
  // src/commands/transcript/convertVttToMarkdown.ts
38088
38127
  function convertVttToMarkdown(inputPath) {
@@ -38092,9 +38131,9 @@ function convertVttToMarkdown(inputPath) {
38092
38131
  // src/commands/transcript/move.ts
38093
38132
  var DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
38094
38133
  function archiveRawVtt(vttDir, sourcePath, filename) {
38095
- const processedDir = join94(vttDir, "processed");
38134
+ const processedDir = join95(vttDir, "processed");
38096
38135
  mkdirSync32(processedDir, { recursive: true });
38097
- renameSync2(sourcePath, join94(processedDir, filename));
38136
+ renameSync2(sourcePath, join95(processedDir, filename));
38098
38137
  }
38099
38138
  function move(file, options2) {
38100
38139
  const { date, client } = options2;
@@ -38103,27 +38142,27 @@ function move(file, options2) {
38103
38142
  process.exit(1);
38104
38143
  }
38105
38144
  const { vttDir, transcriptsDir, summaryDir } = getTranscriptConfig();
38106
- const filename = basename24(file);
38107
- const sourcePath = join94(vttDir, filename);
38145
+ const filename = basename25(file);
38146
+ const sourcePath = join95(vttDir, filename);
38108
38147
  if (!existsSync77(sourcePath)) {
38109
38148
  console.error(`Error: VTT file not found: ${sourcePath}`);
38110
38149
  process.exit(1);
38111
38150
  }
38112
- const base = basename24(filename, ".vtt").replace(/ Transcription$/, "");
38151
+ const base = basename25(filename, ".vtt").replace(/ Transcription$/, "");
38113
38152
  const outputName = `${date} ${base}.md`;
38114
- const formattedDir = join94(transcriptsDir, client);
38153
+ const formattedDir = join95(transcriptsDir, client);
38115
38154
  mkdirSync32(formattedDir, { recursive: true });
38116
- const formattedPath = join94(formattedDir, outputName);
38155
+ const formattedPath = join95(formattedDir, outputName);
38117
38156
  writeFileSync50(formattedPath, convertVttToMarkdown(sourcePath), "utf8");
38118
38157
  archiveRawVtt(vttDir, sourcePath, filename);
38119
- const summaryPath = join94(summaryDir, client, outputName);
38158
+ const summaryPath = join95(summaryDir, client, outputName);
38120
38159
  console.log(`Formatted transcript: ${formattedPath}`);
38121
38160
  console.log(`Summary target: ${summaryPath}`);
38122
38161
  }
38123
38162
 
38124
38163
  // src/commands/transcript/merge.ts
38125
38164
  import { existsSync as existsSync78, writeFileSync as writeFileSync51 } from "fs";
38126
- import { basename as basename25 } from "path";
38165
+ import { basename as basename26 } from "path";
38127
38166
 
38128
38167
  // src/commands/transcript/failTranscript.ts
38129
38168
  function failTranscript(message3) {
@@ -38269,7 +38308,7 @@ function readSource3(file) {
38269
38308
  if (!existsSync78(file)) failTranscript(`VTT file not found: ${file}`);
38270
38309
  const cues = readCleanedCues(file);
38271
38310
  if (cues.length === 0) failTranscript(`no cues found in: ${file}`);
38272
- return { path: file, name: basename25(file), cues };
38311
+ return { path: file, name: basename26(file), cues };
38273
38312
  }
38274
38313
  function wholePassages(sources) {
38275
38314
  return sources.map((source) => ({
@@ -38930,7 +38969,7 @@ function listMigrationFiles(dir) {
38930
38969
 
38931
38970
  // src/commands/verify/migrations/readBaselineMigrations.ts
38932
38971
  import { execSync as execSync63 } from "child_process";
38933
- import { basename as basename26 } from "path";
38972
+ import { basename as basename27 } from "path";
38934
38973
  var MIGRATION_FILE3 = /^migration\d+[A-Za-z0-9]*\.ts$/;
38935
38974
  function readBaselineMigrations(repoRelativeDir, ref) {
38936
38975
  const baseline = /* @__PURE__ */ new Map();
@@ -38943,7 +38982,7 @@ function readBaselineMigrations(repoRelativeDir, ref) {
38943
38982
  } catch {
38944
38983
  return baseline;
38945
38984
  }
38946
- const paths = listing.split("\n").map((line) => line.trim()).filter(Boolean).filter((path106) => MIGRATION_FILE3.test(basename26(path106)));
38985
+ const paths = listing.split("\n").map((line) => line.trim()).filter(Boolean).filter((path106) => MIGRATION_FILE3.test(basename27(path106)));
38947
38986
  for (const path106 of paths) {
38948
38987
  try {
38949
38988
  const content = execSync63(`git show "${ref}:${path106}"`, {
@@ -38951,7 +38990,7 @@ function readBaselineMigrations(repoRelativeDir, ref) {
38951
38990
  maxBuffer: 16 * 1024 * 1024,
38952
38991
  stdio: ["pipe", "pipe", "pipe"]
38953
38992
  });
38954
- baseline.set(basename26(path106), content);
38993
+ baseline.set(basename27(path106), content);
38955
38994
  } catch {
38956
38995
  }
38957
38996
  }
@@ -39152,38 +39191,38 @@ function registerVerify(program2) {
39152
39191
 
39153
39192
  // src/commands/voice/devices.ts
39154
39193
  import { spawnSync as spawnSync9 } from "child_process";
39155
- import { join as join96 } from "path";
39194
+ import { join as join97 } from "path";
39156
39195
 
39157
39196
  // src/commands/voice/shared.ts
39158
39197
  import { homedir as homedir25 } from "os";
39159
- import { dirname as dirname40, join as join95 } from "path";
39198
+ import { dirname as dirname40, join as join96 } from "path";
39160
39199
  import { fileURLToPath as fileURLToPath10 } from "url";
39161
39200
  var __dirname6 = dirname40(fileURLToPath10(import.meta.url));
39162
- var VOICE_DIR = join95(homedir25(), ".assist", "voice");
39201
+ var VOICE_DIR = join96(homedir25(), ".assist", "voice");
39163
39202
  var voicePaths = {
39164
39203
  dir: VOICE_DIR,
39165
- pid: join95(VOICE_DIR, "voice.pid"),
39166
- log: join95(VOICE_DIR, "voice.log"),
39167
- venv: join95(VOICE_DIR, ".venv"),
39168
- lock: join95(VOICE_DIR, "voice.lock")
39204
+ pid: join96(VOICE_DIR, "voice.pid"),
39205
+ log: join96(VOICE_DIR, "voice.log"),
39206
+ venv: join96(VOICE_DIR, ".venv"),
39207
+ lock: join96(VOICE_DIR, "voice.lock")
39169
39208
  };
39170
39209
  function getPythonDir() {
39171
- return join95(__dirname6, "commands", "voice", "python");
39210
+ return join96(__dirname6, "commands", "voice", "python");
39172
39211
  }
39173
39212
  function getVenvPython() {
39174
- return process.platform === "win32" ? join95(voicePaths.venv, "Scripts", "python.exe") : join95(voicePaths.venv, "bin", "python");
39213
+ return process.platform === "win32" ? join96(voicePaths.venv, "Scripts", "python.exe") : join96(voicePaths.venv, "bin", "python");
39175
39214
  }
39176
39215
  function getLockDir() {
39177
39216
  const config = loadConfig();
39178
39217
  return config.voice?.lockDir ?? VOICE_DIR;
39179
39218
  }
39180
39219
  function getLockFile() {
39181
- return join95(getLockDir(), "voice.lock");
39220
+ return join96(getLockDir(), "voice.lock");
39182
39221
  }
39183
39222
 
39184
39223
  // src/commands/voice/devices.ts
39185
39224
  function devices() {
39186
- const script = join96(getPythonDir(), "list_devices.py");
39225
+ const script = join97(getPythonDir(), "list_devices.py");
39187
39226
  spawnSync9(getVenvPython(), [script], { stdio: "inherit" });
39188
39227
  }
39189
39228
 
@@ -39218,12 +39257,12 @@ function logs(options2) {
39218
39257
  // src/commands/voice/setup.ts
39219
39258
  import { spawnSync as spawnSync10 } from "child_process";
39220
39259
  import { mkdirSync as mkdirSync34 } from "fs";
39221
- import { join as join98 } from "path";
39260
+ import { join as join99 } from "path";
39222
39261
 
39223
39262
  // src/commands/voice/checkLockFile.ts
39224
39263
  import { execSync as execSync66 } from "child_process";
39225
39264
  import { existsSync as existsSync81, mkdirSync as mkdirSync33, readFileSync as readFileSync64, writeFileSync as writeFileSync52 } from "fs";
39226
- import { join as join97 } from "path";
39265
+ import { join as join98 } from "path";
39227
39266
  function isProcessAlive2(pid) {
39228
39267
  try {
39229
39268
  process.kill(pid, 0);
@@ -39260,7 +39299,7 @@ function bootstrapVenv() {
39260
39299
  }
39261
39300
  function writeLockFile(pid) {
39262
39301
  const lockFile = getLockFile();
39263
- mkdirSync33(join97(lockFile, ".."), { recursive: true });
39302
+ mkdirSync33(join98(lockFile, ".."), { recursive: true });
39264
39303
  writeFileSync52(
39265
39304
  lockFile,
39266
39305
  JSON.stringify({
@@ -39276,7 +39315,7 @@ function setup() {
39276
39315
  mkdirSync34(voicePaths.dir, { recursive: true });
39277
39316
  bootstrapVenv();
39278
39317
  console.log("\nDownloading models...\n");
39279
- const script = join98(getPythonDir(), "setup_models.py");
39318
+ const script = join99(getPythonDir(), "setup_models.py");
39280
39319
  const result = spawnSync10(getVenvPython(), [script], {
39281
39320
  stdio: "inherit",
39282
39321
  env: { ...process.env, VOICE_LOG_FILE: voicePaths.log }
@@ -39290,7 +39329,7 @@ function setup() {
39290
39329
  // src/commands/voice/start.ts
39291
39330
  import { spawn as spawn9 } from "child_process";
39292
39331
  import { mkdirSync as mkdirSync35, writeFileSync as writeFileSync53 } from "fs";
39293
- import { join as join99 } from "path";
39332
+ import { join as join100 } from "path";
39294
39333
 
39295
39334
  // src/commands/voice/buildDaemonEnv.ts
39296
39335
  function buildDaemonEnv(options2) {
@@ -39328,7 +39367,7 @@ function start2(options2) {
39328
39367
  bootstrapVenv();
39329
39368
  const debug = options2.debug || options2.foreground || process.platform === "win32";
39330
39369
  const env = buildDaemonEnv({ debug });
39331
- const script = join99(getPythonDir(), "voice_daemon.py");
39370
+ const script = join100(getPythonDir(), "voice_daemon.py");
39332
39371
  const python = getVenvPython();
39333
39372
  if (options2.foreground) {
39334
39373
  spawnForeground(python, script, env);
@@ -39459,11 +39498,11 @@ function changedPaths(from, cwd) {
39459
39498
  }
39460
39499
 
39461
39500
  // src/commands/watch/readBuiltVersion.ts
39462
- import { join as join100 } from "path";
39501
+ import { join as join101 } from "path";
39463
39502
  function readBuiltVersion(cwd) {
39464
39503
  try {
39465
39504
  const root2 = runGit3(["rev-parse", "--show-toplevel"], cwd);
39466
- return readPackageJson(join100(root2, "package.json")).version ?? "unknown";
39505
+ return readPackageJson(join101(root2, "package.json")).version ?? "unknown";
39467
39506
  } catch {
39468
39507
  return "unknown";
39469
39508
  }
@@ -40197,7 +40236,7 @@ async function auth() {
40197
40236
  // src/commands/roam/postRoamActivity.ts
40198
40237
  import { execFileSync as execFileSync23 } from "child_process";
40199
40238
  import { readdirSync as readdirSync22, readFileSync as readFileSync67, statSync as statSync13 } from "fs";
40200
- import { join as join101 } from "path";
40239
+ import { join as join102 } from "path";
40201
40240
  function findPortFile(roamDir) {
40202
40241
  let entries;
40203
40242
  try {
@@ -40206,7 +40245,7 @@ function findPortFile(roamDir) {
40206
40245
  return void 0;
40207
40246
  }
40208
40247
  const candidates = entries.filter((name) => /^roam-local-api(-[^.]+)?\.port$/.test(name)).map((name) => {
40209
- const path106 = join101(roamDir, name);
40248
+ const path106 = join102(roamDir, name);
40210
40249
  try {
40211
40250
  return { path: path106, mtimeMs: statSync13(path106).mtimeMs };
40212
40251
  } catch {
@@ -40223,7 +40262,7 @@ var PID_BY_APP = {
40223
40262
  function postRoamActivity(app, event) {
40224
40263
  const appData = process.env.APPDATA;
40225
40264
  if (!appData) return;
40226
- const portFile = findPortFile(join101(appData, "Roam"));
40265
+ const portFile = findPortFile(join102(appData, "Roam"));
40227
40266
  if (!portFile) return;
40228
40267
  let port;
40229
40268
  try {
@@ -40359,7 +40398,7 @@ async function run3(name, args) {
40359
40398
 
40360
40399
  // src/commands/run/add.ts
40361
40400
  import { mkdirSync as mkdirSync36, writeFileSync as writeFileSync54 } from "fs";
40362
- import { join as join102 } from "path";
40401
+ import { join as join103 } from "path";
40363
40402
 
40364
40403
  // src/commands/run/extractOption.ts
40365
40404
  function extractOption(args, flag) {
@@ -40420,7 +40459,7 @@ function saveNewRunConfig(name, command, args, cwd) {
40420
40459
  saveConfig(config);
40421
40460
  }
40422
40461
  function createCommandFile(name) {
40423
- const dir = join102(".claude", "commands");
40462
+ const dir = join103(".claude", "commands");
40424
40463
  mkdirSync36(dir, { recursive: true });
40425
40464
  const content = `---
40426
40465
  description: Run ${name}
@@ -40428,7 +40467,7 @@ description: Run ${name}
40428
40467
 
40429
40468
  Run \`assist run ${name} $ARGUMENTS 2>&1\`.
40430
40469
  `;
40431
- const filePath = join102(dir, `${name}.md`);
40470
+ const filePath = join103(dir, `${name}.md`);
40432
40471
  writeFileSync54(filePath, content);
40433
40472
  console.log(`Created command file: ${filePath}`);
40434
40473
  }
@@ -40485,7 +40524,7 @@ function link2() {
40485
40524
 
40486
40525
  // src/commands/run/remove.ts
40487
40526
  import { existsSync as existsSync85, unlinkSync as unlinkSync22 } from "fs";
40488
- import { join as join103 } from "path";
40527
+ import { join as join104 } from "path";
40489
40528
  function findRemoveIndex() {
40490
40529
  const idx = process.argv.indexOf("remove");
40491
40530
  if (idx === -1 || idx + 1 >= process.argv.length) return -1;
@@ -40500,7 +40539,7 @@ function parseRemoveName() {
40500
40539
  return process.argv[idx + 1];
40501
40540
  }
40502
40541
  function deleteCommandFile(name) {
40503
- const filePath = join103(".claude", "commands", `${name}.md`);
40542
+ const filePath = join104(".claude", "commands", `${name}.md`);
40504
40543
  if (existsSync85(filePath)) {
40505
40544
  unlinkSync22(filePath);
40506
40545
  console.log(`Deleted command file: ${filePath}`);
@@ -41344,16 +41383,16 @@ function setStatus2(session, newStatus) {
41344
41383
 
41345
41384
  // src/commands/sessions/daemon/worktree/reapWorktree.ts
41346
41385
  import { existsSync as existsSync90 } from "fs";
41347
- import { basename as basename27 } from "path";
41386
+ import { basename as basename28 } from "path";
41348
41387
 
41349
41388
  // src/commands/sessions/daemon/worktree/deleteStrandedTree.ts
41350
41389
  import { existsSync as existsSync89 } from "fs";
41351
- import { join as join106 } from "path";
41390
+ import { join as join107 } from "path";
41352
41391
 
41353
41392
  // src/commands/sessions/daemon/worktree/deleteTreeDirectly.ts
41354
41393
  import { statSync as statSync15 } from "fs";
41355
41394
  import { rm as rm4 } from "fs/promises";
41356
- import { join as join105 } from "path";
41395
+ import { join as join106 } from "path";
41357
41396
  async function deleteTreeDirectly(clone, worktreePath, why) {
41358
41397
  if (holdsAGitDirectoryRatherThanALink(worktreePath)) {
41359
41398
  const refusal = "it is a clone of its own, not a linked worktree";
@@ -41381,7 +41420,7 @@ async function deleteTreeDirectly(clone, worktreePath, why) {
41381
41420
  return { removed: true };
41382
41421
  }
41383
41422
  function holdsAGitDirectoryRatherThanALink(worktreePath) {
41384
- return statSync15(join105(worktreePath, ".git"), {
41423
+ return statSync15(join106(worktreePath, ".git"), {
41385
41424
  throwIfNoEntry: false
41386
41425
  })?.isDirectory() === true;
41387
41426
  }
@@ -41417,7 +41456,7 @@ async function deleteStrandedTree(clone, worktreePath, cause) {
41417
41456
  );
41418
41457
  }
41419
41458
  function strandedReason(worktreePath, cause) {
41420
- if (!existsSync89(join106(worktreePath, ".git")))
41459
+ if (!existsSync89(join107(worktreePath, ".git")))
41421
41460
  return "its .git link is already gone";
41422
41461
  if (/not a working tree|not a git repository/i.test(reason2(cause)))
41423
41462
  return "git no longer recognises it as a working tree";
@@ -41487,7 +41526,7 @@ async function reapWorktree(worktreePath, force = false) {
41487
41526
  const clone = owningClone(worktreePath);
41488
41527
  const removal = await removeTree(clone, worktreePath, force);
41489
41528
  if (!removal.removed) return removal;
41490
- await deleteWorktreeBranch(clone, basename27(worktreePath));
41529
+ await deleteWorktreeBranch(clone, basename28(worktreePath));
41491
41530
  forgetWorktree(worktreePath);
41492
41531
  daemonLog(`worktree ${worktreePath} reaped${force ? " (forced)" : ""}`);
41493
41532
  return removal;
@@ -44215,7 +44254,7 @@ function rearmStoppedSessions(sessions, notify2) {
44215
44254
 
44216
44255
  // src/commands/sessions/daemon/worktree/reconcileWorktreesOnRestore.ts
44217
44256
  import { existsSync as existsSync96 } from "fs";
44218
- import { basename as basename29 } from "path";
44257
+ import { basename as basename30 } from "path";
44219
44258
 
44220
44259
  // src/commands/sessions/daemon/worktree/accountedTrees.ts
44221
44260
  function accountedTrees(sessions) {
@@ -44357,7 +44396,7 @@ function capped2(lines2) {
44357
44396
  }
44358
44397
 
44359
44398
  // src/commands/sessions/daemon/worktree/resurfaceOrphanedWorktree.ts
44360
- import { basename as basename28 } from "path";
44399
+ import { basename as basename29 } from "path";
44361
44400
  function resurfaceOrphanedWorktree(sessions, spawnWith, recovered, notify2) {
44362
44401
  const { orphan, reason: reason4, held } = recovered;
44363
44402
  let id;
@@ -44380,7 +44419,7 @@ function orphanedSession(id, recovered) {
44380
44419
  const { orphan, reason: reason4, held } = recovered;
44381
44420
  return {
44382
44421
  ...sessionBase(id, "stopped"),
44383
- name: `recovered ${basename28(orphan.path)}`,
44422
+ name: `recovered ${basename29(orphan.path)}`,
44384
44423
  subtitle: `${held.summary} in ${orphan.path}`,
44385
44424
  commandType: "claude",
44386
44425
  pty: null,
@@ -44441,7 +44480,7 @@ async function recoverOrphanedWorktrees(sessions, spawnWith, notify2) {
44441
44480
  logVanishedTree(sessions, path106);
44442
44481
  vanished.set(clone, [
44443
44482
  ...vanished.get(clone) ?? [],
44444
- { path: path106, branch: basename29(path106) }
44483
+ { path: path106, branch: basename30(path106) }
44445
44484
  ]);
44446
44485
  continue;
44447
44486
  }
@@ -46058,7 +46097,7 @@ import { existsSync as existsSync98 } from "fs";
46058
46097
 
46059
46098
  // src/commands/sessions/daemon/worktree/carryTranscriptToTree.ts
46060
46099
  import { copyFileSync as copyFileSync5, existsSync as existsSync97, mkdirSync as mkdirSync39 } from "fs";
46061
- import { join as join108 } from "path";
46100
+ import { join as join109 } from "path";
46062
46101
  function carryTranscriptToTree(claudeSessionId, fromCwd, toCwd) {
46063
46102
  const dir = projectDirForCwd(toCwd);
46064
46103
  if (dir === projectDirForCwd(fromCwd)) {
@@ -46067,7 +46106,7 @@ function carryTranscriptToTree(claudeSessionId, fromCwd, toCwd) {
46067
46106
  );
46068
46107
  return;
46069
46108
  }
46070
- const dest = join108(dir, `${claudeSessionId}.jsonl`);
46109
+ const dest = join109(dir, `${claudeSessionId}.jsonl`);
46071
46110
  if (existsSync97(dest)) {
46072
46111
  daemonLog(`transcript ${claudeSessionId} already present in ${dir}`);
46073
46112
  return;
@@ -47256,6 +47295,73 @@ async function sshHop(ssh, probes) {
47256
47295
  );
47257
47296
  }
47258
47297
 
47298
+ // src/commands/sessions/nodes/tailscaleStatus.ts
47299
+ import { execFile as execFile13 } from "child_process";
47300
+ import { promisify as promisify12 } from "util";
47301
+ var execFileAsync11 = promisify12(execFile13);
47302
+ var STATUS_TIMEOUT_MS2 = 1e4;
47303
+ function tailscaleCli() {
47304
+ return detectPlatform() === "wsl" ? "tailscale.exe" : "tailscale";
47305
+ }
47306
+ async function tailscaleStatus() {
47307
+ const { stdout } = await execFileAsync11(tailscaleCli(), ["status", "--json"], {
47308
+ encoding: "utf8",
47309
+ timeout: STATUS_TIMEOUT_MS2,
47310
+ windowsHide: true
47311
+ });
47312
+ return JSON.parse(stdout);
47313
+ }
47314
+
47315
+ // src/commands/sessions/nodes/findTailscalePeer.ts
47316
+ function findTailscalePeer(status3, hostname2) {
47317
+ const nodes = [
47318
+ ...status3.Self ? [{ ...status3.Self, Online: true }] : [],
47319
+ ...Object.values(status3.Peer ?? {})
47320
+ ];
47321
+ return nodes.find(
47322
+ (node) => node.DNSName?.replace(/\.$/, "").toLowerCase() === hostname2
47323
+ );
47324
+ }
47325
+
47326
+ // src/commands/sessions/nodes/tailscaleHop.ts
47327
+ async function tailscaleHop(spec, probes) {
47328
+ const cli = tailscaleCli();
47329
+ let status3;
47330
+ try {
47331
+ status3 = await probes.tailscale();
47332
+ } catch (error) {
47333
+ return failed2(
47334
+ "tailscale",
47335
+ describePeerError(error),
47336
+ `\`${cli} status --json\` failed \u2014 install Tailscale on this node and sign in with \`${cli} up\``
47337
+ );
47338
+ }
47339
+ if (status3.BackendState !== "Running")
47340
+ return failed2(
47341
+ "tailscale",
47342
+ `Tailscale on this node is ${status3.BackendState ?? "not running"}`,
47343
+ `run \`${cli} up\` on this node`
47344
+ );
47345
+ const { hostname: hostname2 } = new URL(spec.url);
47346
+ const peer = findTailscalePeer(status3, hostname2);
47347
+ if (!peer)
47348
+ return failed2(
47349
+ "tailscale",
47350
+ `${hostname2} is not in this node's tailnet`,
47351
+ `check the host in \`${cli} status\`, then relink with \`assist sessions nodes link ${spec.name} --tailscale <host> --port <port>\``
47352
+ );
47353
+ if (!peer.Online)
47354
+ return failed2(
47355
+ "tailscale",
47356
+ `${hostname2} is offline in the tailnet`,
47357
+ `is ${spec.name}'s machine awake with Tailscale running and signed in?`
47358
+ );
47359
+ return passed(
47360
+ "tailscale",
47361
+ `${hostname2} online${peer.TailscaleIPs?.[0] ? ` (${peer.TailscaleIPs[0]})` : ""}`
47362
+ );
47363
+ }
47364
+
47259
47365
  // src/commands/sessions/nodes/tunnelHop.ts
47260
47366
  async function tunnelHop(spec, ssh, probes) {
47261
47367
  const bound = `127.0.0.1:${ssh.localPort}`;
@@ -47268,10 +47374,26 @@ async function tunnelHop(spec, ssh, probes) {
47268
47374
  );
47269
47375
  }
47270
47376
 
47377
+ // src/commands/sessions/nodes/tailscaleServeRemediation.ts
47378
+ function tailscaleServeRemediation(spec, error) {
47379
+ const status3 = error.status;
47380
+ if (status3 !== void 0 && status3 !== 502) return void 0;
47381
+ const { hostname: hostname2, port } = new URL(spec.url);
47382
+ const servePort = port || "443";
47383
+ return status3 === 502 ? `tailscale serve on ${hostname2} answered but nothing listens on 127.0.0.1:${servePort} behind it \u2014 is ${spec.name}'s web server running (project-switch webservers)?` : `nothing serves https on ${servePort} on ${hostname2} \u2014 run \`tailscale serve --bg --https=${servePort} http://127.0.0.1:${servePort}\` on ${spec.name} (tailscale.exe under WSL)`;
47384
+ }
47385
+
47386
+ // src/commands/sessions/nodes/isTailscaleLink.ts
47387
+ function isTailscaleLink(spec) {
47388
+ return new URL(spec.url).hostname.endsWith(".ts.net");
47389
+ }
47390
+
47271
47391
  // src/commands/sessions/nodes/webHop.ts
47272
47392
  function webRemediation(spec, error) {
47273
47393
  if (spec.ssh && error.status === void 0)
47274
47394
  return `nothing listening on ${spec.ssh.port} on ${spec.ssh.alias} \u2014 is project-switch running ${spec.name}'s web server?`;
47395
+ const serve = isTailscaleLink(spec) ? tailscaleServeRemediation(spec, error) : void 0;
47396
+ if (serve) return serve;
47275
47397
  const { hostname: hostname2, port } = new URL(spec.url);
47276
47398
  switch (peerErrorCode(error)) {
47277
47399
  case "ECONNREFUSED":
@@ -47311,7 +47433,7 @@ async function webHop(spec, probes) {
47311
47433
  };
47312
47434
  }
47313
47435
 
47314
- // src/commands/sessions/nodes/diagnoseLink.ts
47436
+ // src/commands/sessions/nodes/peerDaemonHop.ts
47315
47437
  function peerDaemonHop(spec, health2) {
47316
47438
  if (health2.daemon?.reachable) return passed("daemon", "reachable");
47317
47439
  return failed2(
@@ -47320,6 +47442,8 @@ function peerDaemonHop(spec, health2) {
47320
47442
  `run \`assist daemon status\` on ${spec.name}; restarting its web server starts the daemon`
47321
47443
  );
47322
47444
  }
47445
+
47446
+ // src/commands/sessions/nodes/diagnoseLink.ts
47323
47447
  async function diagnoseLink(spec, probes) {
47324
47448
  const hops = [];
47325
47449
  const done2 = () => ({ ...spec, ok: hops.every((h) => h.ok), hops });
@@ -47334,6 +47458,10 @@ async function diagnoseLink(spec, probes) {
47334
47458
  if (!hops.at(-1)?.ok) return done2();
47335
47459
  }
47336
47460
  }
47461
+ if (isTailscaleLink(spec)) {
47462
+ hops.push(await tailscaleHop(spec, probes));
47463
+ if (!hops.at(-1)?.ok) return done2();
47464
+ }
47337
47465
  const web3 = await webHop(spec, probes);
47338
47466
  hops.push(web3.hop);
47339
47467
  if (!web3.health) return done2();
@@ -47481,7 +47609,7 @@ function printDiagnosis(diagnosis) {
47481
47609
  console.log(`${chalk253.bold(diagnosis.name)} ${chalk253.dim(diagnosis.url)}`);
47482
47610
  for (const hop of diagnosis.hops) {
47483
47611
  const mark = hop.ok ? chalk253.green("\u2713") : chalk253.red("\u2717");
47484
- console.log(` ${mark} ${hop.hop.padEnd(6)} ${hop.detail ?? hop.error}`);
47612
+ console.log(` ${mark} ${hop.hop.padEnd(9)} ${hop.detail ?? hop.error}`);
47485
47613
  if (hop.remediation) console.log(` ${chalk253.yellow(hop.remediation)}`);
47486
47614
  }
47487
47615
  }
@@ -47494,6 +47622,7 @@ function doctorProbes(live) {
47494
47622
  sshAgent: probeSshAgent,
47495
47623
  ssh: probeSsh,
47496
47624
  tunnel: (localPort) => portAccepts(localPort),
47625
+ tailscale: tailscaleStatus,
47497
47626
  linkState: (name) => {
47498
47627
  if (!live) return "no-daemon";
47499
47628
  return live.links.find((l) => l.name === name) ?? "unknown-link";
@@ -47557,41 +47686,87 @@ async function writeLinks(links2) {
47557
47686
  saveGlobalConfig(updated2);
47558
47687
  }
47559
47688
 
47560
- // src/commands/sessions/nodes/linkNode.ts
47561
- function parseLinkUrl(url) {
47562
- const parsed = new URL(url);
47563
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
47564
- throw new Error(`link url must be http(s): ${url}`);
47565
- return parsed.origin;
47689
+ // src/commands/sessions/nodes/tailnetSuffix.ts
47690
+ function tailnetSuffix(status3) {
47691
+ if (status3.BackendState !== "Running")
47692
+ throw new Error(
47693
+ `Tailscale is not running on this node (${status3.BackendState ?? "unknown state"}) \u2014 run \`${tailscaleCli()} up\``
47694
+ );
47695
+ const suffix = status3.CurrentTailnet?.MagicDNSSuffix ?? status3.MagicDNSSuffix ?? "";
47696
+ if (status3.CurrentTailnet?.MagicDNSEnabled === false || !suffix)
47697
+ throw new Error(
47698
+ "MagicDNS is off in this tailnet \u2014 turn on MagicDNS and HTTPS certificates on the DNS page of the Tailscale admin console"
47699
+ );
47700
+ return suffix.replace(/\.$/, "");
47701
+ }
47702
+
47703
+ // src/commands/sessions/nodes/tailscaleLinkUrl.ts
47704
+ async function localTailnetSuffix() {
47705
+ return tailnetSuffix(await tailscaleStatus());
47566
47706
  }
47707
+ async function tailscaleLinkUrl(host, port, resolveSuffix = localTailnetSuffix) {
47708
+ const suffix = await resolveSuffix();
47709
+ const fqdn = host.replace(/\.$/, "");
47710
+ const bare = fqdn.endsWith(`.${suffix}`) ? fqdn.slice(0, -suffix.length - 1) : fqdn;
47711
+ return `https://${bare}.${suffix}:${port}`;
47712
+ }
47713
+
47714
+ // src/commands/sessions/nodes/parsePort.ts
47567
47715
  function parsePort(flag, value) {
47568
47716
  const port = Number(value);
47569
47717
  if (!Number.isInteger(port) || port < 1 || port > 65535)
47570
47718
  throw new Error(`${flag} must be a port number, got ${value}`);
47571
47719
  return port;
47572
47720
  }
47573
- function freeTunnelPort(port, others) {
47574
- const taken = new Set(others.map((link3) => toLinkSpec(link3).ssh?.localPort));
47575
- let candidate = defaultTunnelPort(port);
47576
- while (taken.has(candidate)) candidate++;
47577
- return candidate;
47721
+
47722
+ // src/commands/sessions/nodes/parseLinkUrl.ts
47723
+ function parseLinkUrl(url) {
47724
+ const parsed = new URL(url);
47725
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
47726
+ throw new Error(`link url must be http(s): ${url}`);
47727
+ return parsed.origin;
47578
47728
  }
47579
- function buildLink(name, url, options2, others) {
47580
- if (url && options2.ssh)
47581
- throw new Error("pass either <url> or --ssh <alias>, not both");
47729
+
47730
+ // src/commands/sessions/nodes/buildLink.ts
47731
+ async function buildLink(name, url, options2, others, resolveTailnetSuffix) {
47732
+ if ([url, options2.ssh, options2.tailscale].filter(Boolean).length > 1)
47733
+ throw new Error(
47734
+ "pass only one of <url>, --tailscale <host> or --ssh <alias>"
47735
+ );
47582
47736
  if (url) return { name, url: parseLinkUrl(url) };
47737
+ if (options2.tailscale) {
47738
+ const port2 = parsePort("--port", options2.port);
47739
+ return {
47740
+ name,
47741
+ url: await tailscaleLinkUrl(
47742
+ options2.tailscale,
47743
+ port2,
47744
+ resolveTailnetSuffix
47745
+ )
47746
+ };
47747
+ }
47583
47748
  if (!options2.ssh)
47584
- throw new Error("pass the peer's <url> or --ssh <alias> --port <port>");
47749
+ throw new Error(
47750
+ "pass the peer's <url>, --tailscale <host> --port <port> or --ssh <alias> --port <port>"
47751
+ );
47585
47752
  const port = parsePort("--port", options2.port);
47586
47753
  const localPort = options2.localPort ? parsePort("--local-port", options2.localPort) : freeTunnelPort(port, others);
47587
47754
  return { name, ssh: options2.ssh, port, localPort };
47588
47755
  }
47756
+ function freeTunnelPort(port, others) {
47757
+ const taken = new Set(others.map((link3) => toLinkSpec(link3).ssh?.localPort));
47758
+ let candidate = defaultTunnelPort(port);
47759
+ while (taken.has(candidate)) candidate++;
47760
+ return candidate;
47761
+ }
47762
+
47763
+ // src/commands/sessions/nodes/linkNode.ts
47589
47764
  async function linkNode(name, url, options2) {
47590
47765
  if (name.includes(":")) throw new Error("node names cannot contain ':'");
47591
47766
  if (name === resolveNodeName())
47592
47767
  throw new Error(`${name} is this node; link to a different node`);
47593
47768
  const others = readLinks().filter((link4) => link4.name !== name);
47594
- const link3 = buildLink(name, url, options2, others);
47769
+ const link3 = await buildLink(name, url, options2, others);
47595
47770
  await writeLinks([...others, link3]);
47596
47771
  console.log(
47597
47772
  chalk255.green(`Linked ${name} (${describeTarget(toLinkSpec(link3))})`)
@@ -47676,11 +47851,17 @@ async function unlinkNode(name) {
47676
47851
  function registerNodes(sessions) {
47677
47852
  const cmd = sessions.command("nodes").description("List this node and every linked node with its link state").option("--json", "Output as JSON").action(listNodes);
47678
47853
  cmd.command("link <name> [url]").description(
47679
- "Link a peer node by its web server URL, or over ssh with --ssh and --port (name must match the peer's sessions.nodeName)"
47854
+ "Link a peer node by its web server URL, by its Tailscale name with --tailscale and --port, or over ssh with --ssh and --port (name must match the peer's sessions.nodeName)"
47855
+ ).option(
47856
+ "--tailscale <host>",
47857
+ "Dial the peer at https://<host>.<tailnet>.ts.net:<port> (the peer runs tailscale serve for its web server port)"
47680
47858
  ).option(
47681
47859
  "--ssh <alias>",
47682
47860
  "Tunnel to the peer through this ~/.ssh/config alias"
47683
- ).option("--port <port>", "The peer's loopback web server port (with --ssh)").option(
47861
+ ).option(
47862
+ "--port <port>",
47863
+ "The peer's web server port (with --tailscale or --ssh)"
47864
+ ).option(
47684
47865
  "--local-port <port>",
47685
47866
  "Local end of the ssh tunnel (default: 43000 + port % 1000, bumped past other links)"
47686
47867
  ).action(linkNode);