@iann29/synapse 1.7.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/synapse.js +69 -537
- package/lib/commands/_context.js +133 -0
- package/lib/commands/_dispatcher.js +75 -0
- package/lib/commands/_help.js +105 -0
- package/lib/commands/convex.js +151 -0
- package/lib/commands/credentials.js +85 -0
- package/lib/commands/deploy.js +72 -0
- package/lib/commands/dev.js +24 -0
- package/lib/commands/doctor.js +82 -0
- package/lib/commands/login.js +43 -0
- package/lib/commands/logout.js +20 -0
- package/lib/commands/open.js +96 -0
- package/lib/commands/select.js +239 -0
- package/lib/commands/status.js +173 -0
- package/lib/commands/version.js +77 -0
- package/lib/commands/whoami.js +22 -0
- package/lib/doctor/checks.js +484 -0
- package/lib/doctor/renderer.js +93 -0
- package/lib/doctor/runner.js +148 -0
- package/lib/output.js +140 -0
- package/package.json +1 -1
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// Runs the doctor checks with the DAG defined in checks.js (each
|
|
2
|
+
// check declares its `dependsOn` ids). Within a tier, checks run in
|
|
3
|
+
// parallel via Promise.all; dependent checks wait on their prereqs.
|
|
4
|
+
//
|
|
5
|
+
// The orchestrator produces a stable DoctorReport — same tree feeds
|
|
6
|
+
// both the human renderer and `--json`.
|
|
7
|
+
|
|
8
|
+
const { ALL_CHECKS } = require("./checks");
|
|
9
|
+
|
|
10
|
+
const SCHEMA_VERSION = "1.0";
|
|
11
|
+
|
|
12
|
+
async function runChecks(checks, ctx) {
|
|
13
|
+
// Topological execution: for each check, wait for its dependencies
|
|
14
|
+
// (already-resolved CheckResults) before invoking run(). Failed or
|
|
15
|
+
// skipped prereqs cascade to skipped state (don't run the dependent).
|
|
16
|
+
const resultsById = new Map();
|
|
17
|
+
const order = []; // preserve insertion order for the final report
|
|
18
|
+
// Resolve sequentially through the catalog — checks declared earlier
|
|
19
|
+
// become available as deps for later ones. Inside a "tier" we still
|
|
20
|
+
// get the parallel benefit because awaiting an already-resolved
|
|
21
|
+
// result is a microtask, not a roundtrip.
|
|
22
|
+
for (const check of checks) {
|
|
23
|
+
order.push(check.id);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// First pass: kick off every check whose dependencies are all "ok"
|
|
27
|
+
// or "warn"; cascade-skip the rest.
|
|
28
|
+
const pending = new Map();
|
|
29
|
+
for (const check of checks) {
|
|
30
|
+
pending.set(check.id, check);
|
|
31
|
+
}
|
|
32
|
+
while (pending.size > 0) {
|
|
33
|
+
let advanced = false;
|
|
34
|
+
for (const [id, check] of pending) {
|
|
35
|
+
const depsResolved = (check.dependsOn || []).every((d) => resultsById.has(d));
|
|
36
|
+
if (!depsResolved) continue;
|
|
37
|
+
const failedDeps = (check.dependsOn || []).filter((d) => {
|
|
38
|
+
const r = resultsById.get(d);
|
|
39
|
+
return r.status === "issue" || r.status === "skipped";
|
|
40
|
+
});
|
|
41
|
+
if (failedDeps.length > 0) {
|
|
42
|
+
resultsById.set(id, {
|
|
43
|
+
id,
|
|
44
|
+
category: check.category,
|
|
45
|
+
title: check.title,
|
|
46
|
+
status: "skipped",
|
|
47
|
+
summary: `skipped (prereq failed: ${failedDeps.join(", ")})`,
|
|
48
|
+
data: { skippedBecause: failedDeps },
|
|
49
|
+
durationMs: 0,
|
|
50
|
+
});
|
|
51
|
+
pending.delete(id);
|
|
52
|
+
advanced = true;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const result = await check.run(ctx);
|
|
56
|
+
resultsById.set(id, {
|
|
57
|
+
id,
|
|
58
|
+
category: check.category,
|
|
59
|
+
title: check.title,
|
|
60
|
+
...result,
|
|
61
|
+
});
|
|
62
|
+
pending.delete(id);
|
|
63
|
+
advanced = true;
|
|
64
|
+
}
|
|
65
|
+
if (!advanced) {
|
|
66
|
+
// Cycle in deps — should never happen; bail to avoid an infinite
|
|
67
|
+
// loop. Mark anything still pending as a hard error.
|
|
68
|
+
for (const [id, check] of pending) {
|
|
69
|
+
resultsById.set(id, {
|
|
70
|
+
id,
|
|
71
|
+
category: check.category,
|
|
72
|
+
title: check.title,
|
|
73
|
+
status: "issue",
|
|
74
|
+
summary: `cycle in dependsOn: ${(check.dependsOn || []).join(", ")}`,
|
|
75
|
+
durationMs: 0,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return order.map((id) => resultsById.get(id));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function totalize(results) {
|
|
86
|
+
const t = { ok: 0, warn: 0, issue: 0, skipped: 0, fixed: 0 };
|
|
87
|
+
for (const r of results) {
|
|
88
|
+
if (r.fixedBy) t.fixed += 1;
|
|
89
|
+
if (r.status === "ok") t.ok += 1;
|
|
90
|
+
else if (r.status === "warn") t.warn += 1;
|
|
91
|
+
else if (r.status === "issue") t.issue += 1;
|
|
92
|
+
else if (r.status === "skipped") t.skipped += 1;
|
|
93
|
+
}
|
|
94
|
+
return t;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function exitCodeFor(totals) {
|
|
98
|
+
if (totals.issue > 0) return 2;
|
|
99
|
+
if (totals.warn > 0) return 1;
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function applyAutoFixes(results, checks, ctx, { allowPrompt = false } = {}) {
|
|
104
|
+
// Walk results, find non-ok ones whose check has autoFix=auto (or
|
|
105
|
+
// prompt + allowPrompt=true), invoke the fix, re-run THAT check.
|
|
106
|
+
// Stays simple: one fix pass, no recursive heal-then-recheck loops.
|
|
107
|
+
const byId = new Map(checks.map((c) => [c.id, c]));
|
|
108
|
+
for (const r of results) {
|
|
109
|
+
if (r.status === "ok" || r.status === "skipped") continue;
|
|
110
|
+
const check = byId.get(r.id);
|
|
111
|
+
if (!check || typeof check.fix !== "function") continue;
|
|
112
|
+
if (check.autoFix === "never") continue;
|
|
113
|
+
if (check.autoFix === "prompt" && !allowPrompt) continue;
|
|
114
|
+
const fixOutcome = await check.fix(ctx, r);
|
|
115
|
+
if (fixOutcome.kind === "applied") {
|
|
116
|
+
// Re-run the check; if it's green now, mark fixedBy.
|
|
117
|
+
const fresh = await check.run(ctx);
|
|
118
|
+
Object.assign(r, fresh, { fixedBy: fixOutcome.message });
|
|
119
|
+
} else if (fixOutcome.kind === "failed") {
|
|
120
|
+
r.detail = (r.detail ? r.detail + "\n" : "") + `fix attempt failed: ${fixOutcome.message}`;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function generateReport(ctx, { fix = false, allowPrompt = false } = {}) {
|
|
126
|
+
const t0 = Date.now();
|
|
127
|
+
const results = await runChecks(ALL_CHECKS, ctx);
|
|
128
|
+
if (fix) {
|
|
129
|
+
await applyAutoFixes(results, ALL_CHECKS, ctx, { allowPrompt });
|
|
130
|
+
}
|
|
131
|
+
const totals = totalize(results);
|
|
132
|
+
const exitCode = exitCodeFor(totals);
|
|
133
|
+
return {
|
|
134
|
+
schemaVersion: SCHEMA_VERSION,
|
|
135
|
+
cliVersion: require("../../package.json").version,
|
|
136
|
+
env: {
|
|
137
|
+
cwd: ctx.cwd,
|
|
138
|
+
synapseUrl: ctx.cfg?.baseUrl ?? null,
|
|
139
|
+
user: ctx.cfg?.user?.email ?? null,
|
|
140
|
+
},
|
|
141
|
+
results,
|
|
142
|
+
totals,
|
|
143
|
+
durationMs: Date.now() - t0,
|
|
144
|
+
exitCode,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
module.exports = { runChecks, totalize, exitCodeFor, applyAutoFixes, generateReport, SCHEMA_VERSION };
|
package/lib/output.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// Shared output layer for every synapse CLI command.
|
|
2
|
+
//
|
|
3
|
+
// Every handler writes through `createOutput()` instead of poking
|
|
4
|
+
// process.stdout directly. This is the only thing that makes `--json`
|
|
5
|
+
// honest: in JSON mode the renderer never emits ANSI codes, partial
|
|
6
|
+
// human strings, or stray progress lines that would corrupt a piped
|
|
7
|
+
// script's parse.
|
|
8
|
+
//
|
|
9
|
+
// Contract:
|
|
10
|
+
// - `result(data, renderHuman)` is THE command result. In JSON mode
|
|
11
|
+
// emits `JSON.stringify(data)` to stdout. In human mode invokes
|
|
12
|
+
// renderHuman(data, ctx).
|
|
13
|
+
// - `info`/`warn` are side-channel status (progress, hints). They go
|
|
14
|
+
// to stderr and are SUPPRESSED entirely in JSON mode so a piped
|
|
15
|
+
// `--json` stream stays parse-clean.
|
|
16
|
+
// - `error(msg, { hint })` writes to stderr. In JSON mode emits
|
|
17
|
+
// `{error, hint}` so scripts can branch on it.
|
|
18
|
+
// - `table(rows, columns)` is a small helper that lays out aligned
|
|
19
|
+
// columns in human mode; in JSON mode the caller passes `rows`
|
|
20
|
+
// directly to `result()` instead.
|
|
21
|
+
|
|
22
|
+
const colors = require("./colors");
|
|
23
|
+
|
|
24
|
+
function createOutput({
|
|
25
|
+
json = false,
|
|
26
|
+
stdout = process.stdout,
|
|
27
|
+
stderr = process.stderr,
|
|
28
|
+
} = {}) {
|
|
29
|
+
function writeOut(s) {
|
|
30
|
+
stdout.write(s);
|
|
31
|
+
}
|
|
32
|
+
function writeErr(s) {
|
|
33
|
+
stderr.write(s);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
json,
|
|
38
|
+
stdout,
|
|
39
|
+
stderr,
|
|
40
|
+
|
|
41
|
+
// The command's structured result. JSON mode emits a single line.
|
|
42
|
+
// Human mode delegates rendering — the renderer is free to write
|
|
43
|
+
// tables, multi-line copy, colors, anything. Callers MUST supply
|
|
44
|
+
// a renderer; commands with no useful human output should pass a
|
|
45
|
+
// no-op (`() => {}`) explicitly so the JSON-vs-human split stays
|
|
46
|
+
// obvious in code review.
|
|
47
|
+
result(data, renderHuman) {
|
|
48
|
+
if (this.json) {
|
|
49
|
+
writeOut(JSON.stringify(data) + "\n");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
renderHuman(data, { stdout, stderr });
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
// Progress / status. Suppressed under --json so a piped consumer
|
|
56
|
+
// never sees mixed JSON and prose on stdout.
|
|
57
|
+
info(msg) {
|
|
58
|
+
if (this.json) return;
|
|
59
|
+
writeErr(msg + "\n");
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
warn(msg) {
|
|
63
|
+
if (this.json) return;
|
|
64
|
+
writeErr(colors.yellow("Warning: ") + msg + "\n");
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
// Fatal-ish error message. Returns nothing — callers throw or
|
|
68
|
+
// set process.exitCode on their own to control control flow.
|
|
69
|
+
// The dispatcher's top-level catch is the canonical exit path.
|
|
70
|
+
error(msg, { hint } = {}) {
|
|
71
|
+
if (this.json) {
|
|
72
|
+
writeErr(JSON.stringify({ error: msg, ...(hint ? { hint } : {}) }) + "\n");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
writeErr(colors.red("Error: ") + msg + "\n");
|
|
76
|
+
if (hint) writeErr(colors.dim("Hint: ") + hint + "\n");
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
// Lightweight column-aligned table renderer for human mode.
|
|
80
|
+
// `columns` is [{ key, header, align?, color? }]. `align` is
|
|
81
|
+
// "left" (default) or "right". `color` is a colors.* function
|
|
82
|
+
// applied per-cell after the value is stringified.
|
|
83
|
+
//
|
|
84
|
+
// In JSON mode the function is a no-op — callers should hand the
|
|
85
|
+
// raw rows array to `result()` instead.
|
|
86
|
+
table(rows, columns) {
|
|
87
|
+
if (this.json) return;
|
|
88
|
+
if (!rows || rows.length === 0) {
|
|
89
|
+
writeOut(colors.dim("(no rows)\n"));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const widths = columns.map((c) => c.header.length);
|
|
93
|
+
const cells = rows.map((row) =>
|
|
94
|
+
columns.map((c, i) => {
|
|
95
|
+
const raw = row[c.key];
|
|
96
|
+
const text = raw == null ? "" : String(raw);
|
|
97
|
+
if (text.length > widths[i]) widths[i] = text.length;
|
|
98
|
+
return text;
|
|
99
|
+
}),
|
|
100
|
+
);
|
|
101
|
+
const fmt = (text, w, align) =>
|
|
102
|
+
align === "right" ? text.padStart(w) : text.padEnd(w);
|
|
103
|
+
const header = columns
|
|
104
|
+
.map((c, i) => colors.dim(fmt(c.header, widths[i], c.align)))
|
|
105
|
+
.join(" ");
|
|
106
|
+
writeOut(header + "\n");
|
|
107
|
+
writeOut(
|
|
108
|
+
colors.dim(widths.map((w) => "─".repeat(w)).join(" ")) + "\n",
|
|
109
|
+
);
|
|
110
|
+
for (const row of cells) {
|
|
111
|
+
const line = row
|
|
112
|
+
.map((cell, i) => {
|
|
113
|
+
const padded = fmt(cell, widths[i], columns[i].align);
|
|
114
|
+
return columns[i].color ? columns[i].color(padded) : padded;
|
|
115
|
+
})
|
|
116
|
+
.join(" ");
|
|
117
|
+
writeOut(line + "\n");
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Detects `--json` from an args vector and returns
|
|
124
|
+
// `{ json: boolean, rest: string[] }` so handlers can strip the flag
|
|
125
|
+
// before parsing their own positionals. Bash/CI scripts commonly
|
|
126
|
+
// pass `--json` in any position; we accept it anywhere.
|
|
127
|
+
function extractJsonFlag(argv) {
|
|
128
|
+
const rest = [];
|
|
129
|
+
let json = false;
|
|
130
|
+
for (const a of argv) {
|
|
131
|
+
if (a === "--json") {
|
|
132
|
+
json = true;
|
|
133
|
+
} else {
|
|
134
|
+
rest.push(a);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return { json, rest };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = { createOutput, extractJsonFlag };
|