@basou/cli 0.30.0 → 0.31.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
@@ -3049,7 +3049,6 @@ import {
3049
3049
  createManifest,
3050
3050
  ensureBasouDirectory,
3051
3051
  resolveRepositoryRoot as resolveRepositoryRoot7,
3052
- tryRemoteUrl,
3053
3052
  writeManifest
3054
3053
  } from "@basou/core";
3055
3054
  function collectValue(value, previous) {
@@ -3058,7 +3057,7 @@ function collectValue(value, previous) {
3058
3057
  function registerInitCommand(program2) {
3059
3058
  program2.command("init").description("Initialize a Basou workspace at the current Git repository root").option("--name <name>", "Workspace name (defaults to the repository directory name)").option("--project-name <name>", "Project display name").option("--project-description <description>", "Project description").option(
3060
3059
  "--repo-url <url>",
3061
- "Repository URL (defaults to git remote.origin.url; pass empty string for null)"
3060
+ "Deprecated and ignored (project.repository_url was removed); accepted for 0.x CLI stability, removed at 1.0"
3062
3061
  ).option(
3063
3062
  "--source-root <path>",
3064
3063
  "Extra import source root, relative to the repo root (repeatable; aggregates sibling repos into this workspace)",
@@ -3083,11 +3082,10 @@ async function doRunInit(options, ctx) {
3083
3082
  const cwd = ctx.cwd ?? process.cwd();
3084
3083
  const repositoryRoot = await resolveRepositoryRootForInit(cwd);
3085
3084
  const workspaceName = options.name ?? basename4(repositoryRoot);
3086
- let repositoryUrl;
3087
3085
  if (options.repoUrl !== void 0) {
3088
- repositoryUrl = options.repoUrl === "" ? null : options.repoUrl;
3089
- } else {
3090
- repositoryUrl = await tryRemoteUrl(repositoryRoot);
3086
+ console.error(
3087
+ "Warning: --repo-url is deprecated and ignored (project.repository_url was removed); the flag will be removed at 1.0."
3088
+ );
3091
3089
  }
3092
3090
  const sourceRoots = (options.sourceRoot ?? []).map((p) => {
3093
3091
  const rel = relative(repositoryRoot, resolve5(cwd, p));
@@ -3098,7 +3096,6 @@ async function doRunInit(options, ctx) {
3098
3096
  workspaceName,
3099
3097
  ...options.projectName !== void 0 ? { projectName: options.projectName } : {},
3100
3098
  ...options.projectDescription !== void 0 ? { projectDescription: options.projectDescription } : {},
3101
- ...repositoryUrl !== void 0 ? { repositoryUrl } : {},
3102
3099
  ...sourceRoots.length > 0 ? { sourceRoots } : {}
3103
3100
  });
3104
3101
  await writeManifest(paths, manifest, { force: options.force === true });
@@ -6707,19 +6704,19 @@ function parseInterval(value) {
6707
6704
  return seconds;
6708
6705
  }
6709
6706
  function abortableSleep(ms, signal) {
6710
- return new Promise((resolve13) => {
6707
+ return new Promise((resolve14) => {
6711
6708
  if (signal.aborted) {
6712
- resolve13();
6709
+ resolve14();
6713
6710
  return;
6714
6711
  }
6715
6712
  let timer;
6716
6713
  const onAbort = () => {
6717
6714
  clearTimeout(timer);
6718
- resolve13();
6715
+ resolve14();
6719
6716
  };
6720
6717
  timer = setTimeout(() => {
6721
6718
  signal.removeEventListener("abort", onAbort);
6722
- resolve13();
6719
+ resolve14();
6723
6720
  }, ms);
6724
6721
  signal.addEventListener("abort", onAbort, { once: true });
6725
6722
  });
@@ -8606,6 +8603,8 @@ async function doRunStatus(options, ctx) {
8606
8603
  if (findErrorCode14(error, "ENOENT")) {
8607
8604
  throw new Error("Workspace not initialized. Run 'basou init' first.");
8608
8605
  }
8606
+ const gateMessage = formatVersionGateMessage(error);
8607
+ if (gateMessage !== void 0) throw new Error(gateMessage, { cause: error });
8609
8608
  throw new Error("Failed to read workspace manifest", { cause: error });
8610
8609
  }
8611
8610
  const snapshot = await buildStatusSnapshot({ manifest, paths });
@@ -8637,6 +8636,18 @@ async function resolveRepositoryRootForStatus(cwd) {
8637
8636
  throw error;
8638
8637
  }
8639
8638
  }
8639
+ function formatVersionGateMessage(error) {
8640
+ const issues = error.issues;
8641
+ if (!Array.isArray(issues)) return void 0;
8642
+ for (const issue of issues) {
8643
+ const path = issue.path;
8644
+ const message = issue.message;
8645
+ if (Array.isArray(path) && (path.includes("schema_version") || path.includes("basou_version")) && typeof message === "string" && message.startsWith("unsupported .basou format version")) {
8646
+ return message;
8647
+ }
8648
+ }
8649
+ return void 0;
8650
+ }
8640
8651
 
8641
8652
  // src/commands/task.ts
8642
8653
  import { readFile as readFile7 } from "fs/promises";
@@ -9859,7 +9870,7 @@ async function assertWorkspaceInitialized14(basouRoot) {
9859
9870
  // src/commands/view.ts
9860
9871
  import { spawn } from "child_process";
9861
9872
  import { createHash } from "crypto";
9862
- import { basename as basename7, resolve as resolve12 } from "path";
9873
+ import { basename as basename8, resolve as resolve13 } from "path";
9863
9874
  import {
9864
9875
  assertBasouRootSafe as assertBasouRootSafe18,
9865
9876
  basouPaths as basouPaths21,
@@ -9997,7 +10008,7 @@ function formatSafetyReport(result) {
9997
10008
 
9998
10009
  // src/lib/view-server.ts
9999
10010
  import { createServer } from "http";
10000
- import { join as join17 } from "path";
10011
+ import { basename as basename7, join as join17, resolve as resolve12 } from "path";
10001
10012
  import {
10002
10013
  computeWorkStats as computeWorkStats2,
10003
10014
  enumerateApprovals as enumerateApprovals2,
@@ -10013,9 +10024,42 @@ import {
10013
10024
  readTaskFile as readTaskFile2,
10014
10025
  renderDecisions as renderDecisions3,
10015
10026
  renderHandoff as renderHandoff3,
10016
- summarizeOrientation
10027
+ summarizeOrientation,
10028
+ tryRemoteUrl
10017
10029
  } from "@basou/core";
10018
10030
 
10031
+ // src/lib/repo-url.ts
10032
+ function toBrowserUrl(remote) {
10033
+ const raw = remote.trim();
10034
+ if (raw.length === 0) return null;
10035
+ let host;
10036
+ let path;
10037
+ if (raw.includes("://")) {
10038
+ let parsed;
10039
+ try {
10040
+ parsed = new URL(raw);
10041
+ } catch {
10042
+ return null;
10043
+ }
10044
+ const scheme = parsed.protocol;
10045
+ if (scheme !== "ssh:" && scheme !== "git:" && scheme !== "http:" && scheme !== "https:") {
10046
+ return null;
10047
+ }
10048
+ host = scheme === "http:" || scheme === "https:" ? parsed.host : parsed.hostname;
10049
+ path = parsed.pathname;
10050
+ } else {
10051
+ const match = /^[^@/\s]+@([^:/\s@]+):(.+)$/.exec(raw);
10052
+ if (match === null || match[1] === void 0 || match[2] === void 0) return null;
10053
+ host = match[1];
10054
+ path = match[2];
10055
+ }
10056
+ const cleanPath = path.replace(/^\/+/, "").replace(/\/+$/, "").replace(/\.git$/, "");
10057
+ if (host.length === 0 || cleanPath.length === 0) return null;
10058
+ if (/\s/.test(host) || /\s/.test(cleanPath)) return null;
10059
+ if (cleanPath.split("/").some((seg) => seg === "." || seg === "..")) return null;
10060
+ return `https://${host}/${cleanPath}`;
10061
+ }
10062
+
10019
10063
  // src/lib/view-ui.ts
10020
10064
  var VIEW_HTML = `<!doctype html>
10021
10065
  <html lang="en">
@@ -10417,9 +10461,30 @@ var VIEW_HTML = `<!doctype html>
10417
10461
  card(c.approvalsPending, 'approvals pending')
10418
10462
  ]);
10419
10463
  detail.appendChild(cards);
10464
+ renderRepos(detail, d.repos || []);
10420
10465
  detail.appendChild(el('p', { class: 'muted', text: 'repo: ' + d.repoRoot }));
10421
10466
  }).catch(fail);
10422
10467
  }
10468
+ // The declared roster repos, each with a LIVE git link (derived server-side
10469
+ // from the repo's local git config at request time, never stored). A repo
10470
+ // with no remote renders as "local only". Links open in a new tab.
10471
+ function renderRepos(detail, repos) {
10472
+ if (!repos.length) return;
10473
+ detail.appendChild(el('h3', { text: 'Repos' }));
10474
+ var rows = el('div', { class: 'repos' }, []);
10475
+ repos.forEach(function (r) {
10476
+ var vis = r.visibility ? (' (' + r.visibility + ')') : '';
10477
+ var link = r.url
10478
+ ? el('a', { href: r.url, target: '_blank', rel: 'noopener noreferrer', text: r.url })
10479
+ : el('span', { class: 'muted', text: 'local only' });
10480
+ rows.appendChild(el('div', { class: 'f' }, [
10481
+ el('strong', { text: r.name }),
10482
+ el('span', { class: 'muted', text: vis + ' ' }),
10483
+ link
10484
+ ]));
10485
+ });
10486
+ detail.appendChild(rows);
10487
+ }
10423
10488
  function card(n, label) {
10424
10489
  return el('div', { class: 'card' }, [
10425
10490
  el('div', { class: 'n', text: String(n) }),
@@ -10658,7 +10723,7 @@ function startViewServer(opts) {
10658
10723
  };
10659
10724
  let boundPort = port;
10660
10725
  const getPort = () => boundPort;
10661
- return new Promise((resolve13, reject) => {
10726
+ return new Promise((resolve14, reject) => {
10662
10727
  const server = createServer((req, res) => {
10663
10728
  handleRequest(req, res, deps, getPort, runExclusive).catch((error) => {
10664
10729
  sendError(res, error instanceof HttpError ? error.status : 500, pathlessMessage(error));
@@ -10669,7 +10734,7 @@ function startViewServer(opts) {
10669
10734
  const address = server.address();
10670
10735
  boundPort = isAddressInfo(address) ? address.port : port;
10671
10736
  server.off("error", reject);
10672
- resolve13({
10737
+ resolve14({
10673
10738
  url: `http://${host}:${boundPort}`,
10674
10739
  port: boundPort,
10675
10740
  close: () => closeServer(server)
@@ -10681,8 +10746,8 @@ function isAddressInfo(value) {
10681
10746
  return value !== null && typeof value === "object";
10682
10747
  }
10683
10748
  function closeServer(server) {
10684
- return new Promise((resolve13) => {
10685
- server.close(() => resolve13());
10749
+ return new Promise((resolve14) => {
10750
+ server.close(() => resolve14());
10686
10751
  server.closeAllConnections();
10687
10752
  });
10688
10753
  }
@@ -10725,20 +10790,29 @@ async function handleGet(res, pathname, deps) {
10725
10790
  sendError(res, 404, "Unknown workspace");
10726
10791
  return;
10727
10792
  }
10728
- if (!await handleWorkspaceGet(res, scoped.sub, ws, deps.nowProvider)) {
10793
+ if (!await handleWorkspaceGet(res, scoped.sub, ws, deps.nowProvider, remoteUrlOf(deps))) {
10729
10794
  sendError(res, 404, "Not found");
10730
10795
  }
10731
10796
  return;
10732
10797
  }
10733
10798
  if (pathname.startsWith(API_PREFIX)) {
10734
10799
  const sub = pathname.slice(API_PREFIX.length);
10735
- if (!await handleWorkspaceGet(res, sub, primaryWorkspace(deps), deps.nowProvider)) {
10800
+ if (!await handleWorkspaceGet(
10801
+ res,
10802
+ sub,
10803
+ primaryWorkspace(deps),
10804
+ deps.nowProvider,
10805
+ remoteUrlOf(deps)
10806
+ )) {
10736
10807
  sendError(res, 404, "Not found");
10737
10808
  }
10738
10809
  return;
10739
10810
  }
10740
10811
  sendError(res, 404, "Not found");
10741
10812
  }
10813
+ function remoteUrlOf(deps) {
10814
+ return deps.remoteUrlOf ?? tryRemoteUrl;
10815
+ }
10742
10816
  async function handlePost(res, pathname, body, deps, runExclusive) {
10743
10817
  const scoped = matchWsRoute(pathname);
10744
10818
  if (scoped !== null) {
@@ -10761,9 +10835,9 @@ async function handlePost(res, pathname, body, deps, runExclusive) {
10761
10835
  }
10762
10836
  sendError(res, 404, "Not found");
10763
10837
  }
10764
- async function handleWorkspaceGet(res, sub, ws, nowProvider) {
10838
+ async function handleWorkspaceGet(res, sub, ws, nowProvider, resolveRemoteUrl) {
10765
10839
  if (sub === "overview") {
10766
- sendJson(res, 200, await overview(ws, nowProvider));
10840
+ sendJson(res, 200, await overview(ws, nowProvider, resolveRemoteUrl));
10767
10841
  return true;
10768
10842
  }
10769
10843
  if (sub === "sessions") {
@@ -10896,7 +10970,7 @@ async function captureStaleness(ws, nowIso) {
10896
10970
  const probe = await probeStaleness({ ctx: ws.importCtx, paths: ws.paths, nowIso });
10897
10971
  return probe === null ? { checked: false } : { checked: true, ...probe };
10898
10972
  }
10899
- async function overview(ws, nowProvider) {
10973
+ async function overview(ws, nowProvider, resolveRemoteUrl) {
10900
10974
  let manifest;
10901
10975
  try {
10902
10976
  manifest = await readManifest13(ws.paths);
@@ -10909,6 +10983,7 @@ async function overview(ws, nowProvider) {
10909
10983
  const nowIso = nowProvider().toISOString();
10910
10984
  const handoff = await renderHandoff3({ paths: ws.paths, nowIso });
10911
10985
  const approvals = await enumerateApprovals2(ws.paths);
10986
+ const repos = await rosterRepos(ws.repoRoot, manifest, resolveRemoteUrl);
10912
10987
  return {
10913
10988
  initialized: true,
10914
10989
  repoRoot: ws.repoRoot,
@@ -10926,9 +11001,26 @@ async function overview(ws, nowProvider) {
10926
11001
  approvalsPending: approvals.pending.length,
10927
11002
  approvalsResolved: approvals.resolved.length
10928
11003
  },
11004
+ repos,
10929
11005
  generatedAt: nowIso
10930
11006
  };
10931
11007
  }
11008
+ async function rosterRepos(repoRoot, manifest, resolveRemoteUrl) {
11009
+ const roster = manifest.repos ?? [];
11010
+ return Promise.all(
11011
+ roster.map(async (repo) => {
11012
+ const abs = resolve12(repoRoot, repo.path);
11013
+ const remote = await resolveRemoteUrl(abs);
11014
+ const url = remote !== void 0 ? toBrowserUrl(remote) : null;
11015
+ return {
11016
+ name: basename7(abs),
11017
+ path: repo.path,
11018
+ ...url !== null ? { url } : {},
11019
+ ...repo.visibility !== void 0 ? { visibility: repo.visibility } : {}
11020
+ };
11021
+ })
11022
+ );
11023
+ }
10932
11024
  async function sessionsList(ws, nowProvider) {
10933
11025
  const entries = await loadSessionEntries4(ws.paths, { now: nowProvider() });
10934
11026
  const sessions = entries.map((entry) => ({
@@ -11173,15 +11265,20 @@ async function buildSingleDeps(ctx, cwd) {
11173
11265
  const paths = basouPaths21(repositoryRoot);
11174
11266
  await assertWorkspaceInitialized15(paths.root);
11175
11267
  const entry = await buildWorkspaceEntry(repositoryRoot, ctx);
11176
- return { workspaces: [entry], mode: "single", nowProvider: nowProviderOf(ctx) };
11268
+ return {
11269
+ workspaces: [entry],
11270
+ mode: "single",
11271
+ nowProvider: nowProviderOf(ctx),
11272
+ ...ctx.remoteUrlOf !== void 0 ? { remoteUrlOf: ctx.remoteUrlOf } : {}
11273
+ };
11177
11274
  }
11178
11275
  async function buildPortfolioDeps(workspaceFlags, ctx, cwd) {
11179
- const specs = workspaceFlags.length > 0 ? workspaceFlags.map((p) => ({ path: resolve12(cwd, p) })) : await loadPortfolioConfig(ctx.portfolioConfigPath);
11276
+ const specs = workspaceFlags.length > 0 ? workspaceFlags.map((p) => ({ path: resolve13(cwd, p) })) : await loadPortfolioConfig(ctx.portfolioConfigPath);
11180
11277
  const entries = [];
11181
11278
  const seenPath = /* @__PURE__ */ new Set();
11182
11279
  const seenKey = /* @__PURE__ */ new Set();
11183
11280
  for (const spec of specs) {
11184
- const repoRoot = resolve12(spec.path);
11281
+ const repoRoot = resolve13(spec.path);
11185
11282
  if (seenPath.has(repoRoot)) continue;
11186
11283
  seenPath.add(repoRoot);
11187
11284
  const entry = await buildWorkspaceEntry(repoRoot, ctx, spec.label);
@@ -11191,7 +11288,12 @@ async function buildPortfolioDeps(workspaceFlags, ctx, cwd) {
11191
11288
  entries.push({ ...entry, key });
11192
11289
  }
11193
11290
  if (entries.length === 0) throw new Error("No workspaces to show.");
11194
- return { workspaces: entries, mode: "portfolio", nowProvider: nowProviderOf(ctx) };
11291
+ return {
11292
+ workspaces: entries,
11293
+ mode: "portfolio",
11294
+ nowProvider: nowProviderOf(ctx),
11295
+ ...ctx.remoteUrlOf !== void 0 ? { remoteUrlOf: ctx.remoteUrlOf } : {}
11296
+ };
11195
11297
  }
11196
11298
  async function buildWorkspaceEntry(repoRoot, ctx, labelOverride) {
11197
11299
  const paths = basouPaths21(repoRoot);
@@ -11214,7 +11316,7 @@ async function buildWorkspaceEntry(repoRoot, ctx, labelOverride) {
11214
11316
  const notFound = error instanceof Error && error.message === "YAML file not found";
11215
11317
  return {
11216
11318
  key: `ws-${createHash("sha1").update(repoRoot).digest("hex").slice(0, 12)}`,
11217
- label: labelOverride ?? basename7(repoRoot),
11319
+ label: labelOverride ?? basename8(repoRoot),
11218
11320
  paths,
11219
11321
  repoRoot,
11220
11322
  importCtx,
@@ -11253,7 +11355,7 @@ function openInBrowser(url, override) {
11253
11355
  }
11254
11356
  }
11255
11357
  function waitForShutdown(signal) {
11256
- return new Promise((resolve13) => {
11358
+ return new Promise((resolve14) => {
11257
11359
  const cleanup = () => {
11258
11360
  process.off("SIGINT", onSignal);
11259
11361
  process.off("SIGTERM", onSignal);
@@ -11261,18 +11363,18 @@ function waitForShutdown(signal) {
11261
11363
  };
11262
11364
  const onSignal = () => {
11263
11365
  cleanup();
11264
- resolve13();
11366
+ resolve14();
11265
11367
  };
11266
11368
  const onAbort = () => {
11267
11369
  cleanup();
11268
- resolve13();
11370
+ resolve14();
11269
11371
  };
11270
11372
  process.on("SIGINT", onSignal);
11271
11373
  process.on("SIGTERM", onSignal);
11272
11374
  if (signal !== void 0) {
11273
11375
  if (signal.aborted) {
11274
11376
  cleanup();
11275
- resolve13();
11377
+ resolve14();
11276
11378
  return;
11277
11379
  }
11278
11380
  signal.addEventListener("abort", onAbort);