@0dai-dev/cli 4.2.0 → 4.3.4
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/README.md +30 -5
- package/bin/0dai.js +289 -60
- package/lib/commands/audit.js +13 -0
- package/lib/commands/auth.js +341 -98
- package/lib/commands/boneyard.js +44 -0
- package/lib/commands/ci.js +329 -0
- package/lib/commands/compliance.js +20 -0
- package/lib/commands/doctor.js +20 -1
- package/lib/commands/experience.js +5 -1
- package/lib/commands/feedback.js +92 -5
- package/lib/commands/gh.js +506 -0
- package/lib/commands/graph.js +78 -10
- package/lib/commands/heatmap.js +17 -0
- package/lib/commands/import_claude_code_agents.js +367 -0
- package/lib/commands/init.js +440 -28
- package/lib/commands/loop.js +108 -0
- package/lib/commands/mcp.js +410 -0
- package/lib/commands/models.js +27 -3
- package/lib/commands/paste.js +114 -0
- package/lib/commands/play.js +173 -0
- package/lib/commands/provider.js +69 -0
- package/lib/commands/quota.js +76 -0
- package/lib/commands/receipt.js +53 -0
- package/lib/commands/report.js +29 -2
- package/lib/commands/run.js +44 -4
- package/lib/commands/runner.js +527 -0
- package/lib/commands/session.js +1 -7
- package/lib/commands/standup.js +40 -0
- package/lib/commands/status.js +26 -1
- package/lib/commands/swarm.js +97 -4
- package/lib/commands/tui.js +81 -13
- package/lib/commands/usage.js +87 -0
- package/lib/commands/vault.js +246 -0
- package/lib/onboarding.js +9 -3
- package/lib/shared.js +29 -14
- package/lib/tui/index.mjs +571 -187
- package/lib/utils/auth.js +1 -0
- package/lib/utils/canonical-counts.js +54 -0
- package/lib/utils/diff-preview.js +192 -0
- package/lib/utils/identity.js +76 -18
- package/lib/utils/mcp-auth.js +607 -0
- package/lib/utils/plan.js +37 -2
- package/lib/vault/cipher.js +125 -0
- package/lib/vault/identity.js +122 -0
- package/lib/vault/index.js +184 -0
- package/lib/vault/storage.js +84 -0
- package/lib/wizard.js +19 -12
- package/package.json +2 -2
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const crypto = require("node:crypto");
|
|
4
|
+
const shared = require("../shared");
|
|
5
|
+
const { T, R, D, E, G, fs, path, spawnSync, findRepoScript, log } = shared;
|
|
6
|
+
|
|
7
|
+
const DEFAULT_API_VERSION = "0dai.ci/v0";
|
|
8
|
+
|
|
9
|
+
function readJson(filePath) {
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
12
|
+
} catch (err) {
|
|
13
|
+
return { error: err.message };
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readArg(args, name, fallback = "") {
|
|
18
|
+
const index = args.indexOf(name);
|
|
19
|
+
if (index >= 0 && args[index + 1]) return args[index + 1];
|
|
20
|
+
return fallback;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function hasFlag(args, name) {
|
|
24
|
+
return args.includes(name);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function pipelineDir(target) {
|
|
28
|
+
return path.join(target, "ai", "ci", "pipelines");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function controlPlanePath(target) {
|
|
32
|
+
return path.join(target, "ai", "manifest", "ci-control-plane.json");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function listPipelines(target) {
|
|
36
|
+
const dir = pipelineDir(target);
|
|
37
|
+
if (!fs.existsSync(dir)) return [];
|
|
38
|
+
return fs.readdirSync(dir)
|
|
39
|
+
.filter((name) => name.endsWith(".json"))
|
|
40
|
+
.map((name) => {
|
|
41
|
+
const filePath = path.join(dir, name);
|
|
42
|
+
const pipeline = readJson(filePath);
|
|
43
|
+
return {
|
|
44
|
+
id: pipeline.id || path.basename(name, ".json"),
|
|
45
|
+
path: filePath,
|
|
46
|
+
name: pipeline.name || pipeline.id || path.basename(name, ".json"),
|
|
47
|
+
status: pipeline.status || "unknown",
|
|
48
|
+
jobs: Array.isArray(pipeline.jobs) ? pipeline.jobs.length : 0,
|
|
49
|
+
};
|
|
50
|
+
})
|
|
51
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function resolvePipelinePath(target, value) {
|
|
55
|
+
const requested = value || "0dai-ci";
|
|
56
|
+
if (requested.endsWith(".json") || requested.includes("/") || requested.includes("\\")) {
|
|
57
|
+
return path.isAbsolute(requested) ? requested : path.resolve(process.cwd(), requested);
|
|
58
|
+
}
|
|
59
|
+
return path.join(pipelineDir(target), `${requested}.json`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizeNeeds(job) {
|
|
63
|
+
if (!job.needs) return [];
|
|
64
|
+
if (Array.isArray(job.needs)) return job.needs.map(String);
|
|
65
|
+
return [String(job.needs)];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function assertPipelineShape(pipeline) {
|
|
69
|
+
if (pipeline.error) throw new Error(`invalid pipeline JSON: ${pipeline.error}`);
|
|
70
|
+
if (pipeline.apiVersion !== DEFAULT_API_VERSION) {
|
|
71
|
+
throw new Error(`unsupported pipeline apiVersion: ${pipeline.apiVersion || "missing"}`);
|
|
72
|
+
}
|
|
73
|
+
if (!pipeline.id || typeof pipeline.id !== "string") throw new Error("pipeline.id is required");
|
|
74
|
+
if (!Array.isArray(pipeline.jobs) || pipeline.jobs.length === 0) {
|
|
75
|
+
throw new Error("pipeline.jobs must contain at least one job");
|
|
76
|
+
}
|
|
77
|
+
const seen = new Set();
|
|
78
|
+
for (const job of pipeline.jobs) {
|
|
79
|
+
if (!job.id || typeof job.id !== "string") throw new Error("each job requires a string id");
|
|
80
|
+
if (seen.has(job.id)) throw new Error(`duplicate job id: ${job.id}`);
|
|
81
|
+
seen.add(job.id);
|
|
82
|
+
if (!Array.isArray(job.run) || job.run.length === 0) {
|
|
83
|
+
throw new Error(`job ${job.id} requires a non-empty run array`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function topoSortJobs(jobs) {
|
|
89
|
+
const byId = new Map(jobs.map((job) => [job.id, job]));
|
|
90
|
+
for (const job of jobs) {
|
|
91
|
+
for (const dep of normalizeNeeds(job)) {
|
|
92
|
+
if (!byId.has(dep)) throw new Error(`job ${job.id} needs unknown job ${dep}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const order = [];
|
|
97
|
+
const state = new Map();
|
|
98
|
+
const stack = [];
|
|
99
|
+
|
|
100
|
+
function visit(job) {
|
|
101
|
+
const current = state.get(job.id);
|
|
102
|
+
if (current === "done") return;
|
|
103
|
+
if (current === "visiting") {
|
|
104
|
+
const cycleStart = stack.indexOf(job.id);
|
|
105
|
+
const cycle = [...stack.slice(Math.max(cycleStart, 0)), job.id].join(" -> ");
|
|
106
|
+
throw new Error(`pipeline contains dependency cycle: ${cycle}`);
|
|
107
|
+
}
|
|
108
|
+
state.set(job.id, "visiting");
|
|
109
|
+
stack.push(job.id);
|
|
110
|
+
for (const dep of normalizeNeeds(job)) visit(byId.get(dep));
|
|
111
|
+
stack.pop();
|
|
112
|
+
state.set(job.id, "done");
|
|
113
|
+
order.push(job);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
for (const job of jobs) visit(job);
|
|
117
|
+
return order;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function deterministicJobId({ tenant, pipelineId, sha, jobId, matrixKey }) {
|
|
121
|
+
return crypto
|
|
122
|
+
.createHash("sha256")
|
|
123
|
+
.update([tenant, pipelineId, sha, jobId, matrixKey || ""].join(":"))
|
|
124
|
+
.digest("hex")
|
|
125
|
+
.slice(0, 24);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function profileFor(controlPlane, job) {
|
|
129
|
+
const profiles = ((controlPlane || {}).worker_profiles || {});
|
|
130
|
+
const id = job.worker_profile || job.profile || "ci-light";
|
|
131
|
+
return { id, ...(profiles[id] || {}) };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function buildPlan(controlPlane, pipeline, options = {}) {
|
|
135
|
+
assertPipelineShape(pipeline);
|
|
136
|
+
const ordered = topoSortJobs(pipeline.jobs);
|
|
137
|
+
const tenant = String(options.tenant || pipeline.tenant || "0dai");
|
|
138
|
+
const repo = String(options.repo || pipeline.repo || "iGeezmo/0dai");
|
|
139
|
+
const sha = String(options.sha || "HEAD");
|
|
140
|
+
const event = String(options.event || "manual");
|
|
141
|
+
const jobs = ordered.map((job, index) => {
|
|
142
|
+
const profile = profileFor(controlPlane, job);
|
|
143
|
+
const timeout = Number(job.timeout_minutes || 10);
|
|
144
|
+
const costPerMinute = Number(profile.cost_per_minute_usd || 0);
|
|
145
|
+
return {
|
|
146
|
+
order: index + 1,
|
|
147
|
+
id: job.id,
|
|
148
|
+
deterministic_id: deterministicJobId({
|
|
149
|
+
tenant,
|
|
150
|
+
pipelineId: pipeline.id,
|
|
151
|
+
sha,
|
|
152
|
+
jobId: job.id,
|
|
153
|
+
matrixKey: job.matrix_key || "",
|
|
154
|
+
}),
|
|
155
|
+
name: job.name || job.id,
|
|
156
|
+
lane: job.lane || pipeline.default_lane || "lane-ci-general",
|
|
157
|
+
worker_profile: profile.id,
|
|
158
|
+
image: job.image || profile.default_image || "ubuntu:24.04",
|
|
159
|
+
timeout_minutes: timeout,
|
|
160
|
+
estimated_max_cost_usd: Number((timeout * costPerMinute).toFixed(4)),
|
|
161
|
+
needs: normalizeNeeds(job),
|
|
162
|
+
run: job.run,
|
|
163
|
+
secrets: job.secrets || [],
|
|
164
|
+
artifacts: job.artifacts || [],
|
|
165
|
+
};
|
|
166
|
+
});
|
|
167
|
+
const lane_summary = {};
|
|
168
|
+
for (const job of jobs) {
|
|
169
|
+
lane_summary[job.lane] = lane_summary[job.lane] || {
|
|
170
|
+
jobs: 0,
|
|
171
|
+
timeout_minutes: 0,
|
|
172
|
+
estimated_max_cost_usd: 0,
|
|
173
|
+
worker_profiles: [],
|
|
174
|
+
};
|
|
175
|
+
lane_summary[job.lane].jobs += 1;
|
|
176
|
+
lane_summary[job.lane].timeout_minutes += job.timeout_minutes;
|
|
177
|
+
lane_summary[job.lane].estimated_max_cost_usd = Number(
|
|
178
|
+
(lane_summary[job.lane].estimated_max_cost_usd + job.estimated_max_cost_usd).toFixed(4),
|
|
179
|
+
);
|
|
180
|
+
if (!lane_summary[job.lane].worker_profiles.includes(job.worker_profile)) {
|
|
181
|
+
lane_summary[job.lane].worker_profiles.push(job.worker_profile);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
schema_version: 1,
|
|
187
|
+
command: "ci plan",
|
|
188
|
+
mode: "read-only-local-plan",
|
|
189
|
+
warning: "0dai.ci/v0 is unstable and does not execute jobs",
|
|
190
|
+
issue: "https://github.com/iGeezmo/0dai/issues/2503",
|
|
191
|
+
tenant,
|
|
192
|
+
repo,
|
|
193
|
+
sha,
|
|
194
|
+
event,
|
|
195
|
+
pipeline: {
|
|
196
|
+
apiVersion: pipeline.apiVersion,
|
|
197
|
+
id: pipeline.id,
|
|
198
|
+
name: pipeline.name || pipeline.id,
|
|
199
|
+
status: pipeline.status || "pilot",
|
|
200
|
+
default_lane: pipeline.default_lane || "lane-ci-general",
|
|
201
|
+
},
|
|
202
|
+
control_plane: {
|
|
203
|
+
apiVersion: (controlPlane || {}).apiVersion || DEFAULT_API_VERSION,
|
|
204
|
+
phase: (controlPlane || {}).phase || "phase-1-dry-run",
|
|
205
|
+
queue_backend: ((controlPlane || {}).queue || {}).backend || "planned-do-managed-postgres",
|
|
206
|
+
mutates_prod: Boolean((controlPlane || {}).mutates_prod),
|
|
207
|
+
},
|
|
208
|
+
jobs,
|
|
209
|
+
lane_summary,
|
|
210
|
+
safeguards: (controlPlane || {}).safeguards || [],
|
|
211
|
+
non_scope: (controlPlane || {}).non_scope || [],
|
|
212
|
+
next_actions: (controlPlane || {}).next_actions || [],
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function printPlanHuman(plan) {
|
|
217
|
+
console.log(`\n ${T}0dai CI plan${R}`);
|
|
218
|
+
console.log(` pipeline: ${plan.pipeline.id} (${plan.pipeline.status})`);
|
|
219
|
+
console.log(` repo: ${plan.repo}`);
|
|
220
|
+
console.log(` sha: ${plan.sha}`);
|
|
221
|
+
console.log(` mode: ${plan.mode}`);
|
|
222
|
+
console.log("\n Jobs");
|
|
223
|
+
for (const job of plan.jobs) {
|
|
224
|
+
const deps = job.needs.length ? ` needs=${job.needs.join(",")}` : "";
|
|
225
|
+
console.log(` - ${String(job.order).padStart(2, " ")} ${job.id.padEnd(22)} lane=${job.lane} profile=${job.worker_profile}${deps}`);
|
|
226
|
+
}
|
|
227
|
+
console.log("\n Lane summary");
|
|
228
|
+
for (const [lane, summary] of Object.entries(plan.lane_summary)) {
|
|
229
|
+
console.log(` - ${lane}: jobs=${summary.jobs} timeout=${summary.timeout_minutes}m max_cost=$${summary.estimated_max_cost_usd}`);
|
|
230
|
+
}
|
|
231
|
+
console.log(`\n ${D}${plan.warning}${R}\n`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function runMergeQueueStatus(target, forwarded) {
|
|
235
|
+
const script = findRepoScript(target, "merge_queue_state.py");
|
|
236
|
+
if (!script) {
|
|
237
|
+
log("merge queue state inspector not found");
|
|
238
|
+
console.log(` ${D}expected: scripts/merge_queue_state.py${R}`);
|
|
239
|
+
process.exitCode = 1;
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const result = spawnSync("python3", [script, ...forwarded], {
|
|
243
|
+
encoding: "utf8",
|
|
244
|
+
timeout: 30000,
|
|
245
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
246
|
+
});
|
|
247
|
+
if (result.error) {
|
|
248
|
+
log(`merge queue status failed: ${result.error.message}`);
|
|
249
|
+
process.exitCode = 1;
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
253
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
254
|
+
if (typeof result.status === "number" && result.status !== 0) {
|
|
255
|
+
process.exitCode = result.status;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function cmdCi(target, sub, args) {
|
|
260
|
+
const command = sub && !sub.startsWith("-") ? sub : "plan";
|
|
261
|
+
const forwarded = command === sub ? args.slice(2) : args.slice(1);
|
|
262
|
+
const asJson = hasFlag(forwarded, "--json") || hasFlag(args, "--json");
|
|
263
|
+
|
|
264
|
+
if (command === "list") {
|
|
265
|
+
const pipelines = listPipelines(target);
|
|
266
|
+
if (asJson) {
|
|
267
|
+
console.log(JSON.stringify({ schema_version: 1, command: "ci list", pipelines }, null, 2));
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
console.log(`\n ${T}0dai CI pipelines${R}`);
|
|
271
|
+
for (const pipeline of pipelines) {
|
|
272
|
+
console.log(` - ${pipeline.id.padEnd(18)} status=${pipeline.status} jobs=${pipeline.jobs}`);
|
|
273
|
+
}
|
|
274
|
+
if (!pipelines.length) console.log(` ${D}no pipelines found in ai/ci/pipelines${R}`);
|
|
275
|
+
console.log("");
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (command === "mq-status") {
|
|
280
|
+
runMergeQueueStatus(target, forwarded);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (command !== "plan") {
|
|
285
|
+
console.log("Usage: 0dai ci [list|plan|mq-status] [--pipeline ID|PATH] [--repo OWNER/REPO] [--sha SHA] [--event EVENT] [--json]");
|
|
286
|
+
process.exitCode = 1;
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const manifest = readJson(controlPlanePath(target));
|
|
291
|
+
if (manifest.error) {
|
|
292
|
+
log(`ci-control-plane manifest not found or invalid: ${manifest.error}`);
|
|
293
|
+
console.log(` ${D}expected: ai/manifest/ci-control-plane.json${R}`);
|
|
294
|
+
process.exitCode = 1;
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const pipelinePath = resolvePipelinePath(target, readArg(forwarded, "--pipeline", "0dai-ci"));
|
|
298
|
+
const pipeline = readJson(pipelinePath);
|
|
299
|
+
if (pipeline.error) {
|
|
300
|
+
log(`pipeline not found or invalid: ${pipeline.error}`);
|
|
301
|
+
console.log(` ${D}expected: ai/ci/pipelines/<id>.json or a JSON path${R}`);
|
|
302
|
+
process.exitCode = 1;
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
const plan = buildPlan(manifest, pipeline, {
|
|
308
|
+
tenant: readArg(forwarded, "--tenant", ""),
|
|
309
|
+
repo: readArg(forwarded, "--repo", ""),
|
|
310
|
+
sha: readArg(forwarded, "--sha", ""),
|
|
311
|
+
event: readArg(forwarded, "--event", ""),
|
|
312
|
+
});
|
|
313
|
+
if (asJson) {
|
|
314
|
+
console.log(JSON.stringify(plan, null, 2));
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
printPlanHuman(plan);
|
|
318
|
+
} catch (err) {
|
|
319
|
+
log(`ci plan failed: ${err.message}`);
|
|
320
|
+
process.exitCode = 1;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
module.exports = {
|
|
325
|
+
buildPlan,
|
|
326
|
+
cmdCi,
|
|
327
|
+
deterministicJobId,
|
|
328
|
+
topoSortJobs,
|
|
329
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const shared = require("../shared");
|
|
4
|
+
const { D, R, findRepoScript, log, spawnSync } = shared;
|
|
5
|
+
|
|
6
|
+
function cmdCompliance(target, args) {
|
|
7
|
+
const complianceScript = findRepoScript(target, "compliance_report.py");
|
|
8
|
+
if (!complianceScript) {
|
|
9
|
+
log("compliance helper unavailable in this environment");
|
|
10
|
+
console.log(` ${D}Expected scripts/compliance_report.py in repo checkout${R}`);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const forwarded = [complianceScript, ...args, "--target", target];
|
|
15
|
+
const result = spawnSync("python3", forwarded, { stdio: "inherit" });
|
|
16
|
+
if (typeof result.status === "number") process.exit(result.status);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = { cmdCompliance };
|
package/lib/commands/doctor.js
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
const shared = require("../shared");
|
|
3
3
|
const { log, T, R, D, fs, path, spawnSync, findRepoScript, SUPPORTED_CLIS, recordExperienceEvent } = shared;
|
|
4
4
|
|
|
5
|
+
function ghostAuthStatus(home = process.env.HOME || process.env.USERPROFILE || "") {
|
|
6
|
+
const customApiEnv = home ? path.join(home, ".custom-api", "custom-api.env") : "";
|
|
7
|
+
const legacyAnthropicEnv = home ? path.join(home, ".claude-api-isolated", "anthropic.env") : "";
|
|
8
|
+
const present = Boolean(customApiEnv && fs.existsSync(customApiEnv));
|
|
9
|
+
const legacyPresent = Boolean(legacyAnthropicEnv && fs.existsSync(legacyAnthropicEnv));
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
name: "ghost-auth",
|
|
13
|
+
present,
|
|
14
|
+
sev: present ? "ok" : "warn",
|
|
15
|
+
hint: present
|
|
16
|
+
? "~/.custom-api/custom-api.env present for ghost ensemble"
|
|
17
|
+
: legacyPresent
|
|
18
|
+
? "legacy ~/.claude-api-isolated/anthropic.env exists, but ghost now requires ~/.custom-api/custom-api.env"
|
|
19
|
+
: "create ~/.custom-api/custom-api.env so ghost ensemble can spawn",
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
5
23
|
function cmdDoctor(target, options = {}) {
|
|
6
24
|
const ai = path.join(target, "ai");
|
|
7
25
|
if (!fs.existsSync(ai)) { log("No 0dai config found. Run: 0dai init"); return; }
|
|
@@ -61,6 +79,7 @@ function cmdDoctor(target, options = {}) {
|
|
|
61
79
|
sev: process.env.GITHUB_TOKEN ? "ok" : "info",
|
|
62
80
|
hint: "Optional — for gh CLI, PR creation",
|
|
63
81
|
},
|
|
82
|
+
ghostAuthStatus(),
|
|
64
83
|
];
|
|
65
84
|
|
|
66
85
|
// Stack-specific creds
|
|
@@ -237,4 +256,4 @@ function cmdDoctor(target, options = {}) {
|
|
|
237
256
|
}
|
|
238
257
|
}
|
|
239
258
|
|
|
240
|
-
module.exports = { cmdDoctor };
|
|
259
|
+
module.exports = { cmdDoctor, ghostAuthStatus };
|
|
@@ -30,6 +30,10 @@ function cmdExperience(target, sub, args) {
|
|
|
30
30
|
if (periodIdx >= 0 && args[periodIdx + 1]) forwarded.push("--period", args[periodIdx + 1]);
|
|
31
31
|
const byIdx = args.indexOf("--by");
|
|
32
32
|
if (byIdx >= 0 && args[byIdx + 1]) forwarded.push("--by", args[byIdx + 1]);
|
|
33
|
+
} else if (command === "sync") {
|
|
34
|
+
if (args.includes("--json")) forwarded.push("--json");
|
|
35
|
+
const limitIdx = args.indexOf("--limit");
|
|
36
|
+
if (limitIdx >= 0 && args[limitIdx + 1]) forwarded.push("--limit", args[limitIdx + 1]);
|
|
33
37
|
} else if (command === "warnings") {
|
|
34
38
|
const detectorScript = findRepoScript(target, "anti_pattern_detector.py");
|
|
35
39
|
if (!detectorScript) { log("anti-pattern detector unavailable"); process.exit(1); }
|
|
@@ -53,7 +57,7 @@ function cmdExperience(target, sub, args) {
|
|
|
53
57
|
if (typeof dr.status === "number") process.exit(dr.status);
|
|
54
58
|
process.exit(1);
|
|
55
59
|
} else {
|
|
56
|
-
console.log("Usage: 0dai experience [list|stats|warnings|dismiss]");
|
|
60
|
+
console.log("Usage: 0dai experience [list|stats|sync|warnings|dismiss]");
|
|
57
61
|
process.exit(1);
|
|
58
62
|
}
|
|
59
63
|
|
package/lib/commands/feedback.js
CHANGED
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
const shared = require("../shared");
|
|
3
|
-
const { log, T, R, D, fs, path, apiCall } = shared;
|
|
3
|
+
const { log, T, R, D, fs, path, apiCall, CONFIG_DIR } = shared;
|
|
4
|
+
|
|
5
|
+
function writeFeedbackOutbox(target, report, result) {
|
|
6
|
+
const outboxDir = path.join(CONFIG_DIR, "feedback-outbox");
|
|
7
|
+
fs.mkdirSync(outboxDir, { recursive: true, mode: 0o700 });
|
|
8
|
+
const safeName = path.basename(target).replace(/[^a-zA-Z0-9._-]+/g, "_") || "project";
|
|
9
|
+
const outboxPath = path.join(outboxDir, `${safeName}-${Date.now()}-${process.pid}.json`);
|
|
10
|
+
fs.writeFileSync(outboxPath, JSON.stringify({
|
|
11
|
+
queued_at: new Date().toISOString(),
|
|
12
|
+
project: path.basename(target),
|
|
13
|
+
report,
|
|
14
|
+
error: result && result.error ? result.error : "feedback endpoint did not acknowledge receipt",
|
|
15
|
+
response: result || null,
|
|
16
|
+
}, null, 2) + "\n", { mode: 0o600 });
|
|
17
|
+
return outboxPath;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function listFeedbackOutbox() {
|
|
21
|
+
const outboxDir = path.join(CONFIG_DIR, "feedback-outbox");
|
|
22
|
+
try {
|
|
23
|
+
return fs.readdirSync(outboxDir)
|
|
24
|
+
.filter((name) => name.endsWith(".json"))
|
|
25
|
+
.sort()
|
|
26
|
+
.map((name) => path.join(outboxDir, name));
|
|
27
|
+
} catch {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function feedbackAccepted(result) {
|
|
33
|
+
return Boolean(result && (result.received || result.ok));
|
|
34
|
+
}
|
|
4
35
|
|
|
5
36
|
async function cmdFeedbackPush(target) {
|
|
6
37
|
const fbDir = path.join(target, "ai", "feedback");
|
|
@@ -44,17 +75,70 @@ async function cmdFeedbackPush(target) {
|
|
|
44
75
|
};
|
|
45
76
|
log(`pushing ${items.length} feedback item(s)...`);
|
|
46
77
|
const result = await apiCall("/v1/feedback", { report });
|
|
47
|
-
if (result
|
|
78
|
+
if (feedbackAccepted(result)) {
|
|
48
79
|
log(`received${result.issue ? `: ${result.issue}` : ""}`);
|
|
49
80
|
if (result.bonus) log(`${T}bonus:${R} ${result.bonus}`);
|
|
81
|
+
if (result.warning) console.log(` ${D}${result.warning}${R}`);
|
|
82
|
+
if (result.issue_error) console.log(` ${D}issue warning: ${result.issue_error}${R}`);
|
|
50
83
|
// Archive pushed entries
|
|
51
84
|
if (fs.existsSync(jsonlPath)) {
|
|
52
85
|
const archivePath = path.join(fbDir, `pushed-${Date.now()}.jsonl`);
|
|
53
86
|
fs.renameSync(jsonlPath, archivePath);
|
|
54
87
|
}
|
|
55
88
|
} else {
|
|
56
|
-
|
|
89
|
+
const outboxPath = writeFeedbackOutbox(target, report, result);
|
|
90
|
+
log(`error: ${(result && result.error) || "unknown"}`);
|
|
91
|
+
console.log(` ${D}queued for retry: ${outboxPath}${R}`);
|
|
92
|
+
console.log(` ${D}source feedback was left in place${R}`);
|
|
93
|
+
process.exitCode = 1;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function cmdFeedbackRetry(args = []) {
|
|
98
|
+
const files = listFeedbackOutbox();
|
|
99
|
+
const json = args.includes("--json");
|
|
100
|
+
if (!files.length) {
|
|
101
|
+
if (json) console.log(JSON.stringify({ retried: 0, remaining: 0 }, null, 2));
|
|
102
|
+
else log("no queued feedback");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let retried = 0;
|
|
107
|
+
const failed = [];
|
|
108
|
+
for (const file of files) {
|
|
109
|
+
let queued = null;
|
|
110
|
+
try {
|
|
111
|
+
queued = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
112
|
+
} catch (err) {
|
|
113
|
+
failed.push({ file, error: `invalid outbox file: ${err.message || err}` });
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const report = queued.report || (queued.payload && queued.payload.report);
|
|
118
|
+
if (!report) {
|
|
119
|
+
failed.push({ file, error: "outbox file is missing report" });
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const result = await apiCall("/v1/feedback", { report });
|
|
124
|
+
if (feedbackAccepted(result)) {
|
|
125
|
+
try { fs.unlinkSync(file); } catch {}
|
|
126
|
+
retried++;
|
|
127
|
+
if (!json) log(`retried: ${path.basename(file)}${result.issue ? ` -> ${result.issue}` : ""}`);
|
|
128
|
+
} else {
|
|
129
|
+
failed.push({ file, error: (result && result.error) || "unknown", response: result || null });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (json) {
|
|
134
|
+
console.log(JSON.stringify({ retried, remaining: failed.length, failed }, null, 2));
|
|
135
|
+
} else {
|
|
136
|
+
log(`retry complete: ${retried} sent, ${failed.length} remaining`);
|
|
137
|
+
for (const item of failed.slice(0, 5)) {
|
|
138
|
+
console.log(` ${D}${path.basename(item.file)}: ${item.error}${R}`);
|
|
139
|
+
}
|
|
57
140
|
}
|
|
141
|
+
if (failed.length) process.exitCode = 1;
|
|
58
142
|
}
|
|
59
143
|
|
|
60
144
|
async function cmdFeedback(target, sub, args) {
|
|
@@ -63,6 +147,9 @@ async function cmdFeedback(target, sub, args) {
|
|
|
63
147
|
if (sub === "push") {
|
|
64
148
|
return cmdFeedbackPush(target);
|
|
65
149
|
}
|
|
150
|
+
if (sub === "retry") {
|
|
151
|
+
return cmdFeedbackRetry(args);
|
|
152
|
+
}
|
|
66
153
|
if (sub === "log") {
|
|
67
154
|
const type = args.find((_, i) => args[i - 1] === "--type") || "suggestion";
|
|
68
155
|
const detail = args.find((_, i) => args[i - 1] === "--detail") || "";
|
|
@@ -86,7 +173,7 @@ async function cmdFeedback(target, sub, args) {
|
|
|
86
173
|
} catch { log("no feedback directory"); }
|
|
87
174
|
return;
|
|
88
175
|
}
|
|
89
|
-
console.log("Usage: 0dai feedback [push|log|list] [--type ...] [--detail '...']");
|
|
176
|
+
console.log("Usage: 0dai feedback [push|retry|log|list] [--type ...] [--detail '...']");
|
|
90
177
|
}
|
|
91
178
|
|
|
92
|
-
module.exports = { cmdFeedbackPush, cmdFeedback };
|
|
179
|
+
module.exports = { cmdFeedbackPush, cmdFeedbackRetry, cmdFeedback, writeFeedbackOutbox, listFeedbackOutbox, feedbackAccepted };
|