@driftless-sh/cli 0.1.38 → 0.1.42

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
@@ -39,8 +39,38 @@ __export(api_client_exports, {
39
39
  api: () => api,
40
40
  formatError: () => formatError,
41
41
  getApiKey: () => getApiKey,
42
- getApiUrl: () => getApiUrl
42
+ getApiUrl: () => getApiUrl,
43
+ getCachedWorkspace: () => getCachedWorkspace,
44
+ saveWorkspaceToConfig: () => saveWorkspaceToConfig
43
45
  });
46
+ function readConfig() {
47
+ try {
48
+ if ((0, import_node_fs.existsSync)(CONFIG_PATH)) {
49
+ return JSON.parse((0, import_node_fs.readFileSync)(CONFIG_PATH, "utf8"));
50
+ }
51
+ } catch {
52
+ }
53
+ return {};
54
+ }
55
+ function getCachedWorkspace() {
56
+ const c = readConfig();
57
+ return { slug: c.workspace_slug, workspaceId: c.workspace_id };
58
+ }
59
+ function saveWorkspaceToConfig(slug, workspaceId) {
60
+ if (!slug) return;
61
+ try {
62
+ const c = readConfig();
63
+ if (c.workspace_slug === slug && (workspaceId === void 0 || c.workspace_id === workspaceId)) {
64
+ return;
65
+ }
66
+ c.workspace_slug = slug;
67
+ if (workspaceId !== void 0) c.workspace_id = workspaceId;
68
+ const dir = (0, import_node_path.dirname)(CONFIG_PATH);
69
+ if (!(0, import_node_fs.existsSync)(dir)) (0, import_node_fs.mkdirSync)(dir, { recursive: true });
70
+ (0, import_node_fs.writeFileSync)(CONFIG_PATH, JSON.stringify(c, null, 2) + "\n");
71
+ } catch {
72
+ }
73
+ }
44
74
  function loadApiKey() {
45
75
  const envKey = process.env["DRIFTLESS_API_KEY"];
46
76
  if (envKey) return envKey;
@@ -94,7 +124,7 @@ function parseError(e) {
94
124
  }
95
125
  return msg;
96
126
  }
97
- function request(method, path, body) {
127
+ function singleRequest(method, path, body, timeoutMs) {
98
128
  return new Promise((resolve8, reject) => {
99
129
  const baseUrl = getBaseUrl();
100
130
  const fullUrl = `${baseUrl}${path}`;
@@ -128,11 +158,42 @@ function request(method, path, body) {
128
158
  });
129
159
  }
130
160
  );
161
+ if (timeoutMs && timeoutMs > 0) {
162
+ req.setTimeout(timeoutMs, () => {
163
+ req.destroy(new Error(`Request timed out after ${timeoutMs}ms`));
164
+ });
165
+ }
131
166
  req.on("error", (e) => reject(new Error(`Connection failed: ${e.message}`)));
132
167
  if (body) req.write(JSON.stringify(body));
133
168
  req.end();
134
169
  });
135
170
  }
171
+ function isTransient(err) {
172
+ const msg = err instanceof Error ? err.message : String(err);
173
+ return !/HTTP 4\d\d:/.test(msg);
174
+ }
175
+ function defaultsFor(method) {
176
+ const isGet = method === "GET";
177
+ return {
178
+ timeoutMs: isGet ? 15e3 : 2e4,
179
+ retries: isGet ? 1 : 0
180
+ };
181
+ }
182
+ async function request(method, path, body, opts) {
183
+ const d = defaultsFor(method);
184
+ const timeoutMs = opts?.timeoutMs ?? d.timeoutMs;
185
+ const retries = opts?.retries ?? d.retries;
186
+ let lastErr;
187
+ for (let attempt = 0; attempt <= retries; attempt++) {
188
+ try {
189
+ return await singleRequest(method, path, body, timeoutMs);
190
+ } catch (e) {
191
+ lastErr = e;
192
+ if (attempt === retries || !isTransient(e)) break;
193
+ }
194
+ }
195
+ throw lastErr;
196
+ }
136
197
  function getApiUrl() {
137
198
  return getBaseUrl();
138
199
  }
@@ -154,11 +215,11 @@ var init_api_client = __esm({
154
215
  CONFIG_PATH = (0, import_node_path.resolve)((0, import_node_os.homedir)(), ".driftless", "config.json");
155
216
  DEFAULT_URL = "http://localhost:3000/api/v1";
156
217
  api = {
157
- get: (path) => request("GET", path),
158
- post: (path, body) => request("POST", path, body),
159
- put: (path, body) => request("PUT", path, body),
160
- patch: (path, body) => request("PATCH", path, body),
161
- delete: (path) => request("DELETE", path)
218
+ get: (path, opts) => request("GET", path, void 0, opts),
219
+ post: (path, body, opts) => request("POST", path, body, opts),
220
+ put: (path, body, opts) => request("PUT", path, body, opts),
221
+ patch: (path, body, opts) => request("PATCH", path, body, opts),
222
+ delete: (path, opts) => request("DELETE", path, void 0, opts)
162
223
  };
163
224
  }
164
225
  });
