@iann29/synapse 1.8.6 → 1.8.7
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 +1 -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/list.js +186 -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,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
|
+
};
|
package/lib/prompts.js
CHANGED
|
@@ -187,6 +187,55 @@ async function choose(label, choices, {
|
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
// typedConfirm prompts the user to literally re-type a string (typically
|
|
191
|
+
// the name of a resource about to be destroyed) before proceeding. Match
|
|
192
|
+
// is case-sensitive and exact — `--yes` does NOT bypass this prompt,
|
|
193
|
+
// only an explicit pre-supplied `typed` answer does (for non-interactive
|
|
194
|
+
// CI where the operator already scripted the confirmation).
|
|
195
|
+
//
|
|
196
|
+
// Why we need this in addition to `confirm`: y/n prompts get muscle-
|
|
197
|
+
// memory-yes'd. Typing the literal name of a prod deployment makes the
|
|
198
|
+
// operator look at what they're deleting one more time. Mirrors the
|
|
199
|
+
// dashboard's `delete-deployment` flow shipped in v1.7.2.
|
|
200
|
+
async function typedConfirm(label, expected, {
|
|
201
|
+
input = process.stdin,
|
|
202
|
+
output = process.stderr,
|
|
203
|
+
typed = null,
|
|
204
|
+
maxAttempts = 3,
|
|
205
|
+
} = {}) {
|
|
206
|
+
if (typeof expected !== "string" || expected === "") {
|
|
207
|
+
throw new Error("typedConfirm requires a non-empty `expected` string");
|
|
208
|
+
}
|
|
209
|
+
// Non-interactive shortcut: caller pre-supplied the typed string via
|
|
210
|
+
// a flag (`--confirm=<name>`). We still require exact match; passing
|
|
211
|
+
// the wrong value should NOT proceed even if the caller meant well.
|
|
212
|
+
if (typed !== null) {
|
|
213
|
+
return typed === expected;
|
|
214
|
+
}
|
|
215
|
+
if (!input.isTTY) {
|
|
216
|
+
throw new Error(
|
|
217
|
+
`Refusing to ${label} without a TTY. Re-run with --confirm=${expected} to confirm non-interactively.`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
const rl = readline.createInterface({ input, output });
|
|
221
|
+
try {
|
|
222
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
223
|
+
const raw = await new Promise((resolve) =>
|
|
224
|
+
rl.question(`Type ${expected} to confirm ${label}: `, resolve),
|
|
225
|
+
);
|
|
226
|
+
const answer = raw.trim();
|
|
227
|
+
if (answer === expected) return true;
|
|
228
|
+
if (answer === "" || answer.toLowerCase() === "n" || answer.toLowerCase() === "no") {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
output.write(`Doesn't match. Type the exact name "${expected}" or press Enter to cancel.\n`);
|
|
232
|
+
}
|
|
233
|
+
return false;
|
|
234
|
+
} finally {
|
|
235
|
+
rl.close();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
190
239
|
module.exports = {
|
|
191
240
|
BACK,
|
|
192
241
|
ask,
|
|
@@ -195,4 +244,5 @@ module.exports = {
|
|
|
195
244
|
choose,
|
|
196
245
|
confirm,
|
|
197
246
|
parseCredentialsInput,
|
|
247
|
+
typedConfirm,
|
|
198
248
|
};
|