@getjack/jack 0.1.2 → 0.1.3
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/package.json +54 -47
- package/src/commands/agents.ts +145 -10
- package/src/commands/down.ts +110 -102
- package/src/commands/feedback.ts +189 -0
- package/src/commands/init.ts +8 -12
- package/src/commands/login.ts +88 -0
- package/src/commands/logout.ts +14 -0
- package/src/commands/logs.ts +21 -0
- package/src/commands/mcp.ts +134 -7
- package/src/commands/new.ts +43 -17
- package/src/commands/open.ts +13 -6
- package/src/commands/projects.ts +269 -143
- package/src/commands/secrets.ts +413 -0
- package/src/commands/services.ts +96 -123
- package/src/commands/ship.ts +5 -1
- package/src/commands/whoami.ts +31 -0
- package/src/index.ts +218 -144
- package/src/lib/agent-files.ts +34 -0
- package/src/lib/agents.ts +390 -22
- package/src/lib/asset-hash.ts +50 -0
- package/src/lib/auth/client.ts +115 -0
- package/src/lib/auth/constants.ts +5 -0
- package/src/lib/auth/guard.ts +57 -0
- package/src/lib/auth/index.ts +18 -0
- package/src/lib/auth/store.ts +54 -0
- package/src/lib/binding-validator.ts +136 -0
- package/src/lib/build-helper.ts +211 -0
- package/src/lib/cloudflare-api.ts +24 -0
- package/src/lib/config.ts +5 -6
- package/src/lib/control-plane.ts +295 -0
- package/src/lib/debug.ts +3 -1
- package/src/lib/deploy-mode.ts +93 -0
- package/src/lib/deploy-upload.ts +92 -0
- package/src/lib/errors.ts +2 -0
- package/src/lib/github.ts +31 -1
- package/src/lib/hooks.ts +4 -12
- package/src/lib/intent.ts +88 -0
- package/src/lib/jsonc.ts +125 -0
- package/src/lib/local-paths.test.ts +902 -0
- package/src/lib/local-paths.ts +258 -0
- package/src/lib/managed-deploy.ts +175 -0
- package/src/lib/managed-down.ts +159 -0
- package/src/lib/mcp-config.ts +55 -34
- package/src/lib/names.ts +9 -29
- package/src/lib/project-operations.ts +676 -249
- package/src/lib/project-resolver.ts +476 -0
- package/src/lib/registry.ts +76 -37
- package/src/lib/resources.ts +196 -0
- package/src/lib/schema.ts +30 -1
- package/src/lib/storage/file-filter.ts +1 -0
- package/src/lib/storage/index.ts +5 -1
- package/src/lib/telemetry.ts +14 -0
- package/src/lib/tty.ts +15 -0
- package/src/lib/zip-packager.ts +255 -0
- package/src/mcp/resources/index.ts +8 -2
- package/src/mcp/server.ts +32 -4
- package/src/mcp/tools/index.ts +35 -13
- package/src/mcp/types.ts +6 -0
- package/src/mcp/utils.ts +1 -1
- package/src/templates/index.ts +42 -4
- package/src/templates/types.ts +13 -0
- package/templates/CLAUDE.md +166 -0
- package/templates/api/.jack.json +4 -0
- package/templates/api/bun.lock +1 -0
- package/templates/api/wrangler.jsonc +5 -0
- package/templates/hello/.jack.json +28 -0
- package/templates/hello/package.json +10 -0
- package/templates/hello/src/index.ts +11 -0
- package/templates/hello/tsconfig.json +11 -0
- package/templates/hello/wrangler.jsonc +5 -0
- package/templates/miniapp/.jack.json +15 -4
- package/templates/miniapp/bun.lock +135 -40
- package/templates/miniapp/index.html +1 -0
- package/templates/miniapp/package.json +3 -1
- package/templates/miniapp/public/.well-known/farcaster.json +7 -5
- package/templates/miniapp/public/icon.png +0 -0
- package/templates/miniapp/public/og.png +0 -0
- package/templates/miniapp/schema.sql +8 -0
- package/templates/miniapp/src/App.tsx +254 -3
- package/templates/miniapp/src/components/ShareSheet.tsx +147 -0
- package/templates/miniapp/src/hooks/useAI.ts +35 -0
- package/templates/miniapp/src/hooks/useGuestbook.ts +11 -1
- package/templates/miniapp/src/hooks/useShare.ts +76 -0
- package/templates/miniapp/src/index.css +15 -0
- package/templates/miniapp/src/lib/api.ts +2 -1
- package/templates/miniapp/src/worker.ts +515 -1
- package/templates/miniapp/wrangler.jsonc +15 -3
- package/LICENSE +0 -190
- package/README.md +0 -55
- package/src/commands/cloud.ts +0 -230
- package/templates/api/wrangler.toml +0 -3
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* jack secrets - Manage project secrets securely
|
|
3
|
+
*
|
|
4
|
+
* Secrets are stored in Cloudflare and never written to disk.
|
|
5
|
+
* For managed projects: uses jack cloud control plane.
|
|
6
|
+
* For BYO projects: uses wrangler secret commands.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { password as passwordPrompt } from "@inquirer/prompts";
|
|
10
|
+
import { $ } from "bun";
|
|
11
|
+
import { getControlApiUrl } from "../lib/control-plane.ts";
|
|
12
|
+
import { JackError, JackErrorCode } from "../lib/errors.ts";
|
|
13
|
+
import { error, info, output, success, warn } from "../lib/output.ts";
|
|
14
|
+
import { type Project, getProject } from "../lib/registry.ts";
|
|
15
|
+
import { getProjectNameFromDir } from "../lib/storage/index.ts";
|
|
16
|
+
|
|
17
|
+
interface SecretsOptions {
|
|
18
|
+
project?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default async function secrets(
|
|
22
|
+
subcommand?: string,
|
|
23
|
+
args: string[] = [],
|
|
24
|
+
options: SecretsOptions = {},
|
|
25
|
+
): Promise<void> {
|
|
26
|
+
if (!subcommand) {
|
|
27
|
+
return showHelp();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
switch (subcommand) {
|
|
31
|
+
case "set":
|
|
32
|
+
case "add":
|
|
33
|
+
return await setSecret(args, options);
|
|
34
|
+
case "list":
|
|
35
|
+
case "ls":
|
|
36
|
+
return await listSecrets(options);
|
|
37
|
+
case "rm":
|
|
38
|
+
case "remove":
|
|
39
|
+
case "delete":
|
|
40
|
+
return await removeSecret(args, options);
|
|
41
|
+
default:
|
|
42
|
+
error(`Unknown subcommand: ${subcommand}`);
|
|
43
|
+
info("Available: set, list, rm");
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function showHelp(): void {
|
|
49
|
+
console.error("");
|
|
50
|
+
info("jack secrets - Manage project secrets");
|
|
51
|
+
console.error("");
|
|
52
|
+
console.error("Commands:");
|
|
53
|
+
console.error(" set <KEY> Set a secret (interactive prompt)");
|
|
54
|
+
console.error(" set KEY=VALUE Set a secret (warning: visible in shell history)");
|
|
55
|
+
console.error(" list List secret names");
|
|
56
|
+
console.error(" rm <KEY> Remove a secret");
|
|
57
|
+
console.error("");
|
|
58
|
+
console.error("Options:");
|
|
59
|
+
console.error(" --project, -p Project name (auto-detected from cwd)");
|
|
60
|
+
console.error("");
|
|
61
|
+
console.error("Piping from stdin (for CI/CD):");
|
|
62
|
+
console.error(" echo $SECRET | jack secrets set KEY");
|
|
63
|
+
console.error("");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Resolve project and determine if it's managed or BYO
|
|
68
|
+
*/
|
|
69
|
+
async function resolveProjectContext(options: SecretsOptions): Promise<{
|
|
70
|
+
projectName: string;
|
|
71
|
+
project: Project | null;
|
|
72
|
+
isManaged: boolean;
|
|
73
|
+
projectId: string | null;
|
|
74
|
+
}> {
|
|
75
|
+
let projectName: string;
|
|
76
|
+
|
|
77
|
+
if (options.project) {
|
|
78
|
+
projectName = options.project;
|
|
79
|
+
} else {
|
|
80
|
+
try {
|
|
81
|
+
projectName = await getProjectNameFromDir(process.cwd());
|
|
82
|
+
} catch {
|
|
83
|
+
error("Could not determine project");
|
|
84
|
+
info("Run from a project directory, or use --project <name>");
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const project = await getProject(projectName);
|
|
90
|
+
const isManaged = project?.deploy_mode === "managed";
|
|
91
|
+
const projectId = project?.remote?.project_id ?? null;
|
|
92
|
+
|
|
93
|
+
return { projectName, project, isManaged, projectId };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Read a secret value interactively without echoing
|
|
98
|
+
* Uses @inquirer/prompts password for robust handling of typing, pasting, and TTY
|
|
99
|
+
*/
|
|
100
|
+
async function readSecretInteractive(keyName: string): Promise<string> {
|
|
101
|
+
try {
|
|
102
|
+
const value = await passwordPrompt({
|
|
103
|
+
message: `Enter value for ${keyName}`,
|
|
104
|
+
mask: "*",
|
|
105
|
+
});
|
|
106
|
+
return value;
|
|
107
|
+
} catch (err) {
|
|
108
|
+
// Handle Ctrl+C / Escape - inquirer throws ExitPromptError
|
|
109
|
+
if (err instanceof Error && err.name === "ExitPromptError") {
|
|
110
|
+
throw new Error("Cancelled");
|
|
111
|
+
}
|
|
112
|
+
throw err;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Read secret value from stdin (for piping in CI/CD)
|
|
118
|
+
*/
|
|
119
|
+
async function readSecretFromStdin(): Promise<string> {
|
|
120
|
+
const chunks: Buffer[] = [];
|
|
121
|
+
|
|
122
|
+
for await (const chunk of process.stdin) {
|
|
123
|
+
chunks.push(chunk);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return Buffer.concat(chunks).toString("utf-8").trim();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Set a secret value
|
|
131
|
+
*/
|
|
132
|
+
async function setSecret(args: string[], options: SecretsOptions): Promise<void> {
|
|
133
|
+
const arg = args[0];
|
|
134
|
+
|
|
135
|
+
if (!arg) {
|
|
136
|
+
error("Missing secret key");
|
|
137
|
+
info("Usage: jack secrets set <KEY> or jack secrets set KEY=VALUE");
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let keyName: string;
|
|
142
|
+
let value: string | undefined;
|
|
143
|
+
|
|
144
|
+
// Check for KEY=VALUE format
|
|
145
|
+
const equalsIndex = arg.indexOf("=");
|
|
146
|
+
if (equalsIndex > 0) {
|
|
147
|
+
keyName = arg.slice(0, equalsIndex);
|
|
148
|
+
value = arg.slice(equalsIndex + 1);
|
|
149
|
+
|
|
150
|
+
// Warn about shell history exposure
|
|
151
|
+
warn("Value visible in shell history. Use interactive mode for sensitive secrets.");
|
|
152
|
+
} else {
|
|
153
|
+
keyName = arg;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Validate key name
|
|
157
|
+
if (!/^[A-Z_][A-Z0-9_]*$/i.test(keyName)) {
|
|
158
|
+
error("Invalid secret name");
|
|
159
|
+
info(
|
|
160
|
+
"Must start with a letter or underscore, and contain only letters, numbers, and underscores",
|
|
161
|
+
);
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Get value if not provided inline
|
|
166
|
+
if (value === undefined) {
|
|
167
|
+
// Check if stdin is piped
|
|
168
|
+
if (!process.stdin.isTTY) {
|
|
169
|
+
value = await readSecretFromStdin();
|
|
170
|
+
if (!value) {
|
|
171
|
+
error("No value provided via stdin");
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
// Interactive prompt
|
|
176
|
+
try {
|
|
177
|
+
value = await readSecretInteractive(keyName);
|
|
178
|
+
if (!value) {
|
|
179
|
+
error("No value provided");
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
} catch (err) {
|
|
183
|
+
if (err instanceof Error && err.message === "Cancelled") {
|
|
184
|
+
info("Cancelled");
|
|
185
|
+
process.exit(0);
|
|
186
|
+
}
|
|
187
|
+
throw err;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const { projectName, isManaged, projectId } = await resolveProjectContext(options);
|
|
193
|
+
|
|
194
|
+
output.start("Setting secret...");
|
|
195
|
+
|
|
196
|
+
if (isManaged && projectId) {
|
|
197
|
+
// Managed mode: use control plane API
|
|
198
|
+
await setSecretManaged(projectId, keyName, value);
|
|
199
|
+
} else {
|
|
200
|
+
// BYO mode: use wrangler
|
|
201
|
+
await setSecretByo(projectName, keyName, value);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
output.stop();
|
|
205
|
+
success(`Secret set: ${keyName}`);
|
|
206
|
+
|
|
207
|
+
// Warn about VITE_ prefix (informative, not blocking)
|
|
208
|
+
if (keyName.startsWith("VITE_")) {
|
|
209
|
+
info("Note: VITE_* variables are embedded at build time, not runtime.");
|
|
210
|
+
info("For frontend access, add to .env and redeploy.");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Set secret via control plane (managed mode)
|
|
216
|
+
*/
|
|
217
|
+
async function setSecretManaged(projectId: string, name: string, value: string): Promise<void> {
|
|
218
|
+
const { authFetch } = await import("../lib/auth/index.ts");
|
|
219
|
+
|
|
220
|
+
const response = await authFetch(`${getControlApiUrl()}/v1/projects/${projectId}/secrets`, {
|
|
221
|
+
method: "POST",
|
|
222
|
+
headers: { "Content-Type": "application/json" },
|
|
223
|
+
body: JSON.stringify({ name, value }),
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
if (!response.ok) {
|
|
227
|
+
const err = (await response.json().catch(() => ({ message: "Unknown error" }))) as {
|
|
228
|
+
message?: string;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
output.stop();
|
|
232
|
+
throw new JackError(
|
|
233
|
+
JackErrorCode.INTERNAL_ERROR,
|
|
234
|
+
err.message || `Failed to set secret: ${response.status}`,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Set secret via wrangler (BYO mode)
|
|
241
|
+
*/
|
|
242
|
+
async function setSecretByo(projectName: string, name: string, value: string): Promise<void> {
|
|
243
|
+
// Use wrangler secret put with stdin
|
|
244
|
+
const result = await $`echo ${value} | wrangler secret put ${name}`.nothrow().quiet();
|
|
245
|
+
|
|
246
|
+
if (result.exitCode !== 0) {
|
|
247
|
+
output.stop();
|
|
248
|
+
const stderr = result.stderr.toString();
|
|
249
|
+
throw new JackError(
|
|
250
|
+
JackErrorCode.DEPLOY_FAILED,
|
|
251
|
+
`Failed to set secret: ${stderr}`,
|
|
252
|
+
"Make sure wrangler is configured and the project is deployed",
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* List secrets
|
|
259
|
+
*/
|
|
260
|
+
async function listSecrets(options: SecretsOptions): Promise<void> {
|
|
261
|
+
const { projectName, isManaged, projectId } = await resolveProjectContext(options);
|
|
262
|
+
|
|
263
|
+
output.start("Loading secrets...");
|
|
264
|
+
|
|
265
|
+
let secrets: Array<{ name: string }>;
|
|
266
|
+
|
|
267
|
+
if (isManaged && projectId) {
|
|
268
|
+
secrets = await listSecretsManaged(projectId);
|
|
269
|
+
} else {
|
|
270
|
+
secrets = await listSecretsByo(projectName);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
output.stop();
|
|
274
|
+
|
|
275
|
+
if (secrets.length === 0) {
|
|
276
|
+
info("No secrets configured");
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
console.error("");
|
|
281
|
+
for (const secret of secrets) {
|
|
282
|
+
console.error(` ${secret.name}`);
|
|
283
|
+
}
|
|
284
|
+
console.error("");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* List secrets via control plane (managed mode)
|
|
289
|
+
*/
|
|
290
|
+
async function listSecretsManaged(projectId: string): Promise<Array<{ name: string }>> {
|
|
291
|
+
const { authFetch } = await import("../lib/auth/index.ts");
|
|
292
|
+
|
|
293
|
+
const response = await authFetch(`${getControlApiUrl()}/v1/projects/${projectId}/secrets`);
|
|
294
|
+
|
|
295
|
+
if (!response.ok) {
|
|
296
|
+
const err = (await response.json().catch(() => ({ message: "Unknown error" }))) as {
|
|
297
|
+
message?: string;
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
output.stop();
|
|
301
|
+
throw new JackError(
|
|
302
|
+
JackErrorCode.INTERNAL_ERROR,
|
|
303
|
+
err.message || `Failed to list secrets: ${response.status}`,
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const data = (await response.json()) as { secrets: Array<{ name: string }> };
|
|
308
|
+
return data.secrets;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* List secrets via wrangler (BYO mode)
|
|
313
|
+
*/
|
|
314
|
+
async function listSecretsByo(_projectName: string): Promise<Array<{ name: string }>> {
|
|
315
|
+
const result = await $`wrangler secret list --format json`.nothrow().quiet();
|
|
316
|
+
|
|
317
|
+
if (result.exitCode !== 0) {
|
|
318
|
+
// If no secrets or project not deployed, return empty list
|
|
319
|
+
const stderr = result.stderr.toString();
|
|
320
|
+
if (stderr.includes("not found") || stderr.includes("does not exist")) {
|
|
321
|
+
return [];
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
output.stop();
|
|
325
|
+
throw new JackError(
|
|
326
|
+
JackErrorCode.DEPLOY_FAILED,
|
|
327
|
+
`Failed to list secrets: ${stderr}`,
|
|
328
|
+
"Make sure wrangler is configured and the project is deployed",
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
const stdout = result.stdout.toString().trim();
|
|
334
|
+
if (!stdout) {
|
|
335
|
+
return [];
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const secrets = JSON.parse(stdout) as Array<{ name: string; type: string }>;
|
|
339
|
+
return secrets.map((s) => ({ name: s.name }));
|
|
340
|
+
} catch {
|
|
341
|
+
// If JSON parsing fails, try line-by-line parsing
|
|
342
|
+
const lines = result.stdout.toString().trim().split("\n");
|
|
343
|
+
return lines.filter((line) => line.trim()).map((line) => ({ name: line.trim() }));
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Remove a secret
|
|
349
|
+
*/
|
|
350
|
+
async function removeSecret(args: string[], options: SecretsOptions): Promise<void> {
|
|
351
|
+
const keyName = args[0];
|
|
352
|
+
|
|
353
|
+
if (!keyName) {
|
|
354
|
+
error("Missing secret key");
|
|
355
|
+
info("Usage: jack secrets rm <KEY>");
|
|
356
|
+
process.exit(1);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const { projectName, isManaged, projectId } = await resolveProjectContext(options);
|
|
360
|
+
|
|
361
|
+
output.start("Removing secret...");
|
|
362
|
+
|
|
363
|
+
if (isManaged && projectId) {
|
|
364
|
+
await removeSecretManaged(projectId, keyName);
|
|
365
|
+
} else {
|
|
366
|
+
await removeSecretByo(projectName, keyName);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
output.stop();
|
|
370
|
+
success(`Secret removed: ${keyName}`);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Remove secret via control plane (managed mode)
|
|
375
|
+
*/
|
|
376
|
+
async function removeSecretManaged(projectId: string, name: string): Promise<void> {
|
|
377
|
+
const { authFetch } = await import("../lib/auth/index.ts");
|
|
378
|
+
|
|
379
|
+
const response = await authFetch(
|
|
380
|
+
`${getControlApiUrl()}/v1/projects/${projectId}/secrets/${encodeURIComponent(name)}`,
|
|
381
|
+
{ method: "DELETE" },
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
if (!response.ok) {
|
|
385
|
+
const err = (await response.json().catch(() => ({ message: "Unknown error" }))) as {
|
|
386
|
+
message?: string;
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
output.stop();
|
|
390
|
+
throw new JackError(
|
|
391
|
+
JackErrorCode.INTERNAL_ERROR,
|
|
392
|
+
err.message || `Failed to remove secret: ${response.status}`,
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Remove secret via wrangler (BYO mode)
|
|
399
|
+
*/
|
|
400
|
+
async function removeSecretByo(_projectName: string, name: string): Promise<void> {
|
|
401
|
+
// Use yes | to auto-confirm the deletion
|
|
402
|
+
const result = await $`yes | wrangler secret delete ${name}`.nothrow().quiet();
|
|
403
|
+
|
|
404
|
+
if (result.exitCode !== 0) {
|
|
405
|
+
output.stop();
|
|
406
|
+
const stderr = result.stderr.toString();
|
|
407
|
+
throw new JackError(
|
|
408
|
+
JackErrorCode.DEPLOY_FAILED,
|
|
409
|
+
`Failed to remove secret: ${stderr}`,
|
|
410
|
+
"Make sure wrangler is configured and the project is deployed",
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
}
|