@@ -3184,7 +3245,7 @@ var require_typescript = __commonJS({
3184
3245
  forEachYieldExpression: () => forEachYieldExpression,
3185
3246
  formatColorAndReset: () => formatColorAndReset,
3186
3247
  formatDiagnostic: () => formatDiagnostic,
3187
- formatDiagnostics: () => formatDiagnostics,
3248
+ formatDiagnostics: () => formatDiagnostics2,
3188
3249
  formatDiagnosticsWithColorAndContext: () => formatDiagnosticsWithColorAndContext,
3189
3250
  formatGeneratedName: () => formatGeneratedName,
3190
3251
  formatGeneratedNamePart: () => formatGeneratedNamePart,
@@ -138049,7 +138110,7 @@ ${lanes.join("\n")}
138049
138110
  }
138050
138111
  return sortAndDeduplicateDiagnostics(diagnostics || emptyArray);
138051
138112
  }
138052
- function formatDiagnostics(diagnostics, host) {
138113
+ function formatDiagnostics2(diagnostics, host) {
138053
138114
  let output = "";
138054
138115
  for (const diagnostic of diagnostics) {
138055
138116
  output += formatDiagnostic(diagnostic, host);
@@ -198687,7 +198748,7 @@ ${options.prefix}` : "\n" : options.prefix
198687
198748
  forEachYieldExpression: () => forEachYieldExpression,
198688
198749
  formatColorAndReset: () => formatColorAndReset,
198689
198750
  formatDiagnostic: () => formatDiagnostic,
198690
- formatDiagnostics: () => formatDiagnostics,
198751
+ formatDiagnostics: () => formatDiagnostics2,
198691
198752
  formatDiagnosticsWithColorAndContext: () => formatDiagnosticsWithColorAndContext,
198692
198753
  formatGeneratedName: () => formatGeneratedName,
198693
198754
  formatGeneratedNamePart: () => formatGeneratedNamePart,
@@ -214581,7 +214642,7 @@ async function installSkillCommand() {
214581
214642
  // src/commands/init.ts
214582
214643
  function getVersion() {
214583
214644
  try {
214584
- return "0.1.38";
214645
+ return "0.1.42";
214585
214646
  } catch {
214586
214647
  return "0.0.0";
214587
214648
  }
@@ -214922,6 +214983,7 @@ async function initCommand(args) {
214922
214983
  process.exit(1);
214923
214984
  }
214924
214985
  const workspaceSlug = workspace.slug;
214986
+ if (workspaceSlug) saveWorkspaceToConfig(workspaceSlug, workspace.workspace_id);
214925
214987
  console.log(`Repository: ${remote.org}/${remote.repo}`);
214926
214988
  console.log(`Workspace: ${workspaceSlug} \u2713`);
214927
214989
  if (srcOverride) console.log(`Scan root: ${srcOverride}`);
@@ -215103,16 +215165,27 @@ async function scanCommand(args) {
215103
215165
  process.exit(1);
215104
215166
  }
215105
215167
  const isJSON = args.includes("--json");
215106
- let workspace;
215168
+ let workspaceSlug;
215169
+ let workspaceId;
215107
215170
  try {
215108
- workspace = await api.get("/me");
215171
+ const me = await api.get("/me", { timeoutMs: 8e3, retries: 1 });
215172
+ workspaceSlug = me?.slug;
215173
+ workspaceId = me?.workspace_id;
215174
+ if (workspaceSlug) saveWorkspaceToConfig(workspaceSlug, workspaceId);
215109
215175
  } catch {
215176
+ }
215177
+ if (!workspaceSlug) {
215178
+ const cached = getCachedWorkspace();
215179
+ workspaceSlug = cached.slug;
215180
+ workspaceId = workspaceId ?? cached.workspaceId;
215181
+ }
215182
+ if (!workspaceSlug) {
215110
215183
  console.error("Could not resolve workspace. Run driftless login first.");
215111
215184
  process.exit(1);
215112
215185
  }
215113
- const repos = await api.get(`/workspaces/${workspace.slug}/repos`);
215186
+ const repos = await api.get(`/workspaces/${workspaceSlug}/repos`);
215114
215187
  if (!Array.isArray(repos)) {
215115
- console.error(`Error: Could not fetch repos for workspace '${workspace.slug}'.`);
215188
+ console.error(`Error: Could not fetch repos for workspace '${workspaceSlug}'.`);
215116
215189
  console.error(`Response:`, JSON.stringify(repos).slice(0, 200));
215117
215190
  console.error("\nTip: Make sure your API key belongs to this workspace.");
215118
215191
  console.error("Run: driftless login --key <key>");
@@ -215156,7 +215229,7 @@ async function scanCommand(args) {
215156
215229
  }
215157
215230
  }
215158
215231
  const result = await api.post("/scan", {
215159
- workspace_id: workspace.workspace_id,
215232
+ workspace_id: workspaceId,
215160
215233
  repo_id: repo.id,
215161
215234
  diff,
215162
215235
  commit_hash: commitHash,
@@ -215164,7 +215237,7 @@ async function scanCommand(args) {
215164
215237
  });
215165
215238
  const rulesEvaluated = result.rules_evaluated || 0;
215166
215239
  if (result.status === "clean") {
215167
- analyticsEvent("cli_scan_run", workspace.slug, { violations_found: false, count: 0 });
215240
+ analyticsEvent("cli_scan_run", workspaceSlug, { violations_found: false, count: 0 });
215168
215241
  if (isJSON) {
215169
215242
  emitJSON({ status: "clean", files: changedFiles, rules_evaluated: rulesEvaluated, violations: [] });
215170
215243
  } else {
@@ -215173,7 +215246,7 @@ async function scanCommand(args) {
215173
215246
  process.exit(0);
215174
215247
  }
215175
215248
  if (!result.violations || result.violations.length === 0) {
215176
- analyticsEvent("cli_scan_run", workspace.slug, { violations_found: false, count: 0 });
215249
+ analyticsEvent("cli_scan_run", workspaceSlug, { violations_found: false, count: 0 });
215177
215250
  if (isJSON) {
215178
215251
  emitJSON({ status: "clean", files: changedFiles, rules_evaluated: rulesEvaluated, violations: [] });
215179
215252
  } else {
@@ -215181,7 +215254,7 @@ async function scanCommand(args) {
215181
215254
  }
215182
215255
  process.exit(0);
215183
215256
  }
215184
- analyticsEvent("cli_scan_run", workspace.slug, { violations_found: true, count: result.violations.length });
215257
+ analyticsEvent("cli_scan_run", workspaceSlug, { violations_found: true, count: result.violations.length });
215185
215258
  if (isJSON) {
215186
215259
  emitJSON({ status: "violated", files: changedFiles, rules_evaluated: rulesEvaluated, violations: result.violations });
215187
215260
  } else {
@@ -215251,11 +215324,13 @@ function renderSummaryHuman(items) {
215251
215324
  const topicWidth = Math.max(...items.map((i) => i.topic.length), 12) + 2;
215252
215325
  const badgeWidth = Math.max(...items.map((i) => formatBadges(i.badges).length), 8) + 2;
215253
215326
  const statusWidth = Math.max(...items.map((i) => (i.classification?.status || "").length), 8) + 2;
215327
+ const kindWidth = Math.max(...items.map((i) => (i.classification?.kind || "").length), 8) + 2;
215254
215328
  for (const item of items) {
215255
215329
  const topic = pad(item.topic, topicWidth);
215256
215330
  const badges = pad(formatBadges(item.badges), badgeWidth);
215257
215331
  const status = pad(item.classification?.status || "", statusWidth);
215258
- console.log(`${topic}${status}${badges}${item.summary}`);
215332
+ const kind = pad(item.classification?.kind || "", kindWidth);
215333
+ console.log(`${topic}${status}${kind}${badges}${item.summary}`);
215259
215334
  }
215260
215335
  console.log(`
215261
215336
  ${items.length} topic${items.length === 1 ? "" : "s"}.`);
@@ -215379,10 +215454,15 @@ function countLocalFilesMatching(pattern) {
215379
215454
  }
215380
215455
  async function resolveWorkspaceSlug() {
215381
215456
  try {
215382
- const me = await api.get("/me");
215383
- if (me?.slug) return me.slug;
215457
+ const me = await api.get("/me", { timeoutMs: 8e3, retries: 1 });
215458
+ if (me?.slug) {
215459
+ saveWorkspaceToConfig(me.slug, me.workspace_id);
215460
+ return me.slug;
215461
+ }
215384
215462
  } catch {
215385
215463
  }
215464
+ const cached = getCachedWorkspace().slug;
215465
+ if (cached) return cached;
215386
215466
  const remote = getGitRemote();
215387
215467
  if (!remote) {
215388
215468
  console.error("Error: no git remote found.");
@@ -215399,6 +215479,41 @@ async function contextCommand(args) {
215399
215479
  const { flags, positional } = parseArgs(args);
215400
215480
  const subCommand = positional[0];
215401
215481
  const isJSON = !!flags["json"];
215482
+ if (subCommand === "doctor") {
215483
+ let audit;
215484
+ try {
215485
+ audit = await api.get(`/workspaces/${workspaceSlug}/watchers/audit`);
215486
+ } catch (e) {
215487
+ console.error(`Failed to audit context: ${formatError(e)}`);
215488
+ process.exit(1);
215489
+ }
215490
+ if (isJSON) {
215491
+ emitJSON2(audit);
215492
+ const blocking2 = audit.orphaned.length + audit.repo_leak.length;
215493
+ process.exit(blocking2 > 0 ? 1 : 0);
215494
+ }
215495
+ const section = (label, items) => {
215496
+ if (items.length === 0) return;
215497
+ console.log(`
215498
+ ${label} (${items.length}):`);
215499
+ for (const it of items) {
215500
+ console.log(` ${it.slug}${it.reason ? ` \u2014 ${it.reason}` : ""}`);
215501
+ }
215502
+ };
215503
+ console.log(`Context audit \u2014 ${audit.total} topic${audit.total === 1 ? "" : "s"}`);
215504
+ section("\u26A0 stale (code changed, context not updated)", audit.stale);
215505
+ section("\u2717 orphaned (repo deleted \u2014 hidden from agents)", audit.orphaned);
215506
+ section("\u2022 draft (suggested, never confirmed)", audit.draft);
215507
+ section("\u2022 docs-pending (doc anchored, never synced)", audit.docs_pending);
215508
+ section("\u2717 repo-leak (references unknown repo ids)", audit.repo_leak);
215509
+ const blocking = audit.orphaned.length + audit.repo_leak.length;
215510
+ if (audit.stale.length + audit.draft.length + audit.docs_pending.length + blocking === 0) {
215511
+ console.log("\nAll context is healthy.");
215512
+ } else if (blocking > 0) {
215513
+ console.log("\nResolve orphaned / repo-leak topics \u2014 they indicate deleted-repo fallout.");
215514
+ }
215515
+ process.exit(blocking > 0 ? 1 : 0);
215516
+ }
215402
215517
  if (subCommand === "list") {
215403
215518
  const query = [];
215404
215519
  if (flags["stale"]) query.push("stale=true");
@@ -216045,45 +216160,92 @@ init_api_client();
216045
216160
  function notLinkedMessage(remote, workspaceSlug) {
216046
216161
  return `Repo '${remote.org}/${remote.repo}' is not registered in workspace '${workspaceSlug}'. Run \`driftless init\` to register it.`;
216047
216162
  }
216163
+ function formatDiagnostics(d) {
216164
+ const lines = [];
216165
+ if (d.remote) lines.push(` repo: ${d.remote.org}/${d.remote.repo}`);
216166
+ lines.push(` tried slugs: ${d.triedSlugs.length ? d.triedSlugs.join(", ") : "(none)"}`);
216167
+ lines.push(` config slug: ${d.configSlug ?? "(not cached \u2014 run `driftless login` or `driftless init`)"}`);
216168
+ if (!d.meAttempted) {
216169
+ lines.push(" /me: not attempted");
216170
+ } else if (d.meReturned) {
216171
+ lines.push(` /me: returned slug=${d.meSlug ?? "(none)"} workspace_id=${d.meWorkspaceId ?? "(none)"}`);
216172
+ } else {
216173
+ lines.push(` /me: FAILED \u2014 ${d.meError ?? "unknown error"}`);
216174
+ }
216175
+ if (d.failedEndpoint) lines.push(` last endpoint: ${d.failedEndpoint} (rejected)`);
216176
+ return lines.join("\n");
216177
+ }
216048
216178
  async function fetchRepos(slug) {
216049
216179
  try {
216050
216180
  const raw = await api.get(`/workspaces/${slug}/repos`);
216051
- return Array.isArray(raw) ? raw : null;
216052
- } catch {
216053
- return null;
216181
+ if (Array.isArray(raw)) return { repos: raw };
216182
+ return { error: "unexpected response shape" };
216183
+ } catch (e) {
216184
+ return { error: e instanceof Error ? e.message : String(e) };
216054
216185
  }
216055
216186
  }
216056
216187
  async function resolveRepo() {
216057
216188
  const remote = getGitRemote();
216058
216189
  if (!remote) return { ok: false, reason: "no_remote" };
216190
+ const configSlug = getCachedWorkspace().slug;
216059
216191
  let meSlug;
216192
+ let meWorkspaceId;
216193
+ let meReturned = false;
216194
+ let meError;
216060
216195
  try {
216061
- const me = await api.get("/me");
216196
+ const me = await api.get("/me", { timeoutMs: 8e3, retries: 1 });
216197
+ meReturned = true;
216062
216198
  meSlug = me?.slug;
216063
- } catch {
216199
+ meWorkspaceId = me?.workspace_id;
216200
+ if (meSlug) saveWorkspaceToConfig(meSlug, meWorkspaceId);
216201
+ } catch (e) {
216202
+ meError = e instanceof Error ? e.message : String(e);
216064
216203
  }
216065
- const candidates = [...new Set([meSlug, remote.org].filter(Boolean))];
216204
+ const candidates = [...new Set([configSlug, meSlug, remote.org].filter(Boolean))];
216066
216205
  let lastLinkedSlug;
216206
+ let failedEndpoint;
216067
216207
  for (const slug of candidates) {
216068
- const repos = await fetchRepos(slug);
216069
- if (!repos) continue;
216070
- const repo = repos.find((r) => r.github_org === remote.org && r.github_repo === remote.repo);
216208
+ const result = await fetchRepos(slug);
216209
+ if ("error" in result) {
216210
+ failedEndpoint = `/workspaces/${slug}/repos`;
216211
+ continue;
216212
+ }
216213
+ const repo = result.repos.find((r) => r.github_org === remote.org && r.github_repo === remote.repo);
216071
216214
  if (repo) {
216215
+ saveWorkspaceToConfig(slug, meWorkspaceId);
216216
+ const source = slug === configSlug ? "config" : slug === meSlug ? "me" : "git-org";
216072
216217
  return {
216073
216218
  ok: true,
216074
216219
  workspaceSlug: slug,
216075
216220
  repoId: repo.id,
216076
216221
  remote,
216077
- hasScanBaseline: !!repo.scan_summary
216222
+ hasScanBaseline: !!repo.scan_summary,
216223
+ source
216078
216224
  };
216079
216225
  }
216080
216226
  lastLinkedSlug = slug;
216081
216227
  }
216228
+ const diagnostics = {
216229
+ remote,
216230
+ triedSlugs: candidates,
216231
+ configSlug,
216232
+ meAttempted: true,
216233
+ meReturned,
216234
+ meSlug,
216235
+ meWorkspaceId,
216236
+ meError,
216237
+ failedEndpoint
216238
+ };
216082
216239
  if (lastLinkedSlug) {
216083
- return { ok: false, reason: "not_linked", workspaceSlug: lastLinkedSlug, remote };
216240
+ return { ok: false, reason: "not_linked", workspaceSlug: lastLinkedSlug, remote, diagnostics };
216084
216241
  }
216085
- const triedSlugs = candidates.join(", ");
216086
- return { ok: false, reason: "no_workspace", remote, detail: `tried slugs: ${triedSlugs}` };
216242
+ return {
216243
+ ok: false,
216244
+ reason: "no_workspace",
216245
+ remote,
216246
+ detail: formatDiagnostics(diagnostics),
216247
+ diagnostics
216248
+ };
216087
216249
  }
216088
216250
 
216089
216251
  // src/commands/sync.ts
@@ -216113,32 +216275,54 @@ async function syncCommand(args) {
216113
216275
  const isJSON = !!flags["json"];
216114
216276
  const resolution = await resolveRepo();
216115
216277
  if (!resolution.ok) {
216116
- const base = resolution.reason === "no_remote" ? "Error: no git remote configured." : resolution.reason === "no_workspace" ? "Error: could not resolve workspace." : notLinkedMessage(resolution.remote, resolution.workspaceSlug);
216117
- const detail = resolution.reason === "no_workspace" && resolution.detail ? ` (${resolution.detail})` : "";
216118
- const msg = base + detail;
216278
+ if (resolution.reason === "no_remote") {
216279
+ if (!isJSON) console.log("\u26A0 Error: no git remote configured.");
216280
+ else emitJSON3({ error: "no_remote", message: "no git remote configured" });
216281
+ process.exit(1);
216282
+ }
216283
+ if (resolution.reason === "not_linked") {
216284
+ const msg = notLinkedMessage(resolution.remote, resolution.workspaceSlug);
216285
+ if (!isJSON) {
216286
+ console.log(`\u26A0 ${msg}`);
216287
+ console.log(" Run `driftless doctor` to diagnose.");
216288
+ } else {
216289
+ emitJSON3({ error: "not_linked", message: msg, diagnostics: resolution.diagnostics });
216290
+ }
216291
+ process.exit(1);
216292
+ }
216119
216293
  if (!isJSON) {
216120
- console.log(`\u26A0 ${msg}`);
216121
- if (resolution.reason !== "no_remote") console.log(" Run `driftless doctor` to diagnose.");
216294
+ console.log("\u26A0 Error: could not resolve workspace.");
216295
+ if (resolution.detail) console.log(resolution.detail);
216296
+ console.log(" Run `driftless doctor` to diagnose.");
216122
216297
  } else {
216123
- emitJSON3({ error: resolution.reason, message: msg });
216298
+ emitJSON3({
216299
+ error: "no_workspace",
216300
+ message: "could not resolve workspace",
216301
+ diagnostics: resolution.diagnostics
216302
+ });
216124
216303
  }
216125
216304
  process.exit(1);
216126
216305
  }
216127
- const { workspaceSlug, repoId, remote } = resolution;
216128
- const [eventsRes, staleTopics, violations, suggestedTopics] = await Promise.allSettled([
216306
+ const { workspaceSlug, repoId, remote, source } = resolution;
216307
+ const [eventsRes, staleTopics, violations, suggestedTopics, prActivity] = await Promise.allSettled([
216129
216308
  api.get(`/workspaces/${workspaceSlug}/watchers/events?repo_id=${repoId}&limit=20`),
216130
216309
  api.get(`/workspaces/${workspaceSlug}/watchers?stale=true&repo=${repoId}`),
216131
216310
  api.get(`/workspaces/${workspaceSlug}/violations?repo_id=${repoId}&status=open&limit=20`),
216132
- api.get(`/workspaces/${workspaceSlug}/watchers?suggested=true&repo=${repoId}`)
216311
+ api.get(`/workspaces/${workspaceSlug}/watchers?suggested=true&repo=${repoId}`),
216312
+ api.get(`/workspaces/${workspaceSlug}/pr-activity?repo_id=${repoId}&limit=5`)
216133
216313
  ]);
216134
216314
  const events = eventsRes.status === "fulfilled" ? eventsRes.value.events ?? [] : [];
216135
216315
  const stale = staleTopics.status === "fulfilled" ? staleTopics.value : [];
216136
216316
  const rawViolations = violations.status === "fulfilled" ? violations.value : [];
216137
216317
  const openViolations = Array.isArray(rawViolations) ? rawViolations : rawViolations.violations ?? rawViolations.items ?? [];
216138
216318
  const suggested = suggestedTopics.status === "fulfilled" ? suggestedTopics.value : [];
216319
+ const rawPr = prActivity.status === "fulfilled" ? prActivity.value : [];
216320
+ const prs = Array.isArray(rawPr) ? rawPr : rawPr.items ?? [];
216139
216321
  if (isJSON) {
216140
216322
  emitJSON3({
216141
216323
  repo: `${remote.org}/${remote.repo}`,
216324
+ workspace: workspaceSlug,
216325
+ resolved_via: source,
216142
216326
  stale_topics: stale.map((t) => ({ topic: t.topic, reason: t.stale?.reason ?? null })),
216143
216327
  recent_events: events.slice(0, 10).map((e) => ({
216144
216328
  type: e.event_type,
@@ -216152,11 +216336,20 @@ async function syncCommand(args) {
216152
216336
  status: v.status,
216153
216337
  author: v.author
216154
216338
  })),
216155
- suggested_pending: suggested.length
216339
+ suggested_pending: suggested.length,
216340
+ pr_activity: prs.slice(0, 5).map((p) => ({
216341
+ pr_number: p.pr_number,
216342
+ title: p.pr_title,
216343
+ author: p.pr_author,
216344
+ risk: p.risk ?? null,
216345
+ topics: p.watcher_slugs ?? [],
216346
+ observed_at: p.observed_at
216347
+ }))
216156
216348
  });
216157
216349
  process.exit(0);
216158
216350
  }
216159
- console.log(`\u258C ${remote.org}/${remote.repo}
216351
+ console.log(`\u258C ${remote.org}/${remote.repo}`);
216352
+ console.log(` workspace: ${workspaceSlug} (resolved via ${source})
216160
216353
  `);
216161
216354
  if (stale.length > 0) {
216162
216355
  console.log(`\u26A0 ${stale.length} stale topic${stale.length === 1 ? "" : "s"} \u2014 code changed, context not updated:`);
@@ -216184,12 +216377,21 @@ async function syncCommand(args) {
216184
216377
  if (openViolations.length > 5) console.log(` ... and ${openViolations.length - 5} more`);
216185
216378
  console.log("");
216186
216379
  }
216380
+ if (prs.length > 0) {
216381
+ console.log(`PR activity (${prs.length}, via GitHub App \u2014 remote, not local):`);
216382
+ for (const p of prs.slice(0, 5)) {
216383
+ const risk = p.risk ? `[${p.risk}] ` : "";
216384
+ const topics = (p.watcher_slugs ?? []).length ? ` \u2192 ${(p.watcher_slugs ?? []).join(", ")}` : "";
216385
+ console.log(` ${risk}#${p.pr_number} ${String(p.pr_title ?? "").slice(0, 70)}${topics}`);
216386
+ }
216387
+ console.log("");
216388
+ }
216187
216389
  if (suggested.length > 0) {
216188
216390
  console.log(`${suggested.length} suggested topic${suggested.length === 1 ? "" : "s"} from init \u2014 review and confirm:`);
216189
216391
  console.log(` driftless context list --suggested`);
216190
216392
  console.log("");
216191
216393
  }
216192
- if (stale.length === 0 && events.length === 0 && openViolations.length === 0 && suggested.length === 0) {
216394
+ if (stale.length === 0 && events.length === 0 && openViolations.length === 0 && suggested.length === 0 && prs.length === 0) {
216193
216395
  console.log("Cloud context is up to date. Nothing to sync.");
216194
216396
  } else {
216195
216397
  console.log("Review stale topics, then update context before touching code.");
@@ -216203,6 +216405,7 @@ var import_node_path6 = require("node:path");
216203
216405
  var import_node_readline = require("node:readline");
216204
216406
  var import_node_child_process2 = require("node:child_process");
216205
216407
  var import_node_os2 = require("node:os");
216408
+ init_api_client();
216206
216409
  var CONFIG_DIR = (0, import_node_path6.resolve)((0, import_node_os2.homedir)(), ".driftless");
216207
216410
  var CONFIG_PATH2 = (0, import_node_path6.resolve)(CONFIG_DIR, "config.json");
216208
216411
  function openBrowser(url) {
@@ -216214,7 +216417,7 @@ async function loginCommand(args) {
216214
216417
  const keyIndex = args.indexOf("--key");
216215
216418
  if (keyIndex !== -1 && args[keyIndex + 1]) {
216216
216419
  const apiKey2 = args[keyIndex + 1];
216217
- saveConfig(apiKey2);
216420
+ await saveConfig(apiKey2);
216218
216421
  return;
216219
216422
  }
216220
216423
  const apiUrl = process.env["DRIFTLESS_API_URL"] || "https://api.driftless.icu/api/v1";
@@ -216245,9 +216448,9 @@ async function loginCommand(args) {
216245
216448
  console.error("Get a valid key from your Driftless Dashboard \u2192 Settings \u2192 API Keys.");
216246
216449
  process.exit(1);
216247
216450
  }
216248
- saveConfig(apiKey, apiUrl);
216451
+ await saveConfig(apiKey, apiUrl);
216249
216452
  }
216250
- function saveConfig(apiKey, apiUrl) {
216453
+ async function saveConfig(apiKey, apiUrl) {
216251
216454
  const url = apiUrl || "https://api.driftless.icu/api/v1";
216252
216455
  try {
216253
216456
  if (!(0, import_node_fs6.existsSync)(CONFIG_DIR)) {
@@ -216257,16 +216460,24 @@ function saveConfig(apiKey, apiUrl) {
216257
216460
  CONFIG_PATH2,
216258
216461
  JSON.stringify({ api_key: apiKey, api_url: url }, null, 2) + "\n"
216259
216462
  );
216260
- console.log();
216261
- console.log("Logged in successfully.");
216262
- console.log(` Config: ${CONFIG_PATH2}`);
216263
- console.log();
216264
- console.log("Try: driftless scan --diff");
216265
- process.exit(0);
216266
216463
  } catch (err) {
216267
216464
  console.error("Failed to save config:", err);
216268
216465
  process.exit(1);
216269
216466
  }
216467
+ console.log();
216468
+ console.log("Logged in successfully.");
216469
+ console.log(` Config: ${CONFIG_PATH2}`);
216470
+ try {
216471
+ const me = await api.get("/me", { timeoutMs: 8e3, retries: 1 });
216472
+ if (me?.slug) {
216473
+ saveWorkspaceToConfig(me.slug, me.workspace_id);
216474
+ console.log(` Workspace: ${me.slug}`);
216475
+ }
216476
+ } catch {
216477
+ }
216478
+ console.log();
216479
+ console.log("Try: driftless scan --diff");
216480
+ process.exit(0);
216270
216481
  }
216271
216482
 
216272
216483
  // src/commands/doctor.ts
@@ -216298,33 +216509,27 @@ async function doctorCommand() {
216298
216509
  async function getMe() {
216299
216510
  if (meCache) return meCache;
216300
216511
  try {
216301
- meCache = await api.get("/me");
216512
+ meCache = await api.get("/me", { timeoutMs: 8e3, retries: 1 });
216302
216513
  } catch {
216303
216514
  meCache = null;
216304
216515
  }
216305
216516
  return meCache;
216306
216517
  }
216307
- let reposCache = null;
216308
- async function getRepos(slug) {
216309
- if (reposCache) return reposCache;
216310
- try {
216311
- reposCache = await api.get(`/workspaces/${slug}/repos`);
216312
- } catch {
216313
- reposCache = null;
216314
- }
216315
- return reposCache;
216316
- }
216317
- if (apiKey && apiUrl !== "http://localhost:3000/api/v1") {
216318
- const me = await getMe();
216319
- if (me?.slug) {
216320
- checks.push({ name: "Workspace", status: "ok", detail: me.slug });
216321
- } else {
216322
- checks.push({ name: "Workspace", status: "warn", detail: "API returned no workspace. Run `driftless init`" });
216323
- }
216324
- } else {
216518
+ const resolution = await resolveRepo();
216519
+ const localOrNoKey = !apiKey || apiUrl === "http://localhost:3000/api/v1";
216520
+ if (localOrNoKey) {
216325
216521
  checks.push({ name: "Workspace", status: "warn", detail: "Skipped (local API or no key)" });
216522
+ } else if (resolution.ok) {
216523
+ checks.push({ name: "Workspace", status: "ok", detail: `${resolution.workspaceSlug} (source: ${resolution.source})` });
216524
+ } else if (resolution.reason === "not_linked" && resolution.workspaceSlug) {
216525
+ checks.push({ name: "Workspace", status: "ok", detail: resolution.workspaceSlug });
216526
+ } else if (resolution.reason === "no_workspace" && resolution.diagnostics) {
216527
+ const d = resolution.diagnostics;
216528
+ const detail = !d.meReturned && d.meError ? `/me failed (${d.meError}); cached=${d.configSlug ?? "none"}` : "Could not resolve. Run `driftless init`";
216529
+ checks.push({ name: "Workspace", status: "warn", detail });
216530
+ } else {
216531
+ checks.push({ name: "Workspace", status: "warn", detail: "Could not resolve (no git remote)" });
216326
216532
  }
216327
- const resolution = await resolveRepo();
216328
216533
  if (resolution.ok) {
216329
216534
  checks.push({ name: "Repo linked", status: "ok", detail: `${resolution.remote.org}/${resolution.remote.repo}` });
216330
216535
  checks.push({
@@ -216358,18 +216563,23 @@ async function doctorCommand() {
216358
216563
  if (me?.slug) {
216359
216564
  try {
216360
216565
  const integrations = await api.get(`/workspaces/${me.slug}/integrations`);
216361
- const ghApp = integrations.find((i) => i.type === "github_app" && i.active);
216362
- if (ghApp) {
216566
+ const ghApp = integrations.find((i) => i.type === "github_app");
216567
+ if (ghApp && ghApp.active) {
216363
216568
  checks.push({ name: "GitHub App", status: "ok", detail: "Installed and active" });
216569
+ } else if (ghApp && !ghApp.active) {
216570
+ checks.push({ name: "GitHub App", status: "warn", detail: "Installed, activating \u2014 wait ~60s then re-run doctor" });
216364
216571
  } else {
216365
- checks.push({ name: "GitHub App", status: "warn", detail: "Not installed \u2014 driftless.icu/ecosystem \u2192 Settings \u2192 Integrations (if just installed, wait ~60s)" });
216572
+ checks.push({ name: "GitHub App", status: "warn", detail: "Not installed \u2014 driftless.icu/ecosystem \u2192 Settings \u2192 Integrations" });
216366
216573
  }
216367
216574
  } catch (err) {
216368
- const isNotFound = err?.status === 404 || String(err?.message ?? "").includes("404");
216575
+ const msg = String(err?.message ?? "");
216576
+ const isNotFound = err?.status === 404 || /HTTP 404|404/.test(msg);
216369
216577
  if (isNotFound) {
216370
216578
  checks.push({ name: "GitHub App", status: "warn", detail: "Not installed \u2014 driftless.icu/ecosystem \u2192 Settings \u2192 Integrations" });
216579
+ } else if (/timed out|Connection failed|ENOTFOUND|ECONNREFUSED/i.test(msg)) {
216580
+ checks.push({ name: "GitHub App", status: "warn", detail: "Could not verify \u2014 API unreachable (network/timeout)" });
216371
216581
  } else {
216372
- checks.push({ name: "GitHub App", status: "warn", detail: "Could not verify \u2014 check your connection or API key" });
216582
+ checks.push({ name: "GitHub App", status: "warn", detail: "Could not verify \u2014 API key may lack permission for this workspace" });
216373
216583
  }
216374
216584
  }
216375
216585
  } else {
@@ -216393,6 +216603,10 @@ async function doctorCommand() {
216393
216603
  }
216394
216604
  console.log(`
216395
216605
  ${okCount} ok, ${warnCount} warnings, ${failCount} failures`);
216606
+ if (!resolution.ok && resolution.reason === "no_workspace" && resolution.diagnostics) {
216607
+ console.log("\nWorkspace resolution diagnostics:");
216608
+ console.log(formatDiagnostics(resolution.diagnostics));
216609
+ }
216396
216610
  if (failCount > 0) {
216397
216611
  console.log("\nFix failures before running `driftless init` or `driftless scan`.");
216398
216612
  process.exit(1);
@@ -216405,7 +216619,7 @@ function pad2(s, n) {
216405
216619
  }
216406
216620
 
216407
216621
  // src/index.ts
216408
- var VERSION = "0.1.38";
216622
+ var VERSION = "0.1.42";
216409
216623
  var HELP_TEXT = `Driftless CLI v${VERSION} \u2014 Living repo context for humans and coding agents
216410
216624
 
216411
216625
  Install: npm install -g @driftless-sh/cli
@@ -216450,6 +216664,7 @@ Context subcommands:
216450
216664
  update <topic> --invariant ".." Append an invariant
216451
216665
  update <topic> --check "..." Append a required check
216452
216666
  delete <topic> Delete a topic
216667
+ doctor Audit context health (stale/orphaned/draft/docs-pending/repo-leak)
216453
216668
  load --files "p1,p2" Match topics by file paths
216454
216669
 
216455
216670
  Flags:
@@ -216550,6 +216765,7 @@ Subcommands:
216550
216765
  sync <topic> --note "..." Add a note to a topic
216551
216766
  update <topic> [opts] Update topic fields
216552
216767
  delete <topic> Delete a topic
216768
+ doctor Audit context health (stale/orphaned/draft/docs-pending/repo-leak)
216553
216769
  load --files "p1,p2,..." Match topics for given file paths
216554
216770
 
216555
216771
  List filters:
@@ -216687,7 +216903,11 @@ async function main() {
216687
216903
  }
216688
216904
  }
216689
216905
  main().catch((err) => {
216690
- console.error("Error:", err.message);
216906
+ const msg = err?.message ?? String(err);
216907
+ console.error("Error:", msg);
216908
+ if (/Connection failed|timed out|ENOTFOUND|ECONNREFUSED|Unauthorized|HTTP 401|HTTP 403|resolve workspace/i.test(msg)) {
216909
+ console.error("\nNext: run `driftless doctor` to diagnose auth/connectivity/workspace.");
216910
+ }
216691
216911
  process.exit(1);
216692
216912
  });
216693
216913
  /*! Bundled license information: