@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 +303 -83
- package/dist/index.js.map +3 -3
- package/package.json +1 -1
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
|
|
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: () =>
|
|
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
|
|
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: () =>
|
|
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.
|
|
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
|
|
215168
|
+
let workspaceSlug;
|
|
215169
|
+
let workspaceId;
|
|
215107
215170
|
try {
|
|
215108
|
-
|
|
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/${
|
|
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 '${
|
|
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:
|
|
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",
|
|
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",
|
|
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",
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
216052
|
-
|
|
216053
|
-
|
|
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
|
-
|
|
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
|
|
216069
|
-
if (
|
|
216070
|
-
|
|
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
|
-
|
|
216086
|
-
|
|
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
|
-
|
|
216117
|
-
|
|
216118
|
-
|
|
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(
|
|
216121
|
-
if (resolution.
|
|
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({
|
|
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
|
-
|
|
216308
|
-
|
|
216309
|
-
|
|
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"
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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:
|