@iann29/synapse 1.8.6 → 1.8.8
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 +18 -1
- package/lib/api.js +54 -0
- package/lib/commands/_help.js +2 -1
- package/lib/commands/_resource.js +152 -0
- package/lib/commands/deployment-create.js +122 -0
- package/lib/commands/deployment-delete.js +117 -0
- package/lib/commands/deployment-rotate-key.js +173 -0
- package/lib/commands/deployment-status.js +143 -0
- package/lib/commands/env-list.js +105 -0
- package/lib/commands/env-pull.js +117 -0
- package/lib/commands/env-push.js +248 -0
- package/lib/commands/env-set.js +129 -0
- package/lib/commands/env-unset.js +77 -0
- package/lib/commands/https-doctor.js +73 -0
- package/lib/commands/https-migrate.js +168 -0
- package/lib/commands/https-remove.js +103 -0
- package/lib/commands/https-setup.js +263 -0
- package/lib/commands/https-status.js +154 -0
- package/lib/commands/list.js +186 -0
- package/lib/https/detect.js +603 -0
- package/lib/https/executor.js +102 -0
- package/lib/https/hosts.js +356 -0
- package/lib/https/mkcert.js +131 -0
- package/lib/https/nextjs.js +91 -0
- package/lib/https/planner.js +412 -0
- package/lib/prompts.js +50 -0
- package/package.json +1 -1
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
// `synapse https setup <domain>` — the wizard.
|
|
2
|
+
//
|
|
3
|
+
// Flow (all idempotent — re-running is a no-op on a healthy machine):
|
|
4
|
+
// 1. SCAN — read 16 detection vars (read-only, ~50ms)
|
|
5
|
+
// 2. PLAN — turn detections into a list of steps
|
|
6
|
+
// 3. PREVIEW — print the plan; ask "ok?" unless --yes
|
|
7
|
+
// 4. EXECUTE — run each step; stop on failure (steps converge on
|
|
8
|
+
// re-run because each is independently idempotent)
|
|
9
|
+
// 5. VERIFY — re-scan + print summary
|
|
10
|
+
//
|
|
11
|
+
// Flags:
|
|
12
|
+
// --force regenerate the cert even if present
|
|
13
|
+
// --yes skip the preview confirm prompt
|
|
14
|
+
// --dry-run SCAN + PLAN + PREVIEW; never execute
|
|
15
|
+
// --skip-hosts skip the hosts file step
|
|
16
|
+
// --skip-script skip the package.json mutation
|
|
17
|
+
// --json machine-readable output
|
|
18
|
+
// --verbose print per-step duration + outcome details
|
|
19
|
+
|
|
20
|
+
const colors = require("../colors");
|
|
21
|
+
const { extractFlags } = require("./_resource");
|
|
22
|
+
const { confirm } = require("../prompts");
|
|
23
|
+
const detectMod = require("../https/detect");
|
|
24
|
+
const plannerMod = require("../https/planner");
|
|
25
|
+
const executorMod = require("../https/executor");
|
|
26
|
+
|
|
27
|
+
function renderPlan(steps, { stdout }) {
|
|
28
|
+
for (const step of steps) {
|
|
29
|
+
const symbol = symbolFor(step.kind);
|
|
30
|
+
stdout.write(` ${symbol} ${step.title}\n`);
|
|
31
|
+
if (step.kind === "blocker") {
|
|
32
|
+
stdout.write(colors.red(` blocker: ${step.blocker || step.reason}\n`));
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (step.reason) {
|
|
36
|
+
stdout.write(colors.dim(` ${step.reason}\n`));
|
|
37
|
+
}
|
|
38
|
+
if (step.skipReason && step.kind !== "exec") {
|
|
39
|
+
stdout.write(colors.dim(` ${step.skipReason}\n`));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function symbolFor(kind) {
|
|
45
|
+
switch (kind) {
|
|
46
|
+
case "exec":
|
|
47
|
+
return colors.cyan("●");
|
|
48
|
+
case "skip":
|
|
49
|
+
return colors.dim("○");
|
|
50
|
+
case "warn":
|
|
51
|
+
return colors.yellow("!");
|
|
52
|
+
case "blocker":
|
|
53
|
+
return colors.red("✗");
|
|
54
|
+
default:
|
|
55
|
+
return " ";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function renderResults(results, { stdout }) {
|
|
60
|
+
for (const r of results) {
|
|
61
|
+
const sym =
|
|
62
|
+
r.kind === "ok"
|
|
63
|
+
? colors.green("✓")
|
|
64
|
+
: r.kind === "failed"
|
|
65
|
+
? colors.red("✗")
|
|
66
|
+
: r.kind === "blocker"
|
|
67
|
+
? colors.red("✗")
|
|
68
|
+
: r.kind === "warn"
|
|
69
|
+
? colors.yellow("!")
|
|
70
|
+
: colors.dim("○");
|
|
71
|
+
const tail =
|
|
72
|
+
r.kind === "ok"
|
|
73
|
+
? colors.dim(` ${r.durationMs}ms`)
|
|
74
|
+
: r.kind === "failed"
|
|
75
|
+
? colors.red(` ${r.outcome.error || "failed"}`)
|
|
76
|
+
: "";
|
|
77
|
+
stdout.write(` ${sym} ${r.title}${tail}\n`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = {
|
|
82
|
+
name: "https setup",
|
|
83
|
+
summary: "Configure local HTTPS for a Next.js dev domain (mkcert + hosts + package.json).",
|
|
84
|
+
usage:
|
|
85
|
+
"synapse https setup <domain> [--force] [--yes] [--dry-run] [--skip-hosts] [--skip-script] [--verbose] [--json]",
|
|
86
|
+
description: `Sets up self-signed HTTPS for a custom dev domain (e.g. dev.myproject.com) so \`next dev --experimental-https\` works without browser warnings.
|
|
87
|
+
|
|
88
|
+
Does six things in one pass:
|
|
89
|
+
1. Detects mkcert + your OS + DNS state + cwd's package.json
|
|
90
|
+
2. Ensures the local CA is trusted (mkcert -install, idempotent)
|
|
91
|
+
3. Generates a cert pair at ~/.config/dev-certs/<domain>/
|
|
92
|
+
4. Adds a 127.0.0.1 entry to your hosts file (sudo/UAC prompt if needed)
|
|
93
|
+
— skipped automatically if your public DNS already resolves to loopback
|
|
94
|
+
5. Adds/updates the dev:https script in package.json
|
|
95
|
+
6. Verifies + prints "now run \`npm run dev:https\`"
|
|
96
|
+
|
|
97
|
+
Re-running is safe (every step is idempotent). The whole thing works
|
|
98
|
+
identically on Linux, macOS, Windows, and WSL.
|
|
99
|
+
|
|
100
|
+
Flags:
|
|
101
|
+
--force regenerate the cert even if one exists
|
|
102
|
+
--yes skip the preview confirmation prompt
|
|
103
|
+
--dry-run show the plan, don't execute (useful for partners
|
|
104
|
+
on Windows who want to see what will happen first)
|
|
105
|
+
--skip-hosts don't touch the hosts file
|
|
106
|
+
--skip-script don't touch package.json
|
|
107
|
+
--verbose per-step duration + outcome details
|
|
108
|
+
--json machine-readable output for CI
|
|
109
|
+
|
|
110
|
+
Examples:
|
|
111
|
+
synapse https setup dev.myproject.com
|
|
112
|
+
synapse https setup dev.myproject.com --dry-run --verbose
|
|
113
|
+
synapse https setup dev.myproject.com --force --yes`,
|
|
114
|
+
|
|
115
|
+
// Exports for tests
|
|
116
|
+
renderPlan,
|
|
117
|
+
renderResults,
|
|
118
|
+
symbolFor,
|
|
119
|
+
|
|
120
|
+
async run(args, ctx) {
|
|
121
|
+
const { flags, rest } = extractFlags(args, {
|
|
122
|
+
string: [],
|
|
123
|
+
boolean: ["force", "yes", "dry-run", "skip-hosts", "skip-script", "verbose", "json"],
|
|
124
|
+
});
|
|
125
|
+
const domain = rest[0];
|
|
126
|
+
if (!domain) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
"Usage: synapse https setup <domain>\n\nExample: synapse https setup dev.myproject.com",
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
if (rest.length > 1) {
|
|
132
|
+
throw new Error(`Unexpected positional: ${rest[1]}.`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const force = flags.force === true;
|
|
136
|
+
const yes = flags.yes === true;
|
|
137
|
+
const dryRun = flags["dry-run"] === true;
|
|
138
|
+
const skipHosts = flags["skip-hosts"] === true;
|
|
139
|
+
const skipScript = flags["skip-script"] === true;
|
|
140
|
+
const verbose = flags.verbose === true;
|
|
141
|
+
|
|
142
|
+
// ---- Phase 1: SCAN ------------------------------------------------
|
|
143
|
+
if (!ctx.out.json) ctx.out.info("Scanning environment…");
|
|
144
|
+
const detection = await detectMod.scan({ domain, cwd: ctx.cwd });
|
|
145
|
+
|
|
146
|
+
// ---- Phase 2: PLAN ------------------------------------------------
|
|
147
|
+
const steps = plannerMod.plan(detection, { force, skipHosts, skipScript });
|
|
148
|
+
const executable = plannerMod.planIsExecutable(steps);
|
|
149
|
+
|
|
150
|
+
// ---- Phase 3: PREVIEW --------------------------------------------
|
|
151
|
+
if (!ctx.out.json) {
|
|
152
|
+
ctx.out.stdout.write(
|
|
153
|
+
`\n${colors.bold("Plan")} ${colors.dim(`for ${domain} on ${detection.platform.id}`)}\n`,
|
|
154
|
+
);
|
|
155
|
+
renderPlan(steps, { stdout: ctx.out.stdout });
|
|
156
|
+
ctx.out.stdout.write("\n");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!executable) {
|
|
160
|
+
const blocker = steps.find((s) => s.kind === "blocker");
|
|
161
|
+
if (ctx.out.json) {
|
|
162
|
+
ctx.out.result(
|
|
163
|
+
{ domain, blocked: true, steps, blocker: blocker?.blocker || blocker?.reason },
|
|
164
|
+
() => {},
|
|
165
|
+
);
|
|
166
|
+
process.exitCode = 1;
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
ctx.out.error(blocker?.blocker || "Plan is blocked.");
|
|
170
|
+
process.exitCode = 1;
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (dryRun) {
|
|
175
|
+
if (ctx.out.json) {
|
|
176
|
+
ctx.out.result({ domain, dryRun: true, steps }, () => {});
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
ctx.out.info("(dry-run — not executing)");
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const willChange = steps.some((s) => s.kind === "exec");
|
|
184
|
+
if (!willChange) {
|
|
185
|
+
if (ctx.out.json) {
|
|
186
|
+
ctx.out.result({ domain, executedAny: false, steps }, () => {});
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
ctx.out.stdout.write(
|
|
190
|
+
colors.green("Nothing to do — everything already in the desired state.\n"),
|
|
191
|
+
);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (!yes && !ctx.out.json) {
|
|
196
|
+
const ok = await confirm(`Apply this plan? [Y/n] `, { defaultAnswer: true });
|
|
197
|
+
if (!ok) {
|
|
198
|
+
ctx.out.info("Aborted.");
|
|
199
|
+
process.exitCode = 1;
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (!yes && ctx.out.json) {
|
|
204
|
+
throw new Error("Refusing to execute in --json mode without --yes.");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ---- Phase 4: EXECUTE --------------------------------------------
|
|
208
|
+
const { results, failedAny } = await executorMod.execute(steps, {
|
|
209
|
+
out: ctx.out,
|
|
210
|
+
verbose,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// ---- Phase 5: VERIFY ---------------------------------------------
|
|
214
|
+
const after = await detectMod.scan({ domain, cwd: ctx.cwd });
|
|
215
|
+
|
|
216
|
+
const summary = {
|
|
217
|
+
domain,
|
|
218
|
+
platform: detection.platform.id,
|
|
219
|
+
certPath: after.existingCerts?.certPath ?? null,
|
|
220
|
+
keyPath: after.existingCerts?.keyPath ?? null,
|
|
221
|
+
hostsEntry: after.hosts.matches.length > 0,
|
|
222
|
+
devHttpsScript: after.pkg.existingDevHttps,
|
|
223
|
+
results,
|
|
224
|
+
failedAny,
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
if (ctx.out.json) {
|
|
228
|
+
ctx.out.result(summary, () => {});
|
|
229
|
+
if (failedAny) process.exitCode = 1;
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
ctx.out.stdout.write("\n");
|
|
234
|
+
renderResults(results, { stdout: ctx.out.stdout });
|
|
235
|
+
ctx.out.stdout.write("\n");
|
|
236
|
+
if (failedAny) {
|
|
237
|
+
ctx.out.error(
|
|
238
|
+
"One or more steps failed. Fix the cause and re-run — every step is idempotent so safe steps won't re-do.",
|
|
239
|
+
);
|
|
240
|
+
process.exitCode = 1;
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
ctx.out.stdout.write(
|
|
244
|
+
`${colors.green("✓")} ${colors.bold(`HTTPS ready for ${domain}`)}\n`,
|
|
245
|
+
);
|
|
246
|
+
if (after.pkg.hasNext) {
|
|
247
|
+
ctx.out.stdout.write(colors.dim(`Next: run \`npm run dev:https\` in ${ctx.cwd}\n`));
|
|
248
|
+
} else {
|
|
249
|
+
ctx.out.stdout.write(
|
|
250
|
+
colors.dim(
|
|
251
|
+
`Cert lives at: ${after.existingCerts?.certPath}\nPair it with:\n next dev --experimental-https --experimental-https-cert <cert> --experimental-https-key <key> --hostname ${domain}\n`,
|
|
252
|
+
),
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
if (after.resolution.source === "dns") {
|
|
256
|
+
ctx.out.stdout.write(
|
|
257
|
+
colors.dim(
|
|
258
|
+
`(Public DNS A record points ${domain} → 127.0.0.1, so no /etc/hosts edit was needed. Partners on other machines also need no setup.)\n`,
|
|
259
|
+
),
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// `synapse https status [domain]` — diagnose what's already
|
|
2
|
+
// configured. With a domain: deep dive on that one. Without: list
|
|
3
|
+
// every domain under ~/.config/dev-certs/.
|
|
4
|
+
|
|
5
|
+
const fs = require("node:fs");
|
|
6
|
+
const path = require("node:path");
|
|
7
|
+
const colors = require("../colors");
|
|
8
|
+
const { extractFlags } = require("./_resource");
|
|
9
|
+
const detectMod = require("../https/detect");
|
|
10
|
+
|
|
11
|
+
function renderDomainStatus(detection, { stdout }) {
|
|
12
|
+
const sym = (b) => (b ? colors.green("✓") : colors.dim("·"));
|
|
13
|
+
stdout.write(`${colors.bold(detection.domain)}\n`);
|
|
14
|
+
stdout.write(
|
|
15
|
+
` ${sym(detection.mkcert.present)} mkcert ${
|
|
16
|
+
detection.mkcert.present ? colors.dim(detection.mkcert.version) : colors.red("missing")
|
|
17
|
+
}\n`,
|
|
18
|
+
);
|
|
19
|
+
stdout.write(
|
|
20
|
+
` ${sym(detection.caTrusted)} CA trusted${
|
|
21
|
+
detection.caTrusted ? "" : colors.red(" (run `mkcert -install`)")
|
|
22
|
+
}\n`,
|
|
23
|
+
);
|
|
24
|
+
const certPresent = detection.existingCerts && detection.existingCerts.present;
|
|
25
|
+
stdout.write(
|
|
26
|
+
` ${sym(certPresent)} cert pair ${
|
|
27
|
+
certPresent ? colors.dim(detection.existingCerts.certPath) : colors.dim("(not generated yet)")
|
|
28
|
+
}\n`,
|
|
29
|
+
);
|
|
30
|
+
if (certPresent && detection.existingCerts.expiry) {
|
|
31
|
+
stdout.write(colors.dim(` expires ${detection.existingCerts.expiry}\n`));
|
|
32
|
+
}
|
|
33
|
+
const resolves = detection.resolution.resolvesToLoopback;
|
|
34
|
+
const src = detection.resolution.source;
|
|
35
|
+
stdout.write(
|
|
36
|
+
` ${sym(resolves)} resolves to 127.0.0.1 ${
|
|
37
|
+
resolves ? colors.dim(`(via ${src})`) : colors.red("(no — add to hosts or DNS A record)")
|
|
38
|
+
}\n`,
|
|
39
|
+
);
|
|
40
|
+
const scriptOk = !!detection.pkg.existingDevHttps;
|
|
41
|
+
stdout.write(
|
|
42
|
+
` ${sym(scriptOk)} package.json dev:https ${
|
|
43
|
+
scriptOk
|
|
44
|
+
? colors.dim("set")
|
|
45
|
+
: detection.pkg.present
|
|
46
|
+
? colors.dim("(missing in cwd's package.json)")
|
|
47
|
+
: colors.dim("(no package.json in cwd)")
|
|
48
|
+
}\n`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = {
|
|
53
|
+
name: "https status",
|
|
54
|
+
summary: "Show local-HTTPS state for one domain or every configured domain.",
|
|
55
|
+
usage: "synapse https status [domain] [--json]",
|
|
56
|
+
description: `Without a domain: lists every cert directory in ~/.config/dev-certs/.
|
|
57
|
+
With a domain: prints a deep diagnostic (mkcert, CA trust, cert presence + expiry, DNS resolution, package.json script).`,
|
|
58
|
+
|
|
59
|
+
async run(args, ctx) {
|
|
60
|
+
const { flags, rest } = extractFlags(args, {
|
|
61
|
+
string: [],
|
|
62
|
+
boolean: ["json"],
|
|
63
|
+
});
|
|
64
|
+
const domain = rest[0];
|
|
65
|
+
if (rest.length > 1) {
|
|
66
|
+
throw new Error(`Unexpected positional: ${rest[1]}.`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!domain) {
|
|
70
|
+
// List mode. We accept TWO layouts in the cert store:
|
|
71
|
+
// 1. Canonical (v1.8.8+): `<root>/<domain>/<domain>.pem` + `-key.pem`
|
|
72
|
+
// 2. Flat (operator's pre-existing intermediate layout):
|
|
73
|
+
// `<root>/<domain>.pem` + `<domain>{-,.}key.pem`
|
|
74
|
+
// Flat-format certs aren't moved automatically — they show up
|
|
75
|
+
// here with a `flat: true` hint so `https migrate --root` can
|
|
76
|
+
// reorganise them.
|
|
77
|
+
const root = detectMod.certsRoot();
|
|
78
|
+
let entries = [];
|
|
79
|
+
try {
|
|
80
|
+
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
81
|
+
} catch {
|
|
82
|
+
entries = [];
|
|
83
|
+
}
|
|
84
|
+
const domains = new Set();
|
|
85
|
+
const flatDomains = new Set();
|
|
86
|
+
// (1) Subdirectories (canonical).
|
|
87
|
+
for (const e of entries) {
|
|
88
|
+
if (e.isDirectory()) domains.add(e.name);
|
|
89
|
+
}
|
|
90
|
+
// (2) Flat pairs in the root.
|
|
91
|
+
const flat = detectMod.detectFlatCertsInStore();
|
|
92
|
+
for (const p of flat) {
|
|
93
|
+
domains.add(p.domain);
|
|
94
|
+
flatDomains.add(p.domain);
|
|
95
|
+
}
|
|
96
|
+
const rows = await Promise.all(
|
|
97
|
+
[...domains].sort().map(async (d) => {
|
|
98
|
+
const det = await detectMod.scan({ domain: d, cwd: ctx.cwd });
|
|
99
|
+
return {
|
|
100
|
+
domain: d,
|
|
101
|
+
cert: det.existingCerts?.present ?? false,
|
|
102
|
+
expiry: det.existingCerts?.expiry ?? null,
|
|
103
|
+
resolves: det.resolution.resolvesToLoopback,
|
|
104
|
+
resolutionSource: det.resolution.source,
|
|
105
|
+
flat: flatDomains.has(d),
|
|
106
|
+
};
|
|
107
|
+
}),
|
|
108
|
+
);
|
|
109
|
+
ctx.out.result(
|
|
110
|
+
{ root, count: rows.length, domains: rows },
|
|
111
|
+
(_d, { stdout }) => {
|
|
112
|
+
stdout.write(colors.dim(`Cert store: ${root}\n`));
|
|
113
|
+
if (rows.length === 0) {
|
|
114
|
+
stdout.write(
|
|
115
|
+
colors.dim("(no certs configured — run `synapse https setup <domain>`)\n"),
|
|
116
|
+
);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
ctx.out.table(
|
|
120
|
+
rows.map((r) => ({
|
|
121
|
+
domain: colors.bold(r.domain) + (r.flat ? colors.dim(" (flat)") : ""),
|
|
122
|
+
cert: r.cert ? colors.green("yes") : colors.dim("no"),
|
|
123
|
+
expiry: r.expiry ? colors.dim(r.expiry.slice(0, 10)) : colors.dim("?"),
|
|
124
|
+
resolves: r.resolves
|
|
125
|
+
? colors.green(r.resolutionSource)
|
|
126
|
+
: colors.red("no"),
|
|
127
|
+
})),
|
|
128
|
+
[
|
|
129
|
+
{ key: "domain", header: "DOMAIN" },
|
|
130
|
+
{ key: "cert", header: "CERT" },
|
|
131
|
+
{ key: "expiry", header: "EXPIRES" },
|
|
132
|
+
{ key: "resolves", header: "LOOPBACK" },
|
|
133
|
+
],
|
|
134
|
+
);
|
|
135
|
+
if (rows.some((r) => r.flat)) {
|
|
136
|
+
stdout.write(
|
|
137
|
+
colors.dim(
|
|
138
|
+
"\n(flat) = cert lives directly in ~/.config/dev-certs/ instead of a subdirectory. Run `synapse https migrate --root=~/.config/dev-certs` to reorganise.\n",
|
|
139
|
+
),
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Single-domain detail mode.
|
|
148
|
+
const det = await detectMod.scan({ domain, cwd: ctx.cwd });
|
|
149
|
+
ctx.out.result(det, (d, { stdout }) => renderDomainStatus(d, { stdout }));
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
// Exports for tests.
|
|
153
|
+
renderDomainStatus,
|
|
154
|
+
};
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// `synapse list [deployments|projects|teams]` — read-only catalog
|
|
2
|
+
// dumper. Single-arg subcommand that mirrors what you'd see in the
|
|
3
|
+
// dashboard, but scriptable (every subcommand supports --json).
|
|
4
|
+
//
|
|
5
|
+
// Resolution rules:
|
|
6
|
+
// - `list teams` → no flag needed (always lists the operator's teams)
|
|
7
|
+
// - `list projects` → uses linked team OR --team=<slug|id>
|
|
8
|
+
// - `list deployments` → uses linked project OR --project=<id>
|
|
9
|
+
|
|
10
|
+
const colors = require("../colors");
|
|
11
|
+
const { extractTeamFlag, extractProjectFlag } = require("./_resource");
|
|
12
|
+
|
|
13
|
+
const KINDS = ["deployments", "projects", "teams"];
|
|
14
|
+
|
|
15
|
+
async function listTeams(ctx) {
|
|
16
|
+
const teams = await ctx.api.teams();
|
|
17
|
+
ctx.out.result(
|
|
18
|
+
{ kind: "teams", count: teams.length, teams },
|
|
19
|
+
(_d, { stdout }) => {
|
|
20
|
+
if (teams.length === 0) {
|
|
21
|
+
stdout.write(colors.dim("(no teams — create one in the dashboard)\n"));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
ctx.out.table(
|
|
25
|
+
teams.map((t) => ({
|
|
26
|
+
name: colors.bold(t.name || t.slug || t.id),
|
|
27
|
+
slug: t.slug || colors.dim("?"),
|
|
28
|
+
id: colors.dim(t.id),
|
|
29
|
+
})),
|
|
30
|
+
[
|
|
31
|
+
{ key: "name", header: "NAME" },
|
|
32
|
+
{ key: "slug", header: "SLUG" },
|
|
33
|
+
{ key: "id", header: "ID" },
|
|
34
|
+
],
|
|
35
|
+
);
|
|
36
|
+
},
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function listProjects(ctx, args) {
|
|
41
|
+
const { teamRef: explicit, rest } = extractTeamFlag(args);
|
|
42
|
+
if (rest.length > 0) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`Unexpected positional: ${rest[0]}. Usage: synapse list projects [--team=<slug|id>]`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
// Linked-project fallback uses ctx.projectConfig.team.slug.
|
|
48
|
+
let teamRef = explicit;
|
|
49
|
+
let source = "flag";
|
|
50
|
+
if (!teamRef) {
|
|
51
|
+
const linked = ctx.projectConfig;
|
|
52
|
+
if (linked && linked.team && (linked.team.slug || linked.team.id)) {
|
|
53
|
+
teamRef = linked.team.slug || linked.team.id;
|
|
54
|
+
source = "linked";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (!teamRef) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
"No linked team. Pass --team=<slug|id> or run `synapse select` first.",
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
const projects = await ctx.api.projects(teamRef);
|
|
63
|
+
ctx.out.result(
|
|
64
|
+
{ kind: "projects", team: teamRef, teamSource: source, count: projects.length, projects },
|
|
65
|
+
(_d, { stdout }) => {
|
|
66
|
+
stdout.write(colors.dim(`Team: ${teamRef}\n`));
|
|
67
|
+
if (projects.length === 0) {
|
|
68
|
+
stdout.write(colors.dim("(no projects)\n"));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
ctx.out.table(
|
|
72
|
+
projects.map((p) => ({
|
|
73
|
+
name: colors.bold(p.name || p.slug),
|
|
74
|
+
slug: p.slug || colors.dim("?"),
|
|
75
|
+
id: colors.dim(p.id),
|
|
76
|
+
})),
|
|
77
|
+
[
|
|
78
|
+
{ key: "name", header: "NAME" },
|
|
79
|
+
{ key: "slug", header: "SLUG" },
|
|
80
|
+
{ key: "id", header: "ID" },
|
|
81
|
+
],
|
|
82
|
+
);
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function listDeployments(ctx, args) {
|
|
88
|
+
const { projectId: explicit, rest } = extractProjectFlag(args);
|
|
89
|
+
if (rest.length > 0) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`Unexpected positional: ${rest[0]}. Usage: synapse list deployments [--project=<id>]`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
let projectId = explicit;
|
|
95
|
+
let source = "flag";
|
|
96
|
+
if (!projectId) {
|
|
97
|
+
const linked = ctx.projectConfig;
|
|
98
|
+
if (linked && linked.project && linked.project.id) {
|
|
99
|
+
projectId = linked.project.id;
|
|
100
|
+
source = "linked";
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (!projectId) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
"No linked project. Pass --project=<id> or run `synapse select` first.",
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
const deployments = await ctx.api.deployments(projectId);
|
|
109
|
+
ctx.out.result(
|
|
110
|
+
{ kind: "deployments", projectId, projectSource: source, count: deployments.length, deployments },
|
|
111
|
+
(_d, { stdout }) => {
|
|
112
|
+
stdout.write(colors.dim(`Project: ${projectId}\n`));
|
|
113
|
+
if (deployments.length === 0) {
|
|
114
|
+
stdout.write(colors.dim("(no deployments — create one with `synapse deployment create`)\n"));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
ctx.out.table(
|
|
118
|
+
deployments.map((d) => ({
|
|
119
|
+
name: colors.bold(d.name),
|
|
120
|
+
type: typeBadge(d.deploymentType || d.type),
|
|
121
|
+
status: colors.statusBadge(d.status || ""),
|
|
122
|
+
url: d.deploymentUrl || d.url || colors.dim("(no url)"),
|
|
123
|
+
})),
|
|
124
|
+
[
|
|
125
|
+
{ key: "name", header: "NAME" },
|
|
126
|
+
{ key: "type", header: "TYPE" },
|
|
127
|
+
{ key: "status", header: "STATUS" },
|
|
128
|
+
{ key: "url", header: "URL" },
|
|
129
|
+
],
|
|
130
|
+
);
|
|
131
|
+
},
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function typeBadge(t) {
|
|
136
|
+
switch (t) {
|
|
137
|
+
case "prod":
|
|
138
|
+
return colors.yellow((t || "").toUpperCase());
|
|
139
|
+
case "dev":
|
|
140
|
+
return colors.cyan ? colors.cyan(t.toUpperCase()) : t.toUpperCase();
|
|
141
|
+
case "preview":
|
|
142
|
+
return colors.dim((t || "").toUpperCase());
|
|
143
|
+
default:
|
|
144
|
+
return t ? colors.dim(t) : "";
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
module.exports = {
|
|
149
|
+
name: "list",
|
|
150
|
+
summary: "List teams, projects, or deployments visible to your session.",
|
|
151
|
+
usage: "synapse list <deployments|projects|teams> [--project=<id>] [--team=<slug>] [--json]",
|
|
152
|
+
description: `Catalogs the resource of the chosen kind. All subcommands support --json for scripting.
|
|
153
|
+
|
|
154
|
+
Resource resolution:
|
|
155
|
+
list teams always lists every team you're a member of
|
|
156
|
+
list projects uses the linked team if no --team flag
|
|
157
|
+
list deployments uses the linked project if no --project flag
|
|
158
|
+
|
|
159
|
+
Examples:
|
|
160
|
+
synapse list teams
|
|
161
|
+
synapse list projects --team=amageia
|
|
162
|
+
synapse list deployments --project=00000000-0000-0000-0000-000000000000
|
|
163
|
+
synapse list deployments --json | jq '.deployments[] | .name'`,
|
|
164
|
+
|
|
165
|
+
// Exports for tests.
|
|
166
|
+
KINDS,
|
|
167
|
+
listTeams,
|
|
168
|
+
listProjects,
|
|
169
|
+
listDeployments,
|
|
170
|
+
|
|
171
|
+
async run(args, ctx) {
|
|
172
|
+
const kind = args[0];
|
|
173
|
+
if (!kind) {
|
|
174
|
+
throw new Error(`Usage: synapse list <${KINDS.join("|")}>`);
|
|
175
|
+
}
|
|
176
|
+
if (!KINDS.includes(kind)) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`Unknown list kind: ${kind}. Try: ${KINDS.join(" | ")}.`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
const rest = args.slice(1);
|
|
182
|
+
if (kind === "teams") return listTeams(ctx);
|
|
183
|
+
if (kind === "projects") return listProjects(ctx, rest);
|
|
184
|
+
return listDeployments(ctx, rest);
|
|
185
|
+
},
|
|
186
|
+
};
|