@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,129 @@
|
|
|
1
|
+
// `synapse env set NAME=value [NAME2=value2 ...] [--for=dev,prod] [--project=<id>] [--json]`
|
|
2
|
+
//
|
|
3
|
+
// Batch-sets project-default env vars. Multiple positionals = single
|
|
4
|
+
// transactional update on the backend (the handler wraps the whole
|
|
5
|
+
// `changes` array in a BEGIN/COMMIT). If any value is rejected the
|
|
6
|
+
// whole batch rolls back.
|
|
7
|
+
//
|
|
8
|
+
// Value parsing: split on the FIRST `=`. So `FOO=a=b` sets FOO to "a=b".
|
|
9
|
+
// Names must match /^[A-Z_][A-Z0-9_]*$/ — same shape the Convex backend
|
|
10
|
+
// accepts (uppercase letters, digits, underscore; leading non-digit).
|
|
11
|
+
|
|
12
|
+
const colors = require("../colors");
|
|
13
|
+
const { extractFlags, resolveProject } = require("./_resource");
|
|
14
|
+
|
|
15
|
+
const NAME_RE = /^[A-Z_][A-Z0-9_]*$/;
|
|
16
|
+
const VALID_FORS = new Set(["dev", "prod", "preview"]);
|
|
17
|
+
|
|
18
|
+
// Splits `NAME=value` on the FIRST `=`. Returns { name, value } or
|
|
19
|
+
// throws when the shape is wrong. Empty value is OK ("FOO=" sets to "").
|
|
20
|
+
function parsePair(arg) {
|
|
21
|
+
const eq = arg.indexOf("=");
|
|
22
|
+
if (eq <= 0) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`Invalid env assignment: ${JSON.stringify(arg)}. Expected NAME=value.`,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
const name = arg.slice(0, eq);
|
|
28
|
+
const value = arg.slice(eq + 1);
|
|
29
|
+
if (!NAME_RE.test(name)) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Invalid env name: ${JSON.stringify(name)}. Names must match [A-Z_][A-Z0-9_]*.`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return { name, value };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Comma-split + trim + dedupe a `--for=dev,prod` argument. Empty
|
|
38
|
+
// returns null (= "use default"). Errors on unknown types.
|
|
39
|
+
function parseFor(raw) {
|
|
40
|
+
if (raw === undefined || raw === null || raw === true) return null;
|
|
41
|
+
const trimmed = String(raw).trim();
|
|
42
|
+
if (trimmed === "") return null;
|
|
43
|
+
const parts = [
|
|
44
|
+
...new Set(trimmed.split(",").map((s) => s.trim().toLowerCase())),
|
|
45
|
+
].filter(Boolean);
|
|
46
|
+
for (const p of parts) {
|
|
47
|
+
if (!VALID_FORS.has(p)) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Invalid --for entry: ${p}. Must be one of: ${[...VALID_FORS].join(", ")}.`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return parts;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = {
|
|
57
|
+
name: "env set",
|
|
58
|
+
summary: "Set one or more project-default environment variables.",
|
|
59
|
+
usage:
|
|
60
|
+
"synapse env set NAME=value [NAME2=value2 ...] [--for=dev,prod] [--project=<id>] [--json]",
|
|
61
|
+
description: `Calls POST /v1/projects/{id}/update_default_environment_variables with op=set. Multiple pairs are applied in a single transaction. Names follow the [A-Z_][A-Z0-9_]* convention; values are byte-preserved (whitespace, quotes, '=' all OK).
|
|
62
|
+
|
|
63
|
+
Flags:
|
|
64
|
+
--for=dev,prod Limit the var to specific deploymentTypes
|
|
65
|
+
(comma-separated). Omitted: backend defaults
|
|
66
|
+
(typically: all types).
|
|
67
|
+
--project=<id> Override the linked project.
|
|
68
|
+
--json Machine-readable output.
|
|
69
|
+
|
|
70
|
+
Examples:
|
|
71
|
+
synapse env set OPENAI_API_KEY=sk-abc...
|
|
72
|
+
synapse env set FOO=bar BAZ=qux --for=prod
|
|
73
|
+
synapse env set REDIS_URL='rediss://user:pass@host:6379/0'`,
|
|
74
|
+
|
|
75
|
+
// Re-exports for tests.
|
|
76
|
+
parsePair,
|
|
77
|
+
parseFor,
|
|
78
|
+
NAME_RE,
|
|
79
|
+
|
|
80
|
+
async run(args, ctx) {
|
|
81
|
+
const { flags, rest } = extractFlags(args, {
|
|
82
|
+
string: ["for", "project"],
|
|
83
|
+
boolean: ["json"],
|
|
84
|
+
});
|
|
85
|
+
if (rest.length === 0) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
"Usage: synapse env set NAME=value [NAME2=value2 ...] [--for=...]",
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
const forList = parseFor(flags.for);
|
|
91
|
+
const pairs = rest.map(parsePair);
|
|
92
|
+
|
|
93
|
+
const resolveArgs = flags.project ? [`--project=${flags.project}`] : [];
|
|
94
|
+
const { projectId, source } = resolveProject(ctx, resolveArgs);
|
|
95
|
+
|
|
96
|
+
const changes = pairs.map(({ name, value }) => ({
|
|
97
|
+
op: "set",
|
|
98
|
+
name,
|
|
99
|
+
value,
|
|
100
|
+
...(forList ? { deploymentTypes: forList } : {}),
|
|
101
|
+
}));
|
|
102
|
+
|
|
103
|
+
if (!ctx.out.json) {
|
|
104
|
+
ctx.out.info(
|
|
105
|
+
`Setting ${pairs.length} env var${pairs.length > 1 ? "s" : ""} on project ${projectId}${forList ? ` (for: ${forList.join(",")})` : ""}…`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
await ctx.api.updateProjectEnvVars(projectId, changes);
|
|
109
|
+
|
|
110
|
+
ctx.out.result(
|
|
111
|
+
{
|
|
112
|
+
projectId,
|
|
113
|
+
projectSource: source,
|
|
114
|
+
applied: changes.length,
|
|
115
|
+
changes: changes.map((c) => ({ name: c.name, deploymentTypes: c.deploymentTypes ?? null })),
|
|
116
|
+
},
|
|
117
|
+
(_d, { stdout }) => {
|
|
118
|
+
for (const { name } of pairs) {
|
|
119
|
+
stdout.write(`${colors.green("+")} ${colors.bold(name)}\n`);
|
|
120
|
+
}
|
|
121
|
+
stdout.write(
|
|
122
|
+
colors.dim(
|
|
123
|
+
"These defaults are injected into NEW deployments. To apply to existing deployments, run `synapse convex env set` per deployment, or re-create them.\n",
|
|
124
|
+
),
|
|
125
|
+
);
|
|
126
|
+
},
|
|
127
|
+
);
|
|
128
|
+
},
|
|
129
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// `synapse env unset NAME [NAME2 ...] [--project=<id>] [--json]`
|
|
2
|
+
//
|
|
3
|
+
// Batch-deletes project-default env vars. Sends op=delete for each
|
|
4
|
+
// name in a single transactional update. Unknown names are not an
|
|
5
|
+
// error server-side (the DELETE is idempotent), but we surface a
|
|
6
|
+
// warning when ALL passed names were unknown — usually a typo.
|
|
7
|
+
|
|
8
|
+
const colors = require("../colors");
|
|
9
|
+
const { extractFlags, resolveProject } = require("./_resource");
|
|
10
|
+
|
|
11
|
+
const NAME_RE = /^[A-Z_][A-Z0-9_]*$/;
|
|
12
|
+
|
|
13
|
+
module.exports = {
|
|
14
|
+
name: "env unset",
|
|
15
|
+
summary: "Delete one or more project-default environment variables.",
|
|
16
|
+
usage: "synapse env unset NAME [NAME2 ...] [--project=<id>] [--json]",
|
|
17
|
+
description: `Calls POST /v1/projects/{id}/update_default_environment_variables with op=delete. Idempotent — deleting a name that doesn't exist is silent.
|
|
18
|
+
|
|
19
|
+
Flags:
|
|
20
|
+
--project=<id> Override the linked project.
|
|
21
|
+
--json Machine-readable output.
|
|
22
|
+
|
|
23
|
+
Examples:
|
|
24
|
+
synapse env unset OLD_API_KEY
|
|
25
|
+
synapse env unset FOO BAR BAZ`,
|
|
26
|
+
|
|
27
|
+
async run(args, ctx) {
|
|
28
|
+
const { flags, rest } = extractFlags(args, {
|
|
29
|
+
string: ["project"],
|
|
30
|
+
boolean: ["json"],
|
|
31
|
+
});
|
|
32
|
+
if (rest.length === 0) {
|
|
33
|
+
throw new Error("Usage: synapse env unset NAME [NAME2 ...]");
|
|
34
|
+
}
|
|
35
|
+
for (const name of rest) {
|
|
36
|
+
if (!NAME_RE.test(name)) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`Invalid env name: ${JSON.stringify(name)}. Names must match [A-Z_][A-Z0-9_]*.`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const resolveArgs = flags.project ? [`--project=${flags.project}`] : [];
|
|
44
|
+
const { projectId, source } = resolveProject(ctx, resolveArgs);
|
|
45
|
+
|
|
46
|
+
// We could pre-fetch the existing var list to warn about unknown
|
|
47
|
+
// names. Skipping: an extra GET on every unset doubles latency for
|
|
48
|
+
// the common case where the operator typed the right name. The
|
|
49
|
+
// backend's op=delete is idempotent.
|
|
50
|
+
const changes = rest.map((name) => ({ op: "delete", name }));
|
|
51
|
+
if (!ctx.out.json) {
|
|
52
|
+
ctx.out.info(
|
|
53
|
+
`Unsetting ${rest.length} env var${rest.length > 1 ? "s" : ""} on project ${projectId}…`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
await ctx.api.updateProjectEnvVars(projectId, changes);
|
|
57
|
+
|
|
58
|
+
ctx.out.result(
|
|
59
|
+
{
|
|
60
|
+
projectId,
|
|
61
|
+
projectSource: source,
|
|
62
|
+
deleted: rest.length,
|
|
63
|
+
names: rest,
|
|
64
|
+
},
|
|
65
|
+
(_d, { stdout }) => {
|
|
66
|
+
for (const name of rest) {
|
|
67
|
+
stdout.write(`${colors.red("-")} ${colors.bold(name)}\n`);
|
|
68
|
+
}
|
|
69
|
+
stdout.write(
|
|
70
|
+
colors.dim(
|
|
71
|
+
"Existing deployments keep their cached value until they're recreated.\n",
|
|
72
|
+
),
|
|
73
|
+
);
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
},
|
|
77
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// `synapse https doctor [domain]` — read-only sanity checks that
|
|
2
|
+
// tell the operator WHAT to fix without changing anything. This
|
|
3
|
+
// mirrors `synapse doctor` but is scoped to the HTTPS-dev surface.
|
|
4
|
+
//
|
|
5
|
+
// Useful when an operator runs the wizard and the dev server still
|
|
6
|
+
// doesn't work — `doctor` re-scans + flags every drift from the
|
|
7
|
+
// expected state.
|
|
8
|
+
|
|
9
|
+
const colors = require("../colors");
|
|
10
|
+
const { extractFlags } = require("./_resource");
|
|
11
|
+
const detectMod = require("../https/detect");
|
|
12
|
+
const plannerMod = require("../https/planner");
|
|
13
|
+
|
|
14
|
+
module.exports = {
|
|
15
|
+
name: "https doctor",
|
|
16
|
+
summary: "Diagnose local-HTTPS setup without changing anything.",
|
|
17
|
+
usage: "synapse https doctor [domain] [--json]",
|
|
18
|
+
description: `Runs the same detection as \`https setup\` but in read-only mode. Prints the plan that WOULD run, plus any warnings (legacy certs in cwd, NSS missing, etc).`,
|
|
19
|
+
|
|
20
|
+
async run(args, ctx) {
|
|
21
|
+
const { flags, rest } = extractFlags(args, {
|
|
22
|
+
string: [],
|
|
23
|
+
boolean: ["json"],
|
|
24
|
+
});
|
|
25
|
+
const domain = rest[0];
|
|
26
|
+
if (rest.length > 1) {
|
|
27
|
+
throw new Error(`Unexpected positional: ${rest[1]}.`);
|
|
28
|
+
}
|
|
29
|
+
if (!domain) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
"Usage: synapse https doctor <domain>\n\nExample: synapse https doctor dev.myproject.com",
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
const detection = await detectMod.scan({ domain, cwd: ctx.cwd });
|
|
35
|
+
const steps = plannerMod.plan(detection, {});
|
|
36
|
+
const blocker = steps.find((s) => s.kind === "blocker");
|
|
37
|
+
const warnings = steps.filter((s) => s.kind === "warn");
|
|
38
|
+
const willExec = steps.filter((s) => s.kind === "exec");
|
|
39
|
+
const willSkip = steps.filter((s) => s.kind === "skip");
|
|
40
|
+
const summary = {
|
|
41
|
+
domain,
|
|
42
|
+
platform: detection.platform.id,
|
|
43
|
+
blocker: blocker ? blocker.blocker || blocker.reason : null,
|
|
44
|
+
warnings: warnings.map((w) => ({ id: w.id, reason: w.reason, hint: w.skipReason })),
|
|
45
|
+
wouldExec: willExec.map((s) => ({ id: s.id, title: s.title, reason: s.reason })),
|
|
46
|
+
skipped: willSkip.map((s) => ({ id: s.id, title: s.title, reason: s.reason })),
|
|
47
|
+
};
|
|
48
|
+
ctx.out.result(summary, (s, { stdout }) => {
|
|
49
|
+
stdout.write(`${colors.bold("HTTPS doctor")} ${colors.dim(`· ${s.domain} · ${s.platform}`)}\n\n`);
|
|
50
|
+
if (s.blocker) {
|
|
51
|
+
stdout.write(colors.red(`Blocker: ${s.blocker}\n`));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (s.wouldExec.length === 0) {
|
|
55
|
+
stdout.write(colors.green("Healthy. Nothing to do.\n"));
|
|
56
|
+
} else {
|
|
57
|
+
stdout.write(colors.yellow(`${s.wouldExec.length} step${s.wouldExec.length > 1 ? "s" : ""} would run:\n`));
|
|
58
|
+
for (const x of s.wouldExec) {
|
|
59
|
+
stdout.write(` ${colors.cyan("●")} ${x.title}\n`);
|
|
60
|
+
if (x.reason) stdout.write(colors.dim(` ${x.reason}\n`));
|
|
61
|
+
}
|
|
62
|
+
stdout.write(colors.dim("\nRun `synapse https setup <domain>` to apply.\n"));
|
|
63
|
+
}
|
|
64
|
+
if (s.warnings.length > 0) {
|
|
65
|
+
stdout.write(`\n${colors.yellow("Warnings:")}\n`);
|
|
66
|
+
for (const w of s.warnings) {
|
|
67
|
+
stdout.write(` ${colors.yellow("!")} ${w.reason}\n`);
|
|
68
|
+
if (w.hint) stdout.write(colors.dim(` ${w.hint}\n`));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
},
|
|
73
|
+
};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// `synapse https migrate [--cwd] [--all] [--yes] [--dry-run] [--json]`
|
|
2
|
+
//
|
|
3
|
+
// Migrates legacy cert layouts to the canonical ~/.config/dev-certs/
|
|
4
|
+
// store. Two modes:
|
|
5
|
+
//
|
|
6
|
+
// 1. cwd mode (default): scans the current directory for legacy
|
|
7
|
+
// `dev.*.pem` + `-key.pem` pairs and migrates them.
|
|
8
|
+
// 2. --all: scans EVERY known project root (operator-supplied via
|
|
9
|
+
// --root flag, default: ~/Documents and the cwd). This is more
|
|
10
|
+
// ambitious and gated behind explicit opt-in.
|
|
11
|
+
//
|
|
12
|
+
// Migration per cert pair:
|
|
13
|
+
// - Move the .pem files into ~/.config/dev-certs/<domain>/
|
|
14
|
+
// - Set mode 0600 on the key
|
|
15
|
+
// - Rewrite package.json dev:https to point at the new paths
|
|
16
|
+
// - Leave the original files in place ONLY if --keep-old; default
|
|
17
|
+
// is to delete them once they've been moved successfully.
|
|
18
|
+
|
|
19
|
+
const fs = require("node:fs");
|
|
20
|
+
const path = require("node:path");
|
|
21
|
+
const colors = require("../colors");
|
|
22
|
+
const { extractFlags } = require("./_resource");
|
|
23
|
+
const { confirm } = require("../prompts");
|
|
24
|
+
const detectMod = require("../https/detect");
|
|
25
|
+
const nextjsMod = require("../https/nextjs");
|
|
26
|
+
|
|
27
|
+
module.exports = {
|
|
28
|
+
name: "https migrate",
|
|
29
|
+
summary: "Move legacy cert pairs from project roots into ~/.config/dev-certs/.",
|
|
30
|
+
usage: "synapse https migrate [--cwd | --root=<path>] [--keep-old] [--dry-run] [--yes] [--json]",
|
|
31
|
+
description: `Some projects have their .pem files in the project root (left over from the manual setup era). This command moves them into the canonical ~/.config/dev-certs/<domain>/ store, then rewrites the project's package.json dev:https script to reference the new paths.
|
|
32
|
+
|
|
33
|
+
Flags:
|
|
34
|
+
--cwd only the current directory (default)
|
|
35
|
+
--root=<path> a specific directory to scan
|
|
36
|
+
--keep-old leave the original .pem files in place after moving
|
|
37
|
+
--dry-run show what would happen, don't change anything
|
|
38
|
+
--yes skip the per-pair confirmation
|
|
39
|
+
--json machine-readable output
|
|
40
|
+
|
|
41
|
+
The cwd is scanned non-recursively. Subdirectories are ignored —
|
|
42
|
+
if you want to migrate every project on disk, run this command in
|
|
43
|
+
each project root (or write a small loop).`,
|
|
44
|
+
|
|
45
|
+
async run(args, ctx) {
|
|
46
|
+
const { flags, rest } = extractFlags(args, {
|
|
47
|
+
string: ["root"],
|
|
48
|
+
boolean: ["cwd", "keep-old", "dry-run", "yes", "json"],
|
|
49
|
+
});
|
|
50
|
+
if (rest.length > 0) {
|
|
51
|
+
throw new Error(`Unexpected positional: ${rest[0]}.`);
|
|
52
|
+
}
|
|
53
|
+
const root = typeof flags.root === "string" ? flags.root : ctx.cwd;
|
|
54
|
+
const dryRun = flags["dry-run"] === true;
|
|
55
|
+
const keepOld = flags["keep-old"] === true;
|
|
56
|
+
const yes = flags.yes === true;
|
|
57
|
+
const legacy = detectMod.detectLegacyCertsInCwd(root);
|
|
58
|
+
|
|
59
|
+
if (legacy.length === 0) {
|
|
60
|
+
ctx.out.result(
|
|
61
|
+
{ root, migrated: [], skipped: [], count: 0 },
|
|
62
|
+
() =>
|
|
63
|
+
ctx.out.stdout.write(
|
|
64
|
+
colors.dim(`No legacy cert pairs found in ${root}.\n`),
|
|
65
|
+
),
|
|
66
|
+
);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!ctx.out.json) {
|
|
71
|
+
ctx.out.stdout.write(
|
|
72
|
+
`${colors.bold(`Found ${legacy.length} legacy cert pair${legacy.length > 1 ? "s" : ""} in ${root}:`)}\n`,
|
|
73
|
+
);
|
|
74
|
+
for (const p of legacy) {
|
|
75
|
+
ctx.out.stdout.write(` · ${colors.bold(p.domain)}\n`);
|
|
76
|
+
ctx.out.stdout.write(colors.dim(` ${p.cert}\n`));
|
|
77
|
+
ctx.out.stdout.write(colors.dim(` ${p.key}\n`));
|
|
78
|
+
}
|
|
79
|
+
ctx.out.stdout.write("\n");
|
|
80
|
+
}
|
|
81
|
+
if (dryRun) {
|
|
82
|
+
ctx.out.info("(dry-run — no changes applied)");
|
|
83
|
+
ctx.out.result({ root, found: legacy, dryRun: true }, () => {});
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (!yes && !ctx.out.json) {
|
|
87
|
+
const ok = await confirm(`Migrate ${legacy.length} pair(s)? [Y/n] `, { defaultAnswer: true });
|
|
88
|
+
if (!ok) {
|
|
89
|
+
ctx.out.info("Aborted.");
|
|
90
|
+
process.exitCode = 1;
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (!yes && ctx.out.json) {
|
|
95
|
+
throw new Error("Refusing to migrate in --json mode without --yes.");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const migrated = [];
|
|
99
|
+
const skipped = [];
|
|
100
|
+
const failed = [];
|
|
101
|
+
for (const p of legacy) {
|
|
102
|
+
try {
|
|
103
|
+
const target = detectMod.certFilesFor(p.domain);
|
|
104
|
+
// Skip pairs whose canonical location is already populated
|
|
105
|
+
// unless --force (which we don't expose here — re-running
|
|
106
|
+
// setup --force is the right way to regenerate).
|
|
107
|
+
if (fs.existsSync(target.cert) && fs.existsSync(target.key)) {
|
|
108
|
+
skipped.push({ domain: p.domain, reason: "target already exists in cert store" });
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
fs.mkdirSync(target.dir, { recursive: true, mode: 0o700 });
|
|
112
|
+
fs.copyFileSync(p.cert, target.cert);
|
|
113
|
+
fs.copyFileSync(p.key, target.key);
|
|
114
|
+
try {
|
|
115
|
+
fs.chmodSync(target.key, 0o600);
|
|
116
|
+
} catch {
|
|
117
|
+
// ignore
|
|
118
|
+
}
|
|
119
|
+
if (!keepOld) {
|
|
120
|
+
fs.rmSync(p.cert, { force: true });
|
|
121
|
+
fs.rmSync(p.key, { force: true });
|
|
122
|
+
}
|
|
123
|
+
// Rewrite package.json dev:https if present + still references
|
|
124
|
+
// the project-root form.
|
|
125
|
+
const pkgPath = path.join(root, "package.json");
|
|
126
|
+
if (fs.existsSync(pkgPath)) {
|
|
127
|
+
const info = nextjsMod.readPackageJson(pkgPath);
|
|
128
|
+
if (info.parsed && info.parsed.scripts && info.parsed.scripts["dev:https"]) {
|
|
129
|
+
const current = info.parsed.scripts["dev:https"];
|
|
130
|
+
// Replace any reference to the project-root pem paths
|
|
131
|
+
// with the canonical ones.
|
|
132
|
+
const newCommand = current
|
|
133
|
+
.replace(p.cert.replace(/\\/g, "/"), target.cert.replace(/\\/g, "/"))
|
|
134
|
+
.replace(p.key.replace(/\\/g, "/"), target.key.replace(/\\/g, "/"))
|
|
135
|
+
.replace(`./${path.basename(p.cert)}`, target.cert.replace(/\\/g, "/"))
|
|
136
|
+
.replace(`./${path.basename(p.key)}`, target.key.replace(/\\/g, "/"));
|
|
137
|
+
if (newCommand !== current) {
|
|
138
|
+
nextjsMod.setDevHttpsScript(pkgPath, newCommand);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
migrated.push({
|
|
143
|
+
domain: p.domain,
|
|
144
|
+
from: { cert: p.cert, key: p.key },
|
|
145
|
+
to: { cert: target.cert, key: target.key },
|
|
146
|
+
});
|
|
147
|
+
} catch (err) {
|
|
148
|
+
failed.push({ domain: p.domain, error: err.message });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
ctx.out.result(
|
|
153
|
+
{ root, migrated, skipped, failed, count: migrated.length },
|
|
154
|
+
(_d, { stdout }) => {
|
|
155
|
+
for (const m of migrated) {
|
|
156
|
+
stdout.write(`${colors.green("✓")} ${colors.bold(m.domain)} → ${colors.dim(m.to.cert)}\n`);
|
|
157
|
+
}
|
|
158
|
+
for (const s of skipped) {
|
|
159
|
+
stdout.write(`${colors.dim("○")} ${colors.bold(s.domain)} skipped: ${s.reason}\n`);
|
|
160
|
+
}
|
|
161
|
+
for (const f of failed) {
|
|
162
|
+
stdout.write(`${colors.red("✗")} ${colors.bold(f.domain)}: ${f.error}\n`);
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
);
|
|
166
|
+
if (failed.length > 0) process.exitCode = 1;
|
|
167
|
+
},
|
|
168
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// `synapse https remove <domain> [--keep-certs] [--keep-script]
|
|
2
|
+
// [--keep-hosts] [--yes] [--json]`
|
|
3
|
+
//
|
|
4
|
+
// Symmetric undo to `https setup`. Removes:
|
|
5
|
+
// - The cert pair at ~/.config/dev-certs/<domain>/
|
|
6
|
+
// - The hosts file entry inside the managed block
|
|
7
|
+
// - The dev:https script from package.json (if it matches the
|
|
8
|
+
// canonical form — never deletes operator-customised scripts)
|
|
9
|
+
|
|
10
|
+
const colors = require("../colors");
|
|
11
|
+
const { extractFlags } = require("./_resource");
|
|
12
|
+
const { confirm } = require("../prompts");
|
|
13
|
+
const detectMod = require("../https/detect");
|
|
14
|
+
const plannerMod = require("../https/planner");
|
|
15
|
+
const executorMod = require("../https/executor");
|
|
16
|
+
|
|
17
|
+
module.exports = {
|
|
18
|
+
name: "https remove",
|
|
19
|
+
summary: "Undo `synapse https setup` for a domain (cert + hosts + script).",
|
|
20
|
+
usage:
|
|
21
|
+
"synapse https remove <domain> [--keep-certs] [--keep-script] [--keep-hosts] [--yes] [--json]",
|
|
22
|
+
description: `Reverses what \`synapse https setup\` did. Idempotent — running on a domain that was never set up is a no-op.
|
|
23
|
+
|
|
24
|
+
Flags:
|
|
25
|
+
--keep-certs don't delete the cert pair from ~/.config/dev-certs/
|
|
26
|
+
--keep-script don't touch package.json
|
|
27
|
+
--keep-hosts don't touch the hosts file
|
|
28
|
+
--yes skip the y/N confirmation
|
|
29
|
+
--json machine-readable output
|
|
30
|
+
|
|
31
|
+
Examples:
|
|
32
|
+
synapse https remove dev.oldproject.com
|
|
33
|
+
synapse https remove dev.test.com --keep-certs # keep the cert, just untangle hosts + script`,
|
|
34
|
+
|
|
35
|
+
async run(args, ctx) {
|
|
36
|
+
const { flags, rest } = extractFlags(args, {
|
|
37
|
+
string: [],
|
|
38
|
+
boolean: ["keep-certs", "keep-script", "keep-hosts", "yes", "json"],
|
|
39
|
+
});
|
|
40
|
+
const domain = rest[0];
|
|
41
|
+
if (!domain) {
|
|
42
|
+
throw new Error("Usage: synapse https remove <domain>");
|
|
43
|
+
}
|
|
44
|
+
if (rest.length > 1) {
|
|
45
|
+
throw new Error(`Unexpected positional: ${rest[1]}.`);
|
|
46
|
+
}
|
|
47
|
+
const yes = flags.yes === true;
|
|
48
|
+
const detection = await detectMod.scan({ domain, cwd: ctx.cwd });
|
|
49
|
+
const steps = plannerMod.planRemove(detection, {
|
|
50
|
+
keepCerts: flags["keep-certs"] === true,
|
|
51
|
+
keepScript: flags["keep-script"] === true,
|
|
52
|
+
keepHosts: flags["keep-hosts"] === true,
|
|
53
|
+
});
|
|
54
|
+
if (!plannerMod.planIsExecutable(steps)) {
|
|
55
|
+
const blocker = steps.find((s) => s.kind === "blocker");
|
|
56
|
+
throw new Error(blocker?.blocker || "Plan is blocked.");
|
|
57
|
+
}
|
|
58
|
+
const willChange = steps.some((s) => s.kind === "exec");
|
|
59
|
+
if (!willChange) {
|
|
60
|
+
ctx.out.result(
|
|
61
|
+
{ domain, executedAny: false, results: [] },
|
|
62
|
+
() => ctx.out.stdout.write(colors.dim("Nothing to remove.\n")),
|
|
63
|
+
);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!ctx.out.json) {
|
|
68
|
+
ctx.out.stdout.write(`${colors.bold("Will remove:")}\n`);
|
|
69
|
+
for (const s of steps) {
|
|
70
|
+
if (s.kind === "exec") {
|
|
71
|
+
ctx.out.stdout.write(` ${colors.red("✗")} ${s.title}\n`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
ctx.out.stdout.write("\n");
|
|
75
|
+
}
|
|
76
|
+
if (!yes && !ctx.out.json) {
|
|
77
|
+
const ok = await confirm(
|
|
78
|
+
`Remove HTTPS setup for ${colors.bold(domain)}? [y/N] `,
|
|
79
|
+
{ defaultAnswer: false },
|
|
80
|
+
);
|
|
81
|
+
if (!ok) {
|
|
82
|
+
ctx.out.info("Aborted.");
|
|
83
|
+
process.exitCode = 1;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (!yes && ctx.out.json) {
|
|
88
|
+
throw new Error("Refusing to remove in --json mode without --yes.");
|
|
89
|
+
}
|
|
90
|
+
const { results, failedAny } = await executorMod.execute(steps, { out: ctx.out });
|
|
91
|
+
ctx.out.result(
|
|
92
|
+
{ domain, results, failedAny },
|
|
93
|
+
(_d, { stdout }) => {
|
|
94
|
+
for (const r of results) {
|
|
95
|
+
if (r.kind === "ok") {
|
|
96
|
+
stdout.write(`${colors.green("✓")} ${r.title}\n`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
);
|
|
101
|
+
if (failedAny) process.exitCode = 1;
|
|
102
|
+
},
|
|
103
|
+
};
|