deepspace 0.5.0 → 0.5.2

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/dist/cli.js CHANGED
@@ -1,16 +1,58 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli/cli.ts
4
- import { defineCommand as defineCommand20, runMain } from "citty";
4
+ import { defineCommand as defineCommand22, runMain } from "citty";
5
5
  import { execSync as execSync4 } from "child_process";
6
- import { readFileSync as readFileSync14, existsSync as existsSync15 } from "fs";
6
+ import { readFileSync as readFileSync14, existsSync as existsSync14 } from "fs";
7
7
  import { dirname as dirname4, join as join16 } from "path";
8
8
  import { fileURLToPath as fileURLToPath2 } from "url";
9
9
 
10
+ // src/cli/lib/cli-errors.ts
11
+ var API_ERROR_HINTS = {
12
+ not_app_owner: "Only the app owner can do this.",
13
+ app_not_found: "App not found. Check the app name (wrangler.toml `name`) and that it has been deployed.",
14
+ not_app_owner_or_collaborator: "You must be the app owner or a collaborator to do this.",
15
+ test_account_cannot_be_collaborator: "Test accounts cannot be added as collaborators. Use a real DeepSpace account.",
16
+ user_not_found: "No DeepSpace user with that email. New users must sign in and use DeepSpace once (e.g. run `deepspace whoami`) before they can be referenced by email."
17
+ };
18
+ function formatCliError(err) {
19
+ const message = err instanceof Error ? err.message : String(err);
20
+ const code = err?.code;
21
+ const slug = typeof code === "string" ? code : /^API \S+ \(\d+\): ([a-z0-9_]+)$/.exec(message)?.[1];
22
+ const hint = slug ? API_ERROR_HINTS[slug] : void 0;
23
+ return hint && !message.includes(hint) ? `${message}
24
+ ${hint}` : message;
25
+ }
26
+ function renderCliError(err) {
27
+ console.error(formatCliError(err));
28
+ if (process.env.DEBUG && err instanceof Error && err.stack) {
29
+ console.error("\n" + err.stack);
30
+ }
31
+ process.exit(1);
32
+ }
33
+ function wrapCommandErrors(cmd) {
34
+ const run2 = cmd.run;
35
+ if (run2) {
36
+ cmd.run = (async (ctx) => {
37
+ try {
38
+ await run2(ctx);
39
+ } catch (err) {
40
+ renderCliError(err);
41
+ }
42
+ });
43
+ }
44
+ if (cmd.subCommands && typeof cmd.subCommands === "object") {
45
+ for (const sub of Object.values(cmd.subCommands)) {
46
+ if (sub && typeof sub === "object") wrapCommandErrors(sub);
47
+ }
48
+ }
49
+ return cmd;
50
+ }
51
+
10
52
  // src/cli/commands/login.ts
11
53
  import { defineCommand } from "citty";
12
- import { writeFileSync as writeFileSync4, mkdirSync } from "fs";
13
- import { join as join2 } from "path";
54
+ import { writeFileSync as writeFileSync5, mkdirSync } from "fs";
55
+ import { join as join3 } from "path";
14
56
  import { homedir } from "os";
15
57
  import { randomBytes, createHash } from "crypto";
16
58
  import * as p2 from "@clack/prompts";
@@ -33,12 +75,288 @@ function createSpinner() {
33
75
  }
34
76
 
35
77
  // src/cli/env.ts
36
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
78
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
79
+
80
+ // src/cli/lib/secrets.ts
81
+ var GENERATED_SECRETS_DIVIDER = "# --- DeepSpace secrets cache: generated by `npx deepspace secrets`; do not edit manually ---";
82
+ var SECRET_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
83
+ var CONFIG_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
84
+ var MAX_SECRET_VALUE_BYTES = 32 * 1024;
85
+ function validateConfigName(configName) {
86
+ if (!CONFIG_NAME_RE.test(configName)) {
87
+ throw new Error(
88
+ `Invalid config "${configName}". Use letters, numbers, underscores, or hyphens; start with a letter or number.`
89
+ );
90
+ }
91
+ return configName;
92
+ }
93
+ function validateSecretName(name) {
94
+ if (!SECRET_NAME_RE.test(name)) {
95
+ throw new Error(
96
+ `Invalid secret name "${name}". Use letters, numbers, and underscores; start with a letter or underscore.`
97
+ );
98
+ }
99
+ return name;
100
+ }
101
+ function validateSecretValue(key, value) {
102
+ if (new TextEncoder().encode(value).byteLength > MAX_SECRET_VALUE_BYTES) {
103
+ throw new Error(`Secret "${key}" exceeds ${MAX_SECRET_VALUE_BYTES} bytes`);
104
+ }
105
+ }
106
+ function defaultConfigNameForEnv(wranglerEnv) {
107
+ return wranglerEnv ?? "prd";
108
+ }
109
+ async function secretsApi(deployUrl, token, path, init = {}) {
110
+ const res = await fetch(`${deployUrl}/api/secrets${path}`, {
111
+ ...init,
112
+ headers: {
113
+ Authorization: `Bearer ${token}`,
114
+ "Content-Type": "application/json",
115
+ ...init.headers ?? {}
116
+ }
117
+ });
118
+ const body = await res.json().catch(() => ({}));
119
+ if (!res.ok) {
120
+ throw new Error(`Secrets API ${path} (${res.status}): ${body.error ?? "request failed"}`);
121
+ }
122
+ return body;
123
+ }
124
+ function listSecrets(deployUrl, token, appId, configName) {
125
+ return secretsApi(deployUrl, token, `/${appId}/configs/${encodeURIComponent(configName)}`);
126
+ }
127
+ function fetchSecretsValues(deployUrl, token, appId, configName) {
128
+ return secretsApi(deployUrl, token, `/${appId}/configs/${encodeURIComponent(configName)}/values`);
129
+ }
130
+ function getSecretPlain(deployUrl, token, appId, configName, key) {
131
+ return secretsApi(
132
+ deployUrl,
133
+ token,
134
+ `/${appId}/configs/${encodeURIComponent(configName)}/secrets/${encodeURIComponent(key)}?plain=true`
135
+ );
136
+ }
137
+ function setSecret(deployUrl, token, appId, configName, key, value) {
138
+ return secretsApi(
139
+ deployUrl,
140
+ token,
141
+ `/${appId}/configs/${encodeURIComponent(configName)}/secrets/${encodeURIComponent(key)}`,
142
+ { method: "PUT", body: JSON.stringify({ value }) }
143
+ );
144
+ }
145
+ function deleteSecret(deployUrl, token, appId, configName, key) {
146
+ return secretsApi(
147
+ deployUrl,
148
+ token,
149
+ `/${appId}/configs/${encodeURIComponent(configName)}/secrets/${encodeURIComponent(key)}`,
150
+ { method: "DELETE" }
151
+ );
152
+ }
153
+ function uploadSecrets(deployUrl, token, appId, configName, secrets, replace) {
154
+ return secretsApi(
155
+ deployUrl,
156
+ token,
157
+ `/${appId}/configs/${encodeURIComponent(configName)}/secrets`,
158
+ { method: "PUT", body: JSON.stringify({ secrets, replace }) }
159
+ );
160
+ }
161
+ function listConfigs(deployUrl, token, appId) {
162
+ return secretsApi(deployUrl, token, `/${appId}/configs`);
163
+ }
164
+ function createConfig(deployUrl, token, appId, configName, copyFrom) {
165
+ return secretsApi(deployUrl, token, `/${appId}/configs`, {
166
+ method: "POST",
167
+ body: JSON.stringify({ name: configName, ...copyFrom ? { copyFrom } : {} })
168
+ });
169
+ }
170
+ function deleteConfig(deployUrl, token, appId, configName) {
171
+ return secretsApi(deployUrl, token, `/${appId}/configs/${encodeURIComponent(configName)}`, {
172
+ method: "DELETE"
173
+ });
174
+ }
175
+ async function pullAppSecretsCache(deployUrl, token, appId, configName) {
176
+ try {
177
+ const { secrets } = await fetchSecretsValues(deployUrl, token, appId, configName);
178
+ return { appId, configName, values: secrets };
179
+ } catch (err) {
180
+ const msg = err instanceof Error ? err.message : String(err);
181
+ if (msg.includes("(404)")) return null;
182
+ throw err;
183
+ }
184
+ }
185
+ function quoteDotenvValue(value) {
186
+ if (/^[A-Za-z0-9_./:@+-]*$/.test(value) && value !== "") return value;
187
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r");
188
+ return `"${escaped}"`;
189
+ }
190
+ function renderSecretsCache(secrets, source) {
191
+ const keys = Object.keys(secrets).sort();
192
+ const lines = [
193
+ GENERATED_SECRETS_DIVIDER,
194
+ `# app ${source.appId} \xB7 config ${source.configName}`,
195
+ "# Use `npx deepspace secrets` to change these values."
196
+ ];
197
+ for (const key of keys) {
198
+ validateSecretValue(key, secrets[key]);
199
+ lines.push(`${key}=${quoteDotenvValue(secrets[key])}`);
200
+ }
201
+ return lines.join("\n");
202
+ }
203
+ function stripGeneratedSecretsCache(content) {
204
+ const idx = content.indexOf(GENERATED_SECRETS_DIVIDER);
205
+ if (idx === -1) return content;
206
+ return content.slice(0, idx).trimEnd() + "\n";
207
+ }
208
+ function parseSecretsUpload(content) {
209
+ const trimmed = content.trim();
210
+ if (!trimmed) return {};
211
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
212
+ const parsed = JSON.parse(trimmed);
213
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
214
+ throw new Error("JSON upload must be an object of KEY=value strings.");
215
+ }
216
+ const out2 = {};
217
+ for (const [key, value] of Object.entries(parsed)) {
218
+ validateSecretName(key);
219
+ if (typeof value !== "string") throw new Error(`Secret "${key}" must be a string`);
220
+ validateSecretValue(key, value);
221
+ out2[key] = value;
222
+ }
223
+ return out2;
224
+ }
225
+ const out = {};
226
+ for (const rawLine of trimmed.split("\n")) {
227
+ const line = rawLine.trim();
228
+ if (!line || line.startsWith("#")) continue;
229
+ const eq = line.indexOf("=");
230
+ if (eq <= 0) continue;
231
+ const key = line.slice(0, eq).trim();
232
+ if (!SECRET_NAME_RE.test(key)) continue;
233
+ let value = line.slice(eq + 1).trim();
234
+ if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
235
+ value = unescapeDotenv(value.slice(1, -1));
236
+ }
237
+ validateSecretValue(key, value);
238
+ out[key] = value;
239
+ }
240
+ return out;
241
+ }
242
+ function unescapeDotenv(v) {
243
+ let out = "";
244
+ for (let i = 0; i < v.length; i++) {
245
+ if (v[i] === "\\" && i + 1 < v.length) {
246
+ const c = v[++i];
247
+ out += c === "n" ? "\n" : c === "r" ? "\r" : c;
248
+ } else {
249
+ out += v[i];
250
+ }
251
+ }
252
+ return out;
253
+ }
254
+ function formatSecretsDownload(secrets, format) {
255
+ const entries = Object.entries(secrets).sort(([a], [b]) => a.localeCompare(b));
256
+ if (format === "json") {
257
+ return JSON.stringify(Object.fromEntries(entries), null, 2) + "\n";
258
+ }
259
+ if (format === "shell") {
260
+ return entries.map(([k, v]) => `export ${k}=${quoteDotenvValue(v)}`).join("\n") + "\n";
261
+ }
262
+ return entries.map(([k, v]) => `${k}=${quoteDotenvValue(v)}`).join("\n") + "\n";
263
+ }
264
+ async function refreshSecretsCache(deployUrl, token, appId, wranglerEnv) {
265
+ const configName = defaultConfigNameForEnv(wranglerEnv);
266
+ const pulled = await pullAppSecretsCache(deployUrl, token, appId, configName);
267
+ if (!pulled) return null;
268
+ const count = Object.keys(pulled.values).length;
269
+ return {
270
+ pulled,
271
+ rendered: renderSecretsCache(pulled.values, pulled),
272
+ summary: `Secrets: ${pulled.configName} (${count} secret${count === 1 ? "" : "s"})`
273
+ };
274
+ }
275
+
276
+ // src/cli/lib/app-identity.ts
277
+ import { existsSync, readFileSync, writeFileSync } from "fs";
278
+ import { join, resolve } from "path";
279
+ import { parse as parseToml } from "smol-toml";
280
+ var APP_ID_RE = /^app_[0-9A-HJKMNP-TV-Z]{26}$/;
281
+ var CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
282
+ function mintAppId(now = Date.now()) {
283
+ let ts = "";
284
+ let t = now;
285
+ for (let i = 0; i < 10; i++) {
286
+ ts = CROCKFORD[t % 32] + ts;
287
+ t = Math.floor(t / 32);
288
+ }
289
+ const rand = new Uint8Array(10);
290
+ crypto.getRandomValues(rand);
291
+ let rs = "";
292
+ let acc = 0;
293
+ let bits = 0;
294
+ for (const byte of rand) {
295
+ acc = acc << 8 | byte;
296
+ bits += 8;
297
+ while (bits >= 5) {
298
+ bits -= 5;
299
+ rs += CROCKFORD[acc >> bits & 31];
300
+ }
301
+ }
302
+ return `app_${ts}${rs}`.slice(0, 30);
303
+ }
304
+ function readAppId(cwd = process.cwd(), wranglerEnv) {
305
+ const wranglerPath = join(resolve(cwd), "wrangler.toml");
306
+ if (!existsSync(wranglerPath)) return null;
307
+ let cfg;
308
+ try {
309
+ cfg = parseToml(readFileSync(wranglerPath, "utf-8"));
310
+ } catch (err) {
311
+ throw new Error(
312
+ `Could not parse ${wranglerPath}: ${err instanceof Error ? err.message : String(err)}`
313
+ );
314
+ }
315
+ const vars = wranglerEnv ? cfg.env?.[wranglerEnv]?.vars : cfg.vars;
316
+ const id = vars?.DEEPSPACE_APP_ID;
317
+ return typeof id === "string" && APP_ID_RE.test(id) ? id : null;
318
+ }
319
+ function writeAppId(cwd, appId, opts = {}) {
320
+ const wranglerPath = join(resolve(cwd), "wrangler.toml");
321
+ if (!existsSync(wranglerPath)) {
322
+ throw new Error(`No wrangler.toml in ${resolve(cwd)}`);
323
+ }
324
+ const existing = readAppId(cwd, opts.wranglerEnv);
325
+ if (existing && !opts.force) {
326
+ if (existing === appId) return;
327
+ throw new Error(
328
+ `wrangler.toml already carries ${existing}. The app id is immutable \u2014 use --new-id only to fork this repo as a separate app.`
329
+ );
330
+ }
331
+ let src = readFileSync(wranglerPath, "utf-8");
332
+ const header = opts.wranglerEnv ? `[env.${opts.wranglerEnv}.vars]` : "[vars]";
333
+ const line = `DEEPSPACE_APP_ID = "${appId}"`;
334
+ const headerRe = new RegExp(`^\\${header.replace(/\./g, "\\.")}[ \\t]*$`, "m");
335
+ const idLineRe = /^DEEPSPACE_APP_ID\s*=.*$/m;
336
+ const match = headerRe.exec(src);
337
+ if (match) {
338
+ const blockStart = match.index + match[0].length;
339
+ const rest = src.slice(blockStart);
340
+ const nextSection = rest.search(/^\s*\[/m);
341
+ const blockEnd = nextSection === -1 ? src.length : blockStart + nextSection;
342
+ const block = src.slice(blockStart, blockEnd);
343
+ const newBlock = idLineRe.test(block) ? block.replace(idLineRe, line) : `
344
+ ${line}${block}`;
345
+ src = src.slice(0, blockStart) + newBlock + src.slice(blockEnd);
346
+ } else {
347
+ src = src.trimEnd() + `
348
+
349
+ ${header}
350
+ ${line}
351
+ `;
352
+ }
353
+ writeFileSync(wranglerPath, src);
354
+ }
37
355
 
38
356
  // src/cli/lib/secure-file.ts
39
- import { chmodSync, writeFileSync } from "fs";
357
+ import { chmodSync, writeFileSync as writeFileSync2 } from "fs";
40
358
  function writeSecretFileSync(path, content) {
41
- writeFileSync(path, content, { mode: 384 });
359
+ writeFileSync2(path, content, { mode: 384 });
42
360
  try {
43
361
  chmodSync(path, 384);
44
362
  } catch {
@@ -46,9 +364,9 @@ function writeSecretFileSync(path, content) {
46
364
  }
47
365
 
48
366
  // src/cli/lib/wrangler-env.ts
49
- import { existsSync, readFileSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
50
- import { join } from "path";
51
- import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
367
+ import { existsSync as existsSync2, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync3 } from "fs";
368
+ import { join as join2 } from "path";
369
+ import { parse as parseToml2, stringify as stringifyToml } from "smol-toml";
52
370
 
53
371
  // src/server/rooms/app-name.ts
54
372
  var APP_NAME_RULES = {
@@ -146,35 +464,35 @@ function resolveAppNameForEnv(config, envName) {
146
464
  }
147
465
  return resolveAppName(envBlock.name);
148
466
  }
149
- function devVarsPathFor(appDir2, envName, opts = {}) {
150
- if (opts.sharedDevVarsCache || !envName) return join(appDir2, ".dev.vars");
151
- return join(appDir2, `.dev.vars.${envName}`);
467
+ function devVarsPathFor(appDir, envName, opts = {}) {
468
+ if (opts.sharedDevVarsCache || !envName) return join2(appDir, ".dev.vars");
469
+ return join2(appDir, `.dev.vars.${envName}`);
152
470
  }
153
- function legacyEnvDevVarsPathFor(appDir2, envName) {
154
- return envName ? join(appDir2, `.dev.vars.${envName}`) : void 0;
471
+ function legacyEnvDevVarsPathFor(appDir, envName) {
472
+ return envName ? join2(appDir, `.dev.vars.${envName}`) : void 0;
155
473
  }
156
- function warnIfLegacyEnvDevVarsExists(appDir2, envName, warn = console.warn) {
157
- const legacyPath = legacyEnvDevVarsPathFor(appDir2, envName);
158
- if (!legacyPath || !existsSync(legacyPath)) return;
474
+ function warnIfLegacyEnvDevVarsExists(appDir, envName, warn = console.warn) {
475
+ const legacyPath = legacyEnvDevVarsPathFor(appDir, envName);
476
+ if (!legacyPath || !existsSync2(legacyPath)) return;
159
477
  warn(
160
- `Warning: ${legacyPath} exists, but DeepSpace now uses ${devVarsPathFor(appDir2, envName, { sharedDevVarsCache: true })} for every environment. This run will ignore ${legacyPath}; move any secrets you still need into .dev.vars, then remove the env-specific file when you're ready.`
478
+ `Warning: ${legacyPath} exists, but DeepSpace now uses ${devVarsPathFor(appDir, envName, { sharedDevVarsCache: true })} for every environment. This run will ignore ${legacyPath}; move any secrets you still need into .dev.vars, then remove the env-specific file when you're ready.`
161
479
  );
162
480
  }
163
- function prepareWranglerEnvConfig(appDir2, envName, opts = {}) {
481
+ function prepareWranglerEnvConfig(appDir, envName, opts = {}) {
164
482
  if (!envName) return { cleanup() {
165
483
  } };
166
484
  if (!opts.sharedDevVarsCache) return { wranglerEnv: envName, cleanup() {
167
485
  } };
168
- warnIfLegacyEnvDevVarsExists(appDir2, envName, opts.warn);
169
- const config = readWranglerConfig(appDir2);
486
+ warnIfLegacyEnvDevVarsExists(appDir, envName, opts.warn);
487
+ const config = readWranglerConfig(appDir);
170
488
  const resolved = resolveAppNameForEnv(config, envName);
171
489
  if (!resolved.ok) throw new Error(resolved.reason);
172
490
  const flattened = flattenWranglerEnvConfig(config, envName);
173
- const configPath = join(
174
- appDir2,
491
+ const configPath = join2(
492
+ appDir,
175
493
  `.wrangler.deepspace.${safeGeneratedConfigSegment(envName)}.${process.pid}.${++generatedConfigSeq}.toml`
176
494
  );
177
- writeFileSync2(
495
+ writeFileSync3(
178
496
  configPath,
179
497
  [
180
498
  "# Generated by DeepSpace for this CLI run.",
@@ -230,17 +548,17 @@ var WranglerConfigError = class extends Error {
230
548
  this.name = "WranglerConfigError";
231
549
  }
232
550
  };
233
- function readWranglerConfig(appDir2) {
234
- const path = join(appDir2, "wrangler.toml");
235
- const raw = readFileSync(path, "utf-8");
551
+ function readWranglerConfig(appDir) {
552
+ const path = join2(appDir, "wrangler.toml");
553
+ const raw = readFileSync2(path, "utf-8");
236
554
  try {
237
- return parseToml(raw);
555
+ return parseToml2(raw);
238
556
  } catch (err) {
239
557
  throw new WranglerConfigError(path, err);
240
558
  }
241
559
  }
242
- function hasWranglerConfig(appDir2) {
243
- return existsSync(join(appDir2, "wrangler.toml"));
560
+ function hasWranglerConfig(appDir) {
561
+ return existsSync2(join2(appDir, "wrangler.toml"));
244
562
  }
245
563
 
246
564
  // src/cli/env.ts
@@ -262,21 +580,12 @@ async function fetchPublicKey(authUrl) {
262
580
  }
263
581
  return data.publicKey;
264
582
  }
265
- function readAppName(appDir2, wranglerEnv2) {
266
- if (!hasWranglerConfig(appDir2)) {
267
- throw new Error("No wrangler.toml found. Are you in a DeepSpace app directory?");
268
- }
269
- const config = readWranglerConfig(appDir2);
270
- const resolved = resolveAppNameForEnv(config, wranglerEnv2);
271
- if (!resolved.ok) throw new Error(resolved.reason);
272
- const rawName = wranglerEnv2 ? config.env?.[wranglerEnv2]?.name : config.name;
273
- if (rawName !== resolved.name) {
274
- const where = wranglerEnv2 ? `[env.${wranglerEnv2}].name` : "`name`";
275
- throw new Error(
276
- `wrangler.toml: ${where} "${rawName}" is not in canonical form. Update it to "${resolved.name}" and re-run.`
277
- );
278
- }
279
- return resolved.name;
583
+ function requireAppIdFor(appDir, wranglerEnv) {
584
+ const id = readAppId(appDir, wranglerEnv);
585
+ if (id) return id;
586
+ throw new Error(
587
+ "wrangler.toml has no DEEPSPACE_APP_ID" + (wranglerEnv ? ` for [env.${wranglerEnv}]` : "") + ". Run `deepspace init` (or `deepspace init --env <name>`) and retry."
588
+ );
280
589
  }
281
590
  async function mintAppOwnerJwt(authUrl, callerJwt, appName) {
282
591
  const res = await fetch(`${authUrl}/api/auth/mint-app-token`, {
@@ -285,7 +594,7 @@ async function mintAppOwnerJwt(authUrl, callerJwt, appName) {
285
594
  "Content-Type": "application/json",
286
595
  Authorization: `Bearer ${callerJwt}`
287
596
  },
288
- body: JSON.stringify({ appName })
597
+ body: JSON.stringify({ appId: appName })
289
598
  });
290
599
  if (!res.ok) {
291
600
  const err = await res.text().catch(() => "");
@@ -299,6 +608,7 @@ async function mintAppOwnerJwt(authUrl, callerJwt, appName) {
299
608
  }
300
609
  async function fetchAppIdentityToken(deployUrl, callerJwt, appName) {
301
610
  const res = await fetch(`${deployUrl}/api/apps/${encodeURIComponent(appName)}/identity-token`, {
611
+ // appName carries the appId
302
612
  method: "POST",
303
613
  headers: { Authorization: `Bearer ${callerJwt}` }
304
614
  });
@@ -327,7 +637,6 @@ var SDK_MANAGED_KEYS = /* @__PURE__ */ new Set([
327
637
  "ALLOW_DEBUG_ROUTES"
328
638
  ]);
329
639
  var DEV_VARS_DIVIDER = "# --- not managed by the SDK; preserved across dev/test runs ---";
330
- var GENERATED_SECRETS_DIVIDER = "# --- DeepSpace secrets cache: generated by `npx deepspace secrets`; do not edit manually ---";
331
640
  function extractCustomDevVars(content) {
332
641
  const lines = content.split("\n");
333
642
  const out = [];
@@ -368,11 +677,6 @@ function extractCustomDevVars(content) {
368
677
  while (out.length && out[out.length - 1].trim() === "") out.pop();
369
678
  return out.join("\n");
370
679
  }
371
- function stripGeneratedSecretsCache(content) {
372
- const idx = content.indexOf(GENERATED_SECRETS_DIVIDER);
373
- if (idx < 0) return content;
374
- return content.slice(0, idx).replace(/\s+$/u, "");
375
- }
376
680
  function removeDevVarsKeys(content, keys) {
377
681
  const lines = content.split("\n");
378
682
  const out = [];
@@ -488,12 +792,12 @@ function unescapeQuotedDevVar(value) {
488
792
  }
489
793
  return out;
490
794
  }
491
- async function writeDevVars(appDir2, ownerId, callerJwt, wranglerEnv2, opts = {}) {
795
+ async function writeDevVars(appDir, ownerId, callerJwt, wranglerEnv, opts = {}) {
492
796
  const urls = PLATFORM_URLS;
493
797
  const publicKey = await fetchPublicKey(urls.auth);
494
- const appName = readAppName(appDir2, wranglerEnv2);
495
- const appOwnerJwt = await mintAppOwnerJwt(urls.auth, callerJwt, appName);
496
- const appIdentityToken = await fetchAppIdentityToken(urls.deploy, callerJwt, appName);
798
+ const appId = opts.appId ?? requireAppIdFor(appDir, wranglerEnv);
799
+ const appOwnerJwt = await mintAppOwnerJwt(urls.auth, callerJwt, appId);
800
+ const appIdentityToken = await fetchAppIdentityToken(urls.deploy, callerJwt, appId);
497
801
  const sdkVars = [
498
802
  `AUTH_JWT_PUBLIC_KEY="${publicKey}"`,
499
803
  `AUTH_JWT_ISSUER=${urls.auth}/api/auth`,
@@ -508,20 +812,27 @@ async function writeDevVars(appDir2, ownerId, callerJwt, wranglerEnv2, opts = {}
508
812
  `ALLOW_DEBUG_ROUTES=true`
509
813
  ].join("\n");
510
814
  const useSharedDevVarsCache = opts.sharedDevVarsCache ?? opts.generatedSecretsCache !== void 0;
511
- const devVarsPath = devVarsPathFor(appDir2, wranglerEnv2, {
815
+ const devVarsPath = devVarsPathFor(appDir, wranglerEnv, {
512
816
  sharedDevVarsCache: useSharedDevVarsCache
513
817
  });
514
- const existing = existsSync2(devVarsPath) ? readFileSync2(devVarsPath, "utf-8") : "";
515
- const custom = opts.generatedSecretsCache ?? extractCustomDevVars(stripGeneratedSecretsCache(existing));
516
- const body = custom ? opts.generatedSecretsCache ? `${sdkVars}
818
+ const existing = existsSync3(devVarsPath) ? readFileSync3(devVarsPath, "utf-8") : "";
819
+ const custom = opts.generatedSecretsCache ?? extractCustomDevVars(stripGeneratedSecretsCache(existing).trimEnd());
820
+ let body;
821
+ if (!custom) {
822
+ body = `${sdkVars}
823
+ `;
824
+ } else if (opts.generatedSecretsCache) {
825
+ body = `${sdkVars}
517
826
 
518
827
  ${custom}
519
- ` : `${sdkVars}
828
+ `;
829
+ } else {
830
+ body = `${sdkVars}
520
831
 
521
832
  ${DEV_VARS_DIVIDER}
522
833
  ${custom}
523
- ` : `${sdkVars}
524
834
  `;
835
+ }
525
836
  writeSecretFileSync(devVarsPath, body);
526
837
  }
527
838
 
@@ -647,13 +958,13 @@ async function pollForCompletion(sessionId, codeVerifier) {
647
958
  return null;
648
959
  }
649
960
  function storeCredentials(sessionToken, jwt) {
650
- const dir = join2(homedir(), ".deepspace");
961
+ const dir = join3(homedir(), ".deepspace");
651
962
  mkdirSync(dir, { recursive: true, mode: 448 });
652
- writeFileSync4(join2(dir, "session"), sessionToken, { mode: 384 });
653
- writeFileSync4(join2(dir, "token"), jwt, { mode: 384 });
963
+ writeFileSync5(join3(dir, "session"), sessionToken, { mode: 384 });
964
+ writeFileSync5(join3(dir, "token"), jwt, { mode: 384 });
654
965
  }
655
966
  function sleep(ms) {
656
- return new Promise((resolve11) => setTimeout(resolve11, ms));
967
+ return new Promise((resolve10) => setTimeout(resolve10, ms));
657
968
  }
658
969
  function base64url(bytes) {
659
970
  return bytes.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
@@ -684,20 +995,20 @@ async function doEmailLogin(email, password) {
684
995
 
685
996
  // src/cli/commands/logout.ts
686
997
  import { defineCommand as defineCommand2 } from "citty";
687
- import { existsSync as existsSync3, readFileSync as readFileSync3, rmSync as rmSync2 } from "fs";
688
- import { join as join3 } from "path";
998
+ import { existsSync as existsSync4, readFileSync as readFileSync4, rmSync as rmSync2 } from "fs";
999
+ import { join as join4 } from "path";
689
1000
  import { homedir as homedir2 } from "os";
690
1001
  var AUTH_URL2 = process.env.DEEPSPACE_AUTH_URL ?? PLATFORM_URLS.auth;
691
- var DIR = join3(homedir2(), ".deepspace");
692
- var SESSION_PATH = join3(DIR, "session");
693
- var TOKEN_PATH = join3(DIR, "token");
1002
+ var DIR = join4(homedir2(), ".deepspace");
1003
+ var SESSION_PATH = join4(DIR, "session");
1004
+ var TOKEN_PATH = join4(DIR, "token");
694
1005
  var logout_default = defineCommand2({
695
1006
  meta: {
696
1007
  name: "logout",
697
1008
  description: "Sign out and remove cached credentials"
698
1009
  },
699
1010
  async run() {
700
- const sessionToken = existsSync3(SESSION_PATH) ? readFileSync3(SESSION_PATH, "utf-8").trim() : null;
1011
+ const sessionToken = existsSync4(SESSION_PATH) ? readFileSync4(SESSION_PATH, "utf-8").trim() : null;
701
1012
  if (sessionToken) {
702
1013
  try {
703
1014
  await fetch(`${AUTH_URL2}/api/auth/sign-out`, {
@@ -712,7 +1023,7 @@ var logout_default = defineCommand2({
712
1023
  }
713
1024
  let removed = 0;
714
1025
  for (const path of [SESSION_PATH, TOKEN_PATH]) {
715
- if (existsSync3(path)) {
1026
+ if (existsSync4(path)) {
716
1027
  rmSync2(path);
717
1028
  removed++;
718
1029
  }
@@ -727,12 +1038,12 @@ var logout_default = defineCommand2({
727
1038
 
728
1039
  // src/cli/commands/dev.ts
729
1040
  import { defineCommand as defineCommand3 } from "citty";
730
- import { resolve as resolve4, basename, join as join8 } from "path";
1041
+ import { resolve as resolve4, basename, join as join9 } from "path";
731
1042
  import spawn from "cross-spawn";
732
1043
 
733
1044
  // src/cli/auth.ts
734
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync5, existsSync as existsSync4, mkdirSync as mkdirSync2 } from "fs";
735
- import { join as join4 } from "path";
1045
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync6, existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
1046
+ import { join as join5 } from "path";
736
1047
  import { homedir as homedir3 } from "os";
737
1048
 
738
1049
  // src/cli/jwt.ts
@@ -746,26 +1057,26 @@ function decodeJwtPayload(token) {
746
1057
 
747
1058
  // src/cli/auth.ts
748
1059
  var AUTH_URL3 = process.env.DEEPSPACE_AUTH_URL ?? PLATFORM_URLS.auth;
749
- var DIR2 = join4(homedir3(), ".deepspace");
750
- var SESSION_PATH2 = join4(DIR2, "session");
751
- var TOKEN_PATH2 = join4(DIR2, "token");
1060
+ var DIR2 = join5(homedir3(), ".deepspace");
1061
+ var SESSION_PATH2 = join5(DIR2, "session");
1062
+ var TOKEN_PATH2 = join5(DIR2, "token");
752
1063
  async function ensureToken() {
753
- if (existsSync4(TOKEN_PATH2)) {
754
- const existing = readFileSync4(TOKEN_PATH2, "utf-8").trim();
1064
+ if (existsSync5(TOKEN_PATH2)) {
1065
+ const existing = readFileSync5(TOKEN_PATH2, "utf-8").trim();
755
1066
  if (isTokenValid(existing)) {
756
1067
  return existing;
757
1068
  }
758
1069
  }
759
- if (!existsSync4(SESSION_PATH2)) {
1070
+ if (!existsSync5(SESSION_PATH2)) {
760
1071
  throw new Error("Not logged in. Run `deepspace login` first.");
761
1072
  }
762
- const sessionToken = readFileSync4(SESSION_PATH2, "utf-8").trim();
1073
+ const sessionToken = readFileSync5(SESSION_PATH2, "utf-8").trim();
763
1074
  const token = await exchangeSession(AUTH_URL3, sessionToken);
764
1075
  if (!token) {
765
1076
  throw new Error("Session expired. Run `deepspace login` to re-authenticate.");
766
1077
  }
767
1078
  mkdirSync2(DIR2, { recursive: true, mode: 448 });
768
- writeFileSync5(TOKEN_PATH2, token, { mode: 384 });
1079
+ writeFileSync6(TOKEN_PATH2, token, { mode: 384 });
769
1080
  return token;
770
1081
  }
771
1082
  function isTokenValid(jwt) {
@@ -779,24 +1090,26 @@ function isTokenValid(jwt) {
779
1090
 
780
1091
  // src/cli/lib/app-context.ts
781
1092
  import {
782
- existsSync as existsSync5,
783
- readFileSync as readFileSync5,
1093
+ existsSync as existsSync6,
1094
+ readFileSync as readFileSync6,
784
1095
  readdirSync,
785
- writeFileSync as writeFileSync6,
1096
+ writeFileSync as writeFileSync7,
786
1097
  mkdirSync as mkdirSync3,
787
1098
  renameSync,
788
1099
  unlinkSync
789
1100
  } from "fs";
790
- import { join as join5, resolve, dirname, isAbsolute, sep } from "path";
791
- import { parse as parseToml2 } from "smol-toml";
1101
+ import { join as join6, resolve as resolve2, dirname, isAbsolute, sep } from "path";
1102
+ import { parse as parseToml3 } from "smol-toml";
792
1103
  function detectAppName(cwd = process.cwd()) {
793
- const wranglerPath = join5(resolve(cwd), "wrangler.toml");
794
- if (!existsSync5(wranglerPath)) return null;
1104
+ const wranglerPath = join6(resolve2(cwd), "wrangler.toml");
1105
+ if (!existsSync6(wranglerPath)) return null;
795
1106
  try {
796
- const cfg = parseToml2(readFileSync5(wranglerPath, "utf-8"));
1107
+ const cfg = parseToml3(readFileSync6(wranglerPath, "utf-8"));
797
1108
  return typeof cfg.name === "string" && cfg.name.length > 0 ? cfg.name : null;
798
- } catch {
799
- return null;
1109
+ } catch (err) {
1110
+ throw new Error(
1111
+ `Could not parse ${wranglerPath}: ${err instanceof Error ? err.message : String(err)}`
1112
+ );
800
1113
  }
801
1114
  }
802
1115
  function requireAppName(explicit, cwd) {
@@ -808,9 +1121,9 @@ function requireAppName(explicit, cwd) {
808
1121
  );
809
1122
  }
810
1123
  function findAppDir(start = process.cwd()) {
811
- let dir = resolve(start);
1124
+ let dir = resolve2(start);
812
1125
  for (; ; ) {
813
- if (existsSync5(join5(dir, "wrangler.toml"))) return dir;
1126
+ if (existsSync6(join6(dir, "wrangler.toml"))) return dir;
814
1127
  const parent = dirname(dir);
815
1128
  if (parent === dir) return null;
816
1129
  dir = parent;
@@ -818,7 +1131,7 @@ function findAppDir(start = process.cwd()) {
818
1131
  }
819
1132
  function findChildApps(dir) {
820
1133
  try {
821
- return readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && existsSync5(join5(dir, e.name, "wrangler.toml"))).map((e) => e.name).sort();
1134
+ return readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && existsSync6(join6(dir, e.name, "wrangler.toml"))).map((e) => e.name).sort();
822
1135
  } catch {
823
1136
  return [];
824
1137
  }
@@ -826,7 +1139,7 @@ function findChildApps(dir) {
826
1139
  function writeLaunchFile(launchPath, config) {
827
1140
  mkdirSync3(dirname(launchPath), { recursive: true });
828
1141
  const tmpPath = `${launchPath}.${process.pid}.tmp`;
829
- writeFileSync6(tmpPath, JSON.stringify(config, null, 2) + "\n");
1142
+ writeFileSync7(tmpPath, JSON.stringify(config, null, 2) + "\n");
830
1143
  try {
831
1144
  renameSync(tmpPath, launchPath);
832
1145
  } catch (err) {
@@ -839,15 +1152,15 @@ function writeLaunchFile(launchPath, config) {
839
1152
  }
840
1153
  function readLaunchFile(launchPath) {
841
1154
  const empty = { version: "0.0.1", configurations: [] };
842
- if (!existsSync5(launchPath)) return empty;
843
- const raw = readFileSync5(launchPath, "utf-8").trim();
1155
+ if (!existsSync6(launchPath)) return empty;
1156
+ const raw = readFileSync6(launchPath, "utf-8").trim();
844
1157
  if (!raw) return empty;
845
1158
  const parsed = JSON.parse(raw);
846
1159
  if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.configurations)) return null;
847
1160
  return parsed;
848
1161
  }
849
- function writeLaunchConfigIfMissing(appDir2, appName, port) {
850
- const launchPath = join5(appDir2, ".claude", "launch.json");
1162
+ function writeLaunchConfigIfMissing(appDir, appName, port) {
1163
+ const launchPath = join6(appDir, ".claude", "launch.json");
851
1164
  try {
852
1165
  const config = readLaunchFile(launchPath);
853
1166
  if (!config) return;
@@ -862,8 +1175,8 @@ function writeLaunchConfigIfMissing(appDir2, appName, port) {
862
1175
  } catch {
863
1176
  }
864
1177
  }
865
- function detectClaudeWorktree(appDir2) {
866
- const segments = resolve(appDir2).split(sep);
1178
+ function detectClaudeWorktree(appDir) {
1179
+ const segments = resolve2(appDir).split(sep);
867
1180
  for (let i = segments.length - 3; i >= 1; i--) {
868
1181
  if (segments[i] === ".claude" && segments[i + 1] === "worktrees" && segments[i + 2]) {
869
1182
  return {
@@ -882,16 +1195,16 @@ function deriveWorktreePort(worktreeName) {
882
1195
  for (const ch of worktreeName) hash = hash * 31 + ch.charCodeAt(0) >>> 0;
883
1196
  return WORKTREE_PORT_MIN + hash % WORKTREE_PORT_RANGE;
884
1197
  }
885
- function upsertWorktreeLaunchConfig(mainRepoRoot, worktreeName, appDir2, opts) {
886
- const launchPath = join5(mainRepoRoot, ".claude", "launch.json");
1198
+ function upsertWorktreeLaunchConfig(mainRepoRoot, worktreeName, appDir, opts) {
1199
+ const launchPath = join6(mainRepoRoot, ".claude", "launch.json");
887
1200
  const entryName = WORKTREE_ENTRY_PREFIX + worktreeName;
888
1201
  try {
889
1202
  const config = readLaunchFile(launchPath);
890
1203
  if (!config) return null;
891
- const worktreesRoot = join5(mainRepoRoot, ".claude", "worktrees") + sep;
1204
+ const worktreesRoot = join6(mainRepoRoot, ".claude", "worktrees") + sep;
892
1205
  config.configurations = config.configurations.filter((c) => {
893
1206
  if (c?.name === entryName) return false;
894
- const isStaleWorktreeEntry = typeof c?.name === "string" && c.name.startsWith(WORKTREE_ENTRY_PREFIX) && typeof c.cwd === "string" && isAbsolute(c.cwd) && c.cwd.startsWith(worktreesRoot) && !existsSync5(c.cwd);
1207
+ const isStaleWorktreeEntry = typeof c?.name === "string" && c.name.startsWith(WORKTREE_ENTRY_PREFIX) && typeof c.cwd === "string" && isAbsolute(c.cwd) && c.cwd.startsWith(worktreesRoot) && !existsSync6(c.cwd);
895
1208
  return !isStaleWorktreeEntry;
896
1209
  });
897
1210
  let port = opts.port;
@@ -912,7 +1225,7 @@ function upsertWorktreeLaunchConfig(mainRepoRoot, worktreeName, appDir2, opts) {
912
1225
  // the Claude Code desktop launch.json reference alongside `port`) and
913
1226
  // verified end-to-end: with it set, the preview server builds from the
914
1227
  // worktree instead of the session's main repo.
915
- cwd: resolve(appDir2)
1228
+ cwd: resolve2(appDir)
916
1229
  });
917
1230
  writeLaunchFile(launchPath, config);
918
1231
  return { entryName, port };
@@ -920,11 +1233,11 @@ function upsertWorktreeLaunchConfig(mainRepoRoot, worktreeName, appDir2, opts) {
920
1233
  return null;
921
1234
  }
922
1235
  }
923
- function resolveWorktreePort(appDir2) {
924
- const worktree = detectClaudeWorktree(appDir2);
1236
+ function resolveWorktreePort(appDir) {
1237
+ const worktree = detectClaudeWorktree(appDir);
925
1238
  if (!worktree) return null;
926
1239
  try {
927
- const config = readLaunchFile(join5(worktree.mainRepoRoot, ".claude", "launch.json"));
1240
+ const config = readLaunchFile(join6(worktree.mainRepoRoot, ".claude", "launch.json"));
928
1241
  const entry = config?.configurations.find(
929
1242
  (c) => c?.name === WORKTREE_ENTRY_PREFIX + worktree.worktreeName
930
1243
  );
@@ -934,28 +1247,44 @@ function resolveWorktreePort(appDir2) {
934
1247
  }
935
1248
  return deriveWorktreePort(worktree.worktreeName);
936
1249
  }
1250
+ function requireAppIdArg(explicit) {
1251
+ if (explicit) {
1252
+ if (!APP_ID_RE.test(explicit)) {
1253
+ throw new Error(`"${explicit}" is not an app id (app_\u2026). Pass --app <appId>.`);
1254
+ }
1255
+ return explicit;
1256
+ }
1257
+ const appDir = findAppDir();
1258
+ const id = appDir ? readAppId(appDir) : null;
1259
+ if (!id) {
1260
+ throw new Error(
1261
+ "No app id. Run from an app directory whose wrangler.toml carries DEEPSPACE_APP_ID, or pass --app <appId>."
1262
+ );
1263
+ }
1264
+ return id;
1265
+ }
937
1266
 
938
1267
  // src/cli/lib/install-status.ts
939
- import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
940
- import { join as join6 } from "path";
941
- function ensureInstallReady(appDir2) {
942
- if (existsSync6(join6(appDir2, "node_modules", "deepspace", "package.json"))) return;
943
- const sentinelDir = join6(appDir2, ".deepspace");
944
- const errPath = join6(sentinelDir, "install.err");
945
- const startedPath = join6(sentinelDir, "install.started");
946
- const donePath = join6(sentinelDir, "install.done");
947
- const logPath = join6(sentinelDir, "install.log");
948
- if (existsSync6(errPath)) {
1268
+ import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
1269
+ import { join as join7 } from "path";
1270
+ function ensureInstallReady(appDir) {
1271
+ if (existsSync7(join7(appDir, "node_modules", "deepspace", "package.json"))) return;
1272
+ const sentinelDir = join7(appDir, ".deepspace");
1273
+ const errPath = join7(sentinelDir, "install.err");
1274
+ const startedPath = join7(sentinelDir, "install.started");
1275
+ const donePath = join7(sentinelDir, "install.done");
1276
+ const logPath = join7(sentinelDir, "install.log");
1277
+ if (existsSync7(errPath)) {
949
1278
  console.error("Background install failed:");
950
- console.error(readFileSync6(errPath, "utf-8").trimEnd());
951
- if (existsSync6(logPath)) console.error(`
1279
+ console.error(readFileSync7(errPath, "utf-8").trimEnd());
1280
+ if (existsSync7(logPath)) console.error(`
952
1281
  Full log: ${logPath}`);
953
1282
  console.error("\nRun `npm install` (or `bun install`) manually, then retry.");
954
1283
  process.exit(1);
955
1284
  }
956
- if (existsSync6(startedPath) && !existsSync6(donePath)) {
1285
+ if (existsSync7(startedPath) && !existsSync7(donePath)) {
957
1286
  console.error("Dependencies are still installing in the background.");
958
- if (existsSync6(logPath)) console.error(`Tail progress: tail -f ${logPath}`);
1287
+ if (existsSync7(logPath)) console.error(`Tail progress: tail -f ${logPath}`);
959
1288
  console.error("Retry once it finishes.");
960
1289
  process.exit(1);
961
1290
  }
@@ -965,26 +1294,35 @@ Full log: ${logPath}`);
965
1294
 
966
1295
  // src/cli/lib/preflight.ts
967
1296
  import { execFileSync } from "child_process";
968
- import { existsSync as existsSync7 } from "fs";
969
- import { resolve as resolve2 } from "path";
1297
+ import { existsSync as existsSync8 } from "fs";
1298
+ import * as nodeModule from "module";
1299
+ import { resolve as resolve3 } from "path";
1300
+ function preflightNodeVersion(command, hasRegisterHooks = typeof nodeModule.registerHooks === "function") {
1301
+ if (hasRegisterHooks) return;
1302
+ console.error(
1303
+ `deepspace ${command} requires Node 22.15 or newer (found v${process.versions.node}).`
1304
+ );
1305
+ console.error("Install the current LTS from https://nodejs.org and re-run.");
1306
+ process.exit(1);
1307
+ }
970
1308
  var DLL_NOT_FOUND = -1073741515;
971
- function preflightWindowsWorkerd(appDir2) {
1309
+ function preflightWindowsWorkerd(appDir) {
972
1310
  if (process.platform !== "win32") return;
973
- const workerdExe = resolve2(
974
- appDir2,
1311
+ const workerdExe = resolve3(
1312
+ appDir,
975
1313
  "node_modules",
976
1314
  "@cloudflare",
977
1315
  "workerd-windows-64",
978
1316
  "bin",
979
1317
  "workerd.exe"
980
1318
  );
981
- if (!existsSync7(workerdExe)) return;
1319
+ if (!existsSync8(workerdExe)) return;
982
1320
  try {
983
1321
  execFileSync(workerdExe, ["--help"], { stdio: "pipe", timeout: 1e4 });
984
1322
  return;
985
1323
  } catch (err) {
986
- const status2 = err.status;
987
- if (status2 !== DLL_NOT_FOUND) return;
1324
+ const status3 = err.status;
1325
+ if (status3 !== DLL_NOT_FOUND) return;
988
1326
  console.error("");
989
1327
  console.error(" workerd (Cloudflare Workers runtime) cannot start on this system.");
990
1328
  console.error(" It needs the Microsoft Visual C++ Redistributable, which is not installed.");
@@ -1000,728 +1338,33 @@ function preflightWindowsWorkerd(appDir2) {
1000
1338
  }
1001
1339
  }
1002
1340
 
1003
- // src/cli/lib/secrets.ts
1004
- import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "fs";
1005
- import { join as join7, resolve as resolve3 } from "path";
1006
-
1007
- // src/server/rooms/binding-manifest.ts
1008
- var AUTO_PROVISION_SENTINEL = "auto";
1009
- var VECTORIZE_METRICS = /* @__PURE__ */ new Set(["cosine", "euclidean", "dot-product"]);
1010
- var ALLOWED_BINDING_TYPES = /* @__PURE__ */ new Set([
1011
- "vectorize",
1012
- "ai",
1013
- "r2_bucket",
1014
- "kv_namespace",
1015
- "d1",
1016
- "queue",
1017
- "browser_rendering",
1018
- "analytics_engine",
1019
- "hyperdrive"
1020
- ]);
1021
- var RESERVED_BINDING_NAMES = /* @__PURE__ */ new Set([
1022
- "ASSETS",
1023
- "PLATFORM_WORKER",
1024
- "API_WORKER",
1025
- "APP_NAME",
1026
- "OWNER_USER_ID",
1027
- "AUTH_JWT_PUBLIC_KEY",
1028
- "AUTH_JWT_ISSUER",
1029
- "AUTH_WORKER_URL",
1030
- "APP_IDENTITY_TOKEN",
1031
- "APP_OWNER_JWT",
1032
- "USAGE_EVENTS"
1033
- ]);
1034
- function validateBindingManifest(manifest) {
1035
- if (!Array.isArray(manifest)) {
1036
- return { valid: false, errors: [{ reason: "Manifest must be an array" }] };
1341
+ // src/cli/lib/macos-junk.ts
1342
+ import { readdirSync as readdirSync2, rmSync as rmSync3 } from "fs";
1343
+ import { join as join8 } from "path";
1344
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".wrangler", ".vite"]);
1345
+ function removeMacosJunk(dir) {
1346
+ let removed = 0;
1347
+ let entries;
1348
+ try {
1349
+ entries = readdirSync2(dir, { withFileTypes: true });
1350
+ } catch {
1351
+ return removed;
1037
1352
  }
1038
- const errors = [];
1039
- const seenNames = /* @__PURE__ */ new Set();
1040
- for (const entry of manifest) {
1041
- if (!entry || typeof entry !== "object") {
1042
- errors.push({ reason: `Entry must be an object (got ${entry === null ? "null" : typeof entry})` });
1043
- continue;
1044
- }
1045
- const e = entry;
1046
- const type = e.type;
1047
- const name = e.name;
1048
- if (typeof type !== "string" || !ALLOWED_BINDING_TYPES.has(type)) {
1049
- errors.push({
1050
- reason: `Disallowed binding type: ${typeof type === "string" ? type : `(${typeof type})`}. Allowed: ${[...ALLOWED_BINDING_TYPES].sort().join(", ")}`
1051
- });
1052
- continue;
1053
- }
1054
- if (typeof name !== "string" || !name) {
1055
- errors.push({ reason: `Binding of type "${type}" missing 'name' (string)` });
1056
- continue;
1057
- }
1058
- if (RESERVED_BINDING_NAMES.has(name)) {
1059
- errors.push({ reason: `Binding name "${name}" is reserved by the SDK` });
1353
+ for (const entry of entries) {
1354
+ const path = join8(dir, entry.name);
1355
+ if (entry.isDirectory()) {
1356
+ if (!SKIP_DIRS.has(entry.name)) removed += removeMacosJunk(path);
1060
1357
  continue;
1061
1358
  }
1062
- if (seenNames.has(name)) {
1063
- errors.push({ reason: `Duplicate binding name "${name}"` });
1064
- continue;
1359
+ if (entry.name === ".DS_Store" || entry.name.startsWith("._")) {
1360
+ try {
1361
+ rmSync3(path, { force: true });
1362
+ removed++;
1363
+ } catch {
1364
+ }
1065
1365
  }
1066
- seenNames.add(name);
1067
- const binding = entry;
1068
- const fieldErr = requiredFieldError(binding);
1069
- if (fieldErr) errors.push({ binding, reason: fieldErr });
1070
1366
  }
1071
- if (errors.length) return { valid: false, errors };
1072
- return { valid: true, bindings: manifest };
1073
- }
1074
- function requiredFieldError(b) {
1075
- switch (b.type) {
1076
- case "vectorize": {
1077
- if (!b.index_name) return `vectorize binding "${b.name}" missing index_name (or "auto")`;
1078
- if (b.index_name === AUTO_PROVISION_SENTINEL) {
1079
- if (typeof b.dimensions !== "number" || b.dimensions <= 0) {
1080
- return `vectorize binding "${b.name}" with index_name="auto" requires "dimensions" (positive number)`;
1081
- }
1082
- if (!b.metric || !VECTORIZE_METRICS.has(b.metric)) {
1083
- return `vectorize binding "${b.name}" with index_name="auto" requires "metric" (one of: ${[...VECTORIZE_METRICS].join(", ")})`;
1084
- }
1085
- }
1086
- return null;
1087
- }
1088
- case "r2_bucket":
1089
- return b.bucket_name ? null : `r2_bucket binding "${b.name}" missing bucket_name (or "auto")`;
1090
- case "kv_namespace": {
1091
- if (!b.namespace_id) return `kv_namespace binding "${b.name}" missing namespace_id (or "auto")`;
1092
- if (b.namespace_id === AUTO_PROVISION_SENTINEL && !b.title) {
1093
- return `kv_namespace binding "${b.name}" with namespace_id="auto" requires "title"`;
1094
- }
1095
- return null;
1096
- }
1097
- case "d1": {
1098
- if (!b.id) return `d1 binding "${b.name}" missing id (or "auto")`;
1099
- if (b.id === AUTO_PROVISION_SENTINEL && !b.database_name) {
1100
- return `d1 binding "${b.name}" with id="auto" requires "database_name"`;
1101
- }
1102
- return null;
1103
- }
1104
- case "queue":
1105
- return b.queue_name ? null : `queue binding "${b.name}" missing queue_name (or "auto")`;
1106
- case "hyperdrive":
1107
- if (!b.id) return `hyperdrive binding "${b.name}" missing id`;
1108
- if (b.id === AUTO_PROVISION_SENTINEL) {
1109
- return `hyperdrive binding "${b.name}": auto-provisioning not yet supported (provide a real id)`;
1110
- }
1111
- return null;
1112
- case "ai":
1113
- case "browser_rendering":
1114
- case "analytics_engine":
1115
- return null;
1116
- // no required fields beyond `name`
1117
- default: {
1118
- const _exhaustive = b;
1119
- void _exhaustive;
1120
- return null;
1121
- }
1122
- }
1123
- }
1124
- function bindingManifestFromOutputConfig(outputConfig) {
1125
- const out = [];
1126
- const aiBinding = pluckString(outputConfig.ai, "binding");
1127
- if (aiBinding) out.push({ type: "ai", name: aiBinding });
1128
- for (const v of asObjectArray(outputConfig.vectorize)) {
1129
- const name = pluckString(v, "binding");
1130
- const index_name = pluckString(v, "index_name");
1131
- if (!name || !index_name) continue;
1132
- const dimensions = pluckNumber(v, "dimensions");
1133
- const metric = pluckString(v, "metric");
1134
- out.push({
1135
- type: "vectorize",
1136
- name,
1137
- index_name,
1138
- ...dimensions != null && { dimensions },
1139
- ...metric && (metric === "cosine" || metric === "euclidean" || metric === "dot-product") && { metric }
1140
- });
1141
- }
1142
- for (const r of asObjectArray(outputConfig.r2_buckets)) {
1143
- const name = pluckString(r, "binding");
1144
- const bucket_name = pluckString(r, "bucket_name");
1145
- if (name && bucket_name) out.push({ type: "r2_bucket", name, bucket_name });
1146
- }
1147
- for (const k of asObjectArray(outputConfig.kv_namespaces)) {
1148
- const name = pluckString(k, "binding");
1149
- const namespace_id = pluckString(k, "id");
1150
- if (!name || !namespace_id) continue;
1151
- const title = pluckString(k, "title");
1152
- out.push({
1153
- type: "kv_namespace",
1154
- name,
1155
- namespace_id,
1156
- ...title && { title }
1157
- });
1158
- }
1159
- for (const d of asObjectArray(outputConfig.d1_databases)) {
1160
- const name = pluckString(d, "binding");
1161
- const id = pluckString(d, "database_id");
1162
- if (!name || !id) continue;
1163
- const database_name = pluckString(d, "database_name");
1164
- out.push({
1165
- type: "d1",
1166
- name,
1167
- id,
1168
- ...database_name && { database_name }
1169
- });
1170
- }
1171
- const queues = isObject(outputConfig.queues) ? outputConfig.queues : null;
1172
- for (const q of asObjectArray(queues?.producers)) {
1173
- const name = pluckString(q, "binding");
1174
- const queue_name = pluckString(q, "queue");
1175
- if (name && queue_name) out.push({ type: "queue", name, queue_name });
1176
- }
1177
- const browserBinding = pluckString(outputConfig.browser, "binding");
1178
- if (browserBinding) out.push({ type: "browser_rendering", name: browserBinding });
1179
- for (const a of asObjectArray(outputConfig.analytics_engine_datasets)) {
1180
- const name = pluckString(a, "binding");
1181
- const dataset = pluckString(a, "dataset");
1182
- if (name) out.push({ type: "analytics_engine", name, dataset: dataset ?? void 0 });
1183
- }
1184
- for (const h of asObjectArray(outputConfig.hyperdrive)) {
1185
- const name = pluckString(h, "binding");
1186
- const id = pluckString(h, "id");
1187
- if (name && id) out.push({ type: "hyperdrive", name, id });
1188
- }
1189
- return out;
1190
- }
1191
- function isObject(v) {
1192
- return v !== null && typeof v === "object" && !Array.isArray(v);
1193
- }
1194
- function pluckString(v, key) {
1195
- if (!isObject(v)) return null;
1196
- const val = v[key];
1197
- return typeof val === "string" && val.length > 0 ? val : null;
1198
- }
1199
- function pluckNumber(v, key) {
1200
- if (!isObject(v)) return null;
1201
- const val = v[key];
1202
- return typeof val === "number" && Number.isFinite(val) ? val : null;
1203
- }
1204
- function asObjectArray(v) {
1205
- if (!Array.isArray(v)) return [];
1206
- return v.filter(isObject);
1207
- }
1208
-
1209
- // src/cli/lib/secrets.ts
1210
- var DEEPSPACE_SECRETS_PROJECT_VAR = "DEEPSPACE_SECRETS_PROJECT";
1211
- var DEEPSPACE_SECRETS_CONFIG_VAR = "DEEPSPACE_SECRETS_CONFIG";
1212
- var GENERATED_SECRETS_DIVIDER2 = "# --- DeepSpace secrets cache: generated by `npx deepspace secrets`; do not edit manually ---";
1213
- var REQUIRED_SECRETS_BEGIN = "# --- DeepSpace detected secrets (names only; values live in `npx deepspace secrets`) ---";
1214
- var REQUIRED_SECRETS_END = "# --- end DeepSpace detected secrets ---";
1215
- var SECRET_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
1216
- var PROJECT_NAME_RE = /^[a-z0-9][a-z0-9-]{0,62}$/;
1217
- var CONFIG_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
1218
- var MAX_SECRET_VALUE_BYTES = 32 * 1024;
1219
- function validateProjectName(projectName) {
1220
- const trimmed = projectName.trim();
1221
- if (!PROJECT_NAME_RE.test(trimmed)) {
1222
- throw new Error(
1223
- `Invalid secrets project "${projectName}". Use lowercase letters, numbers, and hyphens; start with a letter or number.`
1224
- );
1225
- }
1226
- return trimmed;
1227
- }
1228
- function validateConfigName(configName) {
1229
- const trimmed = configName.trim();
1230
- if (!CONFIG_NAME_RE.test(trimmed)) {
1231
- throw new Error(
1232
- `Invalid secrets config "${configName}". Use letters, numbers, underscores, or hyphens; start with a letter or number.`
1233
- );
1234
- }
1235
- return trimmed;
1236
- }
1237
- function validateSecretName(name) {
1238
- const trimmed = name.trim();
1239
- if (!SECRET_NAME_RE.test(trimmed)) {
1240
- throw new Error(
1241
- `Invalid secret name "${name}". Secret names must use letters, numbers, and underscores, and may not start with a number.`
1242
- );
1243
- }
1244
- if (RESERVED_BINDING_NAMES.has(trimmed)) {
1245
- throw new Error(`"${trimmed}" is reserved by DeepSpace and cannot be used as an app secret.`);
1246
- }
1247
- return trimmed;
1248
- }
1249
- function validateSecretValue(key, value) {
1250
- const bytes = new TextEncoder().encode(value).byteLength;
1251
- if (bytes > MAX_SECRET_VALUE_BYTES) {
1252
- throw new Error(`Secret "${key}" exceeds ${MAX_SECRET_VALUE_BYTES} bytes.`);
1253
- }
1254
- }
1255
- function defaultConfigNameForEnv(wranglerEnv2) {
1256
- return wranglerEnv2 ? validateConfigName(wranglerEnv2) : "prd";
1257
- }
1258
- function readTopLevelAppName(appDir2) {
1259
- const cfg = readWranglerConfig(appDir2);
1260
- const resolved = resolveAppNameForEnv(cfg, void 0);
1261
- if (!resolved.ok) throw new Error(resolved.reason);
1262
- if (cfg.name !== resolved.name) {
1263
- throw new Error(
1264
- `wrangler.toml: \`name\` "${cfg.name}" is not in canonical form. Update it to "${resolved.name}" and re-run.`
1265
- );
1266
- }
1267
- return resolved.name;
1268
- }
1269
- function resolveSecretsAppName(appDir2, wranglerEnv2) {
1270
- const cfg = readWranglerConfig(appDir2);
1271
- const resolved = resolveAppNameForEnv(cfg, wranglerEnv2);
1272
- if (!resolved.ok) throw new Error(resolved.reason);
1273
- return resolved.name;
1274
- }
1275
- function stringVar(value) {
1276
- return typeof value === "string" && value.trim() ? value.trim() : void 0;
1277
- }
1278
- function resolveSecretsLink(appDir2, wranglerEnv2) {
1279
- const cfg = readWranglerConfig(appDir2);
1280
- const defaultProjectName = readTopLevelAppName(appDir2);
1281
- const defaultConfigName = defaultConfigNameForEnv(wranglerEnv2);
1282
- const topVars = cfg.vars ?? {};
1283
- const envVars = wranglerEnv2 ? cfg.env?.[wranglerEnv2]?.vars ?? {} : {};
1284
- const projectName = stringVar(envVars[DEEPSPACE_SECRETS_PROJECT_VAR]) ?? stringVar(topVars[DEEPSPACE_SECRETS_PROJECT_VAR]);
1285
- if (!projectName) {
1286
- return { linked: false, defaultProjectName, defaultConfigName, wranglerEnv: wranglerEnv2 };
1287
- }
1288
- const configName = stringVar(envVars[DEEPSPACE_SECRETS_CONFIG_VAR]) ?? (wranglerEnv2 ? defaultConfigName : stringVar(topVars[DEEPSPACE_SECRETS_CONFIG_VAR]) ?? "prd");
1289
- return {
1290
- linked: true,
1291
- projectName: validateProjectName(projectName),
1292
- configName: validateConfigName(configName),
1293
- wranglerEnv: wranglerEnv2
1294
- };
1295
- }
1296
- function resolveSecretsTarget(appDir2, opts) {
1297
- const link = resolveSecretsLink(appDir2, opts.wranglerEnv);
1298
- const projectName = validateProjectName(
1299
- opts.project ?? (link.linked ? link.projectName : link.defaultProjectName)
1300
- );
1301
- const configName = validateConfigName(
1302
- opts.config ?? (link.linked ? link.configName : link.defaultConfigName)
1303
- );
1304
- return { projectName, configName, wranglerEnv: opts.wranglerEnv };
1305
- }
1306
- function renderSecretsCache(secrets, source) {
1307
- const keys = Object.keys(secrets).sort();
1308
- const lines = [
1309
- GENERATED_SECRETS_DIVIDER2,
1310
- `# Source: project=${source.projectName} config=${source.configName}`,
1311
- `# Pulled: ${source.pulledAt ?? (/* @__PURE__ */ new Date()).toISOString()}`,
1312
- "# Use `npx deepspace secrets` to change these values."
1313
- ];
1314
- for (const key of keys) {
1315
- validateSecretName(key);
1316
- validateSecretValue(key, secrets[key]);
1317
- lines.push(`${key}=${quoteDotenvValue(secrets[key])}`);
1318
- }
1319
- return lines.join("\n");
1320
- }
1321
- function stripGeneratedSecretsCache2(content) {
1322
- const idx = content.indexOf(GENERATED_SECRETS_DIVIDER2);
1323
- if (idx < 0) return content.replace(/\s+$/u, "");
1324
- return content.slice(0, idx).replace(/\s+$/u, "");
1325
- }
1326
- function formatSecretsDownload(secrets, format) {
1327
- const entries = Object.entries(secrets).sort(([a], [b]) => a.localeCompare(b));
1328
- switch (format) {
1329
- case "json":
1330
- case "dotnet-json":
1331
- return JSON.stringify(Object.fromEntries(entries), null, 2) + "\n";
1332
- case "env":
1333
- return entries.map(([key, value]) => `${key}=${quoteDotenvValue(value)}`).join("\n") + "\n";
1334
- case "env-no-quotes":
1335
- case "docker":
1336
- return entries.map(([key, value]) => `${key}=${value}`).join("\n") + "\n";
1337
- case "yaml":
1338
- return entries.map(([key, value]) => `${key}: ${JSON.stringify(value)}`).join("\n") + "\n";
1339
- }
1340
- }
1341
- function writeSecretsCacheFile(appDir2, secrets, source) {
1342
- const devVarsPath = secretsCacheFilePath(appDir2);
1343
- const existing = existsSync8(devVarsPath) ? readFileSync7(devVarsPath, "utf-8") : "";
1344
- const head = stripGeneratedSecretsCache2(existing);
1345
- const rendered = renderSecretsCache(secrets, source);
1346
- writeSecretFileSync(devVarsPath, head ? `${head}
1347
-
1348
- ${rendered}
1349
- ` : `${rendered}
1350
- `);
1351
- }
1352
- function clearSecretsCacheFile(appDir2) {
1353
- const devVarsPath = secretsCacheFilePath(appDir2);
1354
- if (!existsSync8(devVarsPath)) return;
1355
- const existing = readFileSync7(devVarsPath, "utf-8");
1356
- const next = stripGeneratedSecretsCache2(existing);
1357
- writeSecretFileSync(devVarsPath, next ? `${next}
1358
- ` : "");
1359
- }
1360
- function secretsTargetMatchesLink(appDir2, target) {
1361
- const link = resolveSecretsLink(appDir2, target.wranglerEnv);
1362
- return link.linked && link.projectName === target.projectName && link.configName === target.configName;
1363
- }
1364
- function appScopedSecretsApiOptions(appDir2, target) {
1365
- const link = resolveSecretsLink(appDir2, target.wranglerEnv);
1366
- if (!link.linked || link.projectName !== target.projectName) return {};
1367
- return { appName: resolveSecretsAppName(appDir2, target.wranglerEnv) };
1368
- }
1369
- function secretsCacheFilePath(appDir2) {
1370
- return join7(appDir2, ".dev.vars");
1371
- }
1372
- async function pullLinkedSecretsCache(deployUrl, token, appDir2, wranglerEnv2) {
1373
- const link = resolveSecretsLink(appDir2, wranglerEnv2);
1374
- if (!link.linked) return null;
1375
- const values = await fetchSecretsValues(deployUrl, token, link.projectName, link.configName, {
1376
- appName: resolveSecretsAppName(appDir2, wranglerEnv2)
1377
- });
1378
- return {
1379
- linked: true,
1380
- projectName: link.projectName,
1381
- configName: link.configName,
1382
- wranglerEnv: wranglerEnv2,
1383
- values
1384
- };
1385
- }
1386
- async function fetchSecretsValues(deployUrl, token, projectName, configName, options = {}) {
1387
- const body = await secretsApi(
1388
- deployUrl,
1389
- token,
1390
- `/api/secrets/${encodeURIComponent(projectName)}/configs/${encodeURIComponent(configName)}/values`,
1391
- {},
1392
- options
1393
- );
1394
- return body.secrets;
1395
- }
1396
- async function listSecrets(deployUrl, token, projectName, configName, options = {}) {
1397
- return secretsApi(
1398
- deployUrl,
1399
- token,
1400
- `/api/secrets/${encodeURIComponent(projectName)}/configs/${encodeURIComponent(configName)}`,
1401
- {},
1402
- options
1403
- );
1404
- }
1405
- async function getSecretPlain(deployUrl, token, projectName, configName, key, options = {}) {
1406
- const body = await secretsApi(
1407
- deployUrl,
1408
- token,
1409
- `/api/secrets/${encodeURIComponent(projectName)}/configs/${encodeURIComponent(configName)}/secrets/${encodeURIComponent(key)}?plain=true`,
1410
- {},
1411
- options
1412
- );
1413
- return body.value;
1414
- }
1415
- async function deleteSecret(deployUrl, token, projectName, configName, key, options = {}) {
1416
- await secretsApi(
1417
- deployUrl,
1418
- token,
1419
- `/api/secrets/${encodeURIComponent(projectName)}/configs/${encodeURIComponent(configName)}/secrets/${encodeURIComponent(key)}`,
1420
- { method: "DELETE" },
1421
- options
1422
- );
1423
- }
1424
- async function uploadSecrets(deployUrl, token, projectName, configName, secrets, replace, options = {}) {
1425
- await secretsApi(
1426
- deployUrl,
1427
- token,
1428
- `/api/secrets/${encodeURIComponent(projectName)}/configs/${encodeURIComponent(configName)}/secrets`,
1429
- {
1430
- method: "PUT",
1431
- body: JSON.stringify({ secrets, replace })
1432
- },
1433
- options
1434
- );
1435
- }
1436
- async function listConfigs(deployUrl, token, projectName, options = {}) {
1437
- const body = await secretsApi(
1438
- deployUrl,
1439
- token,
1440
- `/api/secrets/${encodeURIComponent(projectName)}/configs`,
1441
- {},
1442
- options
1443
- );
1444
- return body.configs;
1445
- }
1446
- async function createConfig(deployUrl, token, projectName, configName, copyFromConfigName, options = {}) {
1447
- return secretsApi(
1448
- deployUrl,
1449
- token,
1450
- `/api/secrets/${encodeURIComponent(projectName)}/configs`,
1451
- {
1452
- method: "POST",
1453
- body: JSON.stringify({
1454
- name: configName,
1455
- ...copyFromConfigName ? { copyFrom: copyFromConfigName } : {}
1456
- })
1457
- },
1458
- options
1459
- );
1460
- }
1461
- async function deleteConfig(deployUrl, token, projectName, configName, options = {}) {
1462
- await secretsApi(
1463
- deployUrl,
1464
- token,
1465
- `/api/secrets/${encodeURIComponent(projectName)}/configs/${encodeURIComponent(configName)}`,
1466
- { method: "DELETE" },
1467
- options
1468
- );
1469
- }
1470
- async function setServerSecretsLink(deployUrl, token, appName, projectName) {
1471
- await secretsApi(deployUrl, token, `/api/secrets-links/${encodeURIComponent(appName)}`, {
1472
- method: "PUT",
1473
- body: JSON.stringify({ projectName })
1474
- });
1475
- }
1476
- async function clearServerSecretsLink(deployUrl, token, appName) {
1477
- await secretsApi(deployUrl, token, `/api/secrets-links/${encodeURIComponent(appName)}`, {
1478
- method: "DELETE"
1479
- });
1480
- }
1481
- function parseSecretsUpload(content) {
1482
- const trimmed = content.trim();
1483
- if (!trimmed) return {};
1484
- if (trimmed.startsWith("{")) {
1485
- const parsed2 = JSON.parse(trimmed);
1486
- if (!parsed2 || typeof parsed2 !== "object" || Array.isArray(parsed2)) {
1487
- throw new Error("JSON upload must be an object of KEY=value strings.");
1488
- }
1489
- const out2 = {};
1490
- for (const [key, value] of Object.entries(parsed2)) {
1491
- const name = validateSecretName(key);
1492
- if (typeof value !== "string") {
1493
- throw new Error(`Secret "${name}" must be a string.`);
1494
- }
1495
- validateSecretValue(name, value);
1496
- out2[name] = value;
1497
- }
1498
- return out2;
1499
- }
1500
- const parsed = parseDevVars(content);
1501
- const out = {};
1502
- for (const [key, value] of Object.entries(parsed)) {
1503
- const name = validateSecretName(key);
1504
- validateSecretValue(name, value);
1505
- out[name] = value;
1506
- }
1507
- return out;
1508
- }
1509
- function writeWranglerSecretsLink(appDir2, target, opts = {}) {
1510
- const path = join7(resolve3(appDir2), "wrangler.toml");
1511
- const raw = readFileSync7(path, "utf-8");
1512
- const table = target.wranglerEnv ? `env.${target.wranglerEnv}.vars` : "vars";
1513
- const vars = {
1514
- [DEEPSPACE_SECRETS_CONFIG_VAR]: target.configName
1515
- };
1516
- if (!target.wranglerEnv || opts.projectOverride) {
1517
- vars[DEEPSPACE_SECRETS_PROJECT_VAR] = target.projectName;
1518
- } else {
1519
- const link = resolveSecretsLink(appDir2, void 0);
1520
- if (!link.linked) {
1521
- vars[DEEPSPACE_SECRETS_PROJECT_VAR] = target.projectName;
1522
- }
1523
- }
1524
- writeFileSync7(path, setTomlVars(raw, table, vars));
1525
- }
1526
- function removeWranglerSecretsLink(appDir2, wranglerEnv2) {
1527
- removeWranglerSecretsLinkKeys(appDir2, wranglerEnv2, ["project", "config"]);
1528
- }
1529
- function removeWranglerSecretsLinkKeys(appDir2, wranglerEnv2, keys) {
1530
- const path = join7(resolve3(appDir2), "wrangler.toml");
1531
- const raw = readFileSync7(path, "utf-8");
1532
- const table = wranglerEnv2 ? `env.${wranglerEnv2}.vars` : "vars";
1533
- const vars = {};
1534
- if (keys.includes("project")) vars[DEEPSPACE_SECRETS_PROJECT_VAR] = void 0;
1535
- if (keys.includes("config")) vars[DEEPSPACE_SECRETS_CONFIG_VAR] = void 0;
1536
- if (Object.keys(vars).length === 0) return;
1537
- writeFileSync7(path, setTomlVars(raw, table, vars));
1538
- }
1539
- function updateWranglerRequiredSecrets(appDir2, secretNames, opts = {}) {
1540
- const path = join7(resolve3(appDir2), "wrangler.toml");
1541
- const raw = readFileSync7(path, "utf-8");
1542
- const existing = readRequiredSecretNamesFromToml(raw);
1543
- const incoming = normalizeRequiredSecretNames(secretNames);
1544
- const next = opts.replace ? incoming : normalizeRequiredSecretNames([...existing, ...incoming]);
1545
- writeFileSync7(
1546
- path,
1547
- setRequiredSecretNamesInToml(raw, next, existing.length > 0 || hasRequiredSecretsBlock(raw))
1548
- );
1549
- return next;
1550
- }
1551
- function removeWranglerRequiredSecrets(appDir2, secretNames) {
1552
- const path = join7(resolve3(appDir2), "wrangler.toml");
1553
- const raw = readFileSync7(path, "utf-8");
1554
- const remove2 = new Set(normalizeRequiredSecretNames(secretNames));
1555
- const next = readRequiredSecretNamesFromToml(raw).filter((key) => !remove2.has(key));
1556
- writeFileSync7(path, setRequiredSecretNamesInToml(raw, next, hasRequiredSecretsBlock(raw)));
1557
- return next;
1558
- }
1559
- function quoteDotenvValue(value) {
1560
- if (value === "") return '""';
1561
- if (/^[^\s"'#=]+$/u.test(value)) return value;
1562
- return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
1563
- }
1564
- var SecretsApiError = class extends Error {
1565
- status;
1566
- detail;
1567
- constructor(path, status2, detail) {
1568
- super(`Secrets API ${path} (${status2}): ${detail}`);
1569
- this.name = "SecretsApiError";
1570
- this.status = status2;
1571
- this.detail = detail;
1572
- }
1573
- };
1574
- async function secretsApi(deployUrl, token, path, init = {}, options = {}) {
1575
- const scopedPath = withSecretsApiOptions(path, options);
1576
- let res = await fetch(`${deployUrl}${scopedPath}`, {
1577
- ...init,
1578
- headers: {
1579
- Authorization: `Bearer ${token}`,
1580
- "Content-Type": "application/json",
1581
- ...init.headers ?? {}
1582
- }
1583
- });
1584
- let text2 = await res.text();
1585
- const parsedError = parseSecretsApiError(text2);
1586
- if (!res.ok && res.status === 404 && parsedError === "app_not_found" && options.appName) {
1587
- res = await fetch(`${deployUrl}${path}`, {
1588
- ...init,
1589
- headers: {
1590
- Authorization: `Bearer ${token}`,
1591
- "Content-Type": "application/json",
1592
- ...init.headers ?? {}
1593
- }
1594
- });
1595
- text2 = await res.text();
1596
- }
1597
- if (!res.ok) {
1598
- let detail = text2;
1599
- try {
1600
- detail = JSON.parse(text2).error ?? text2;
1601
- } catch {
1602
- }
1603
- throw new SecretsApiError(path, res.status, detail);
1604
- }
1605
- if (!text2) return void 0;
1606
- return JSON.parse(text2);
1607
- }
1608
- function withSecretsApiOptions(path, options) {
1609
- if (!options.appName) return path;
1610
- const sep2 = path.includes("?") ? "&" : "?";
1611
- return `${path}${sep2}appName=${encodeURIComponent(options.appName)}`;
1612
- }
1613
- function parseSecretsApiError(text2) {
1614
- try {
1615
- const parsed = JSON.parse(text2);
1616
- return typeof parsed.error === "string" ? parsed.error : null;
1617
- } catch {
1618
- return null;
1619
- }
1620
- }
1621
- function setTomlVars(raw, table, vars) {
1622
- const lines = raw.replace(/\r\n/g, "\n").split("\n");
1623
- const header = `[${table}]`;
1624
- const headerRe = new RegExp(`^\\s*\\[${escapeRegExp(table)}\\]\\s*(?:#.*)?$`);
1625
- let start = lines.findIndex((line) => headerRe.test(line));
1626
- if (start < 0) {
1627
- const additions = Object.entries(vars).filter(([, value]) => value !== void 0).map(([key, value]) => `${key} = ${JSON.stringify(value)}`);
1628
- const prefix = raw.trimEnd();
1629
- return `${prefix}${prefix ? "\n\n" : ""}${header}
1630
- ${additions.join("\n")}
1631
- `;
1632
- }
1633
- let end = start + 1;
1634
- while (end < lines.length && !/^\s*\[\[?[^\]]+\]\]?\s*(?:#.*)?$/u.test(lines[end])) end++;
1635
- const tableLines = lines.slice(start + 1, end);
1636
- const seen = /* @__PURE__ */ new Set();
1637
- const nextTableLines = [];
1638
- for (const line of tableLines) {
1639
- const key = Object.keys(vars).find(
1640
- (name) => new RegExp(`^\\s*${escapeRegExp(name)}\\s*=`).test(line)
1641
- );
1642
- if (!key) {
1643
- nextTableLines.push(line);
1644
- continue;
1645
- }
1646
- seen.add(key);
1647
- const value = vars[key];
1648
- if (value !== void 0) nextTableLines.push(`${key} = ${JSON.stringify(value)}`);
1649
- }
1650
- for (const [key, value] of Object.entries(vars)) {
1651
- if (!seen.has(key) && value !== void 0) {
1652
- nextTableLines.push(`${key} = ${JSON.stringify(value)}`);
1653
- }
1654
- }
1655
- lines.splice(start + 1, end - start - 1, ...nextTableLines);
1656
- return lines.join("\n").replace(/\s*$/u, "\n");
1657
- }
1658
- function readRequiredSecretNamesFromToml(raw) {
1659
- const block = findRequiredSecretsBlock(raw);
1660
- if (!block) return [];
1661
- const names = [];
1662
- for (const line of block.lines.slice(block.start + 1, block.end)) {
1663
- const match = /^\s*#\s+([A-Za-z_][A-Za-z0-9_]*)\s*$/u.exec(line);
1664
- if (match) names.push(validateSecretName(match[1]));
1665
- }
1666
- return normalizeRequiredSecretNames(names);
1667
- }
1668
- function setRequiredSecretNamesInToml(raw, names, keepEmptyBlock) {
1669
- const normalized = raw.replace(/\r\n/g, "\n").replace(/\s*$/u, "\n");
1670
- const block = findRequiredSecretsBlock(normalized);
1671
- if (names.length === 0 && !block && !keepEmptyBlock) return normalized;
1672
- const rendered = renderRequiredSecretsBlock(names);
1673
- if (block) {
1674
- const lines2 = [...block.lines];
1675
- lines2.splice(block.start, block.end - block.start + 1, ...rendered);
1676
- return lines2.join("\n").replace(/\s*$/u, "\n");
1677
- }
1678
- const lines = normalized.split("\n");
1679
- if (lines[lines.length - 1] === "") lines.pop();
1680
- const insertAt = requiredSecretsInsertIndex(lines);
1681
- const insertion = insertAt < lines.length ? [...rendered, ""] : lines.length > 0 ? ["", ...rendered] : rendered;
1682
- lines.splice(insertAt, 0, ...insertion);
1683
- return lines.join("\n").replace(/\s*$/u, "\n");
1684
- }
1685
- function renderRequiredSecretsBlock(names) {
1686
- return [
1687
- REQUIRED_SECRETS_BEGIN,
1688
- ...names.length > 0 ? names.map((name) => `# ${name}`) : ["# (none declared)"],
1689
- REQUIRED_SECRETS_END
1690
- ];
1691
- }
1692
- function normalizeRequiredSecretNames(secretNames) {
1693
- return Array.from(new Set(Array.from(secretNames, (name) => validateSecretName(name)))).sort(
1694
- (a, b) => a.localeCompare(b)
1695
- );
1696
- }
1697
- function hasRequiredSecretsBlock(raw) {
1698
- return findRequiredSecretsBlock(raw) !== null;
1699
- }
1700
- function findRequiredSecretsBlock(raw) {
1701
- const lines = raw.replace(/\r\n/g, "\n").split("\n");
1702
- const start = lines.findIndex((line) => requiredSecretsBeginRe().test(line));
1703
- if (start < 0) return null;
1704
- const end = lines.findIndex((line, index) => index > start && requiredSecretsEndRe().test(line));
1705
- return { lines, start, end: end < 0 ? start : end };
1706
- }
1707
- function requiredSecretsInsertIndex(lines) {
1708
- const varsHeader = /^\s*\[vars\]\s*(?:#.*)?$/u;
1709
- const tableHeader = /^\s*\[\[?[^\]]+\]\]?\s*(?:#.*)?$/u;
1710
- const varsStart = lines.findIndex((line) => varsHeader.test(line));
1711
- if (varsStart < 0) return lines.length;
1712
- let end = varsStart + 1;
1713
- while (end < lines.length && !tableHeader.test(lines[end])) end++;
1714
- while (end > varsStart + 1 && lines[end - 1].trim() === "") end--;
1715
- return end;
1716
- }
1717
- function requiredSecretsBeginRe() {
1718
- return /^# --- DeepSpace (?:detected|required) secrets\b.*---$/u;
1719
- }
1720
- function requiredSecretsEndRe() {
1721
- return /^# --- end DeepSpace (?:detected|required) secrets ---$/u;
1722
- }
1723
- function escapeRegExp(value) {
1724
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1367
+ return removed;
1725
1368
  }
1726
1369
 
1727
1370
  // src/cli/lib/port.ts
@@ -1762,10 +1405,11 @@ var dev_default = defineCommand3({
1762
1405
  }
1763
1406
  },
1764
1407
  async run({ args }) {
1765
- const wranglerEnv2 = typeof args.env === "string" && args.env.trim() ? args.env.trim() : void 0;
1408
+ preflightNodeVersion("dev");
1409
+ const wranglerEnv = typeof args.env === "string" && args.env.trim() ? args.env.trim() : void 0;
1766
1410
  const start = resolve4(args.dir ?? ".");
1767
- const appDir2 = findAppDir(start);
1768
- if (!appDir2) {
1411
+ const appDir = findAppDir(start);
1412
+ if (!appDir) {
1769
1413
  console.error(`No wrangler.toml found at or above ${start}.`);
1770
1414
  const children = findChildApps(start);
1771
1415
  if (children.length > 0) {
@@ -1776,18 +1420,20 @@ var dev_default = defineCommand3({
1776
1420
  }
1777
1421
  process.exit(1);
1778
1422
  }
1779
- const worktree = detectClaudeWorktree(appDir2);
1423
+ const junk = removeMacosJunk(appDir);
1424
+ if (junk > 0) console.log(`Removed ${junk} macOS metadata file(s) (._*, .DS_Store)`);
1425
+ const worktree = detectClaudeWorktree(appDir);
1780
1426
  const explicitPort = Boolean(args.port);
1781
1427
  let port = worktree && !explicitPort ? deriveWorktreePort(worktree.worktreeName) : resolvePort(args.port);
1782
1428
  if (worktree) {
1783
1429
  const upserted = upsertWorktreeLaunchConfig(
1784
1430
  worktree.mainRepoRoot,
1785
1431
  worktree.worktreeName,
1786
- appDir2,
1432
+ appDir,
1787
1433
  {
1788
1434
  port,
1789
1435
  probePort: !explicitPort,
1790
- extraArgs: [...wranglerEnv2 ? ["--env", wranglerEnv2] : []]
1436
+ extraArgs: [...wranglerEnv ? ["--env", wranglerEnv] : []]
1791
1437
  }
1792
1438
  );
1793
1439
  if (upserted) {
@@ -1797,7 +1443,7 @@ var dev_default = defineCommand3({
1797
1443
  );
1798
1444
  } else {
1799
1445
  console.warn(
1800
- `Warning: could not update ${join8(worktree.mainRepoRoot, ".claude", "launch.json")} (malformed or unwritable) \u2014 the Claude preview tool may serve the main repo's code instead of this worktree.`
1446
+ `Warning: could not update ${join9(worktree.mainRepoRoot, ".claude", "launch.json")} (malformed or unwritable) \u2014 the Claude preview tool may serve the main repo's code instead of this worktree.`
1801
1447
  );
1802
1448
  }
1803
1449
  if (!explicitPort && process.env.DEEPSPACE_PORT) {
@@ -1806,8 +1452,8 @@ var dev_default = defineCommand3({
1806
1452
  );
1807
1453
  }
1808
1454
  }
1809
- writeLaunchConfigIfMissing(appDir2, detectAppName(appDir2) ?? basename(appDir2), port);
1810
- ensureInstallReady(appDir2);
1455
+ writeLaunchConfigIfMissing(appDir, detectAppName(appDir) ?? basename(appDir), port);
1456
+ ensureInstallReady(appDir);
1811
1457
  let token;
1812
1458
  try {
1813
1459
  token = await ensureToken();
@@ -1823,39 +1469,40 @@ var dev_default = defineCommand3({
1823
1469
  process.exit(1);
1824
1470
  }
1825
1471
  console.log(`Logged in as ${payload.name ?? payload.email}`);
1826
- if (wranglerEnv2) console.log(`Wrangler env: ${wranglerEnv2}`);
1472
+ if (wranglerEnv) console.log(`Wrangler env: ${wranglerEnv}`);
1827
1473
  console.log(`Port: ${port}`);
1828
1474
  let generatedSecretsCache;
1829
- try {
1830
- const linkedSecrets = await pullLinkedSecretsCache(DEPLOY_URL, token, appDir2, wranglerEnv2);
1831
- if (linkedSecrets) {
1832
- generatedSecretsCache = renderSecretsCache(linkedSecrets.values, linkedSecrets);
1833
- console.log(
1834
- `Secrets: ${linkedSecrets.projectName}/${linkedSecrets.configName} (${Object.keys(linkedSecrets.values).length})`
1475
+ const appIdForSecrets = readAppId(appDir, wranglerEnv);
1476
+ if (appIdForSecrets) {
1477
+ try {
1478
+ const refreshed = await refreshSecretsCache(DEPLOY_URL, token, appIdForSecrets, wranglerEnv);
1479
+ if (refreshed) {
1480
+ generatedSecretsCache = refreshed.rendered;
1481
+ console.log(refreshed.summary);
1482
+ }
1483
+ } catch (err) {
1484
+ console.error(
1485
+ `Failed to refresh app secrets: ${err instanceof Error ? err.message : String(err)}`
1835
1486
  );
1487
+ process.exit(1);
1836
1488
  }
1837
- } catch (err) {
1838
- console.error(
1839
- `Failed to refresh linked secrets: ${err instanceof Error ? err.message : String(err)}`
1840
- );
1841
- process.exit(1);
1842
1489
  }
1843
1490
  const sharedDevVarsCache = generatedSecretsCache !== void 0;
1844
- await writeDevVars(appDir2, payload.sub, token, wranglerEnv2, {
1491
+ await writeDevVars(appDir, payload.sub, token, wranglerEnv, {
1845
1492
  generatedSecretsCache,
1846
1493
  sharedDevVarsCache
1847
1494
  });
1848
1495
  let wranglerConfig;
1849
1496
  try {
1850
- wranglerConfig = prepareWranglerEnvConfig(appDir2, wranglerEnv2, { sharedDevVarsCache });
1497
+ wranglerConfig = prepareWranglerEnvConfig(appDir, wranglerEnv, { sharedDevVarsCache });
1851
1498
  } catch (err) {
1852
1499
  console.error(err instanceof Error ? err.message : String(err));
1853
1500
  process.exit(1);
1854
1501
  }
1855
- preflightWindowsWorkerd(appDir2);
1502
+ preflightWindowsWorkerd(appDir);
1856
1503
  console.log("Starting dev server...\n");
1857
1504
  const vite = spawn("npx", ["vite", "--port", String(port), "--strictPort", "--host"], {
1858
- cwd: appDir2,
1505
+ cwd: appDir,
1859
1506
  stdio: "inherit",
1860
1507
  env: wranglerViteEnv(process.env, wranglerConfig, { DEEPSPACE_PORT: String(port) })
1861
1508
  });
@@ -2028,18 +1675,18 @@ import { sync as spawnSync3 } from "cross-spawn";
2028
1675
  // src/cli/lib/playwright.ts
2029
1676
  import { execSync } from "child_process";
2030
1677
  import { sync as spawnSync2 } from "cross-spawn";
2031
- function ensurePlaywright(appDir2) {
1678
+ function ensurePlaywright(appDir) {
2032
1679
  try {
2033
- execSync("npx playwright --version", { cwd: appDir2, stdio: "pipe" });
1680
+ execSync("npx playwright --version", { cwd: appDir, stdio: "pipe" });
2034
1681
  } catch {
2035
1682
  console.log("Installing Playwright...");
2036
- execSync("npm install -D @playwright/test", { cwd: appDir2, stdio: "pipe" });
1683
+ execSync("npm install -D @playwright/test", { cwd: appDir, stdio: "pipe" });
2037
1684
  }
2038
1685
  const isLinux = process.platform === "linux";
2039
1686
  const isRoot = isLinux && typeof process.getuid === "function" && process.getuid() === 0;
2040
1687
  const args = isRoot ? ["install", "--with-deps", "chromium"] : ["install", "chromium"];
2041
1688
  const result = spawnSync2("npx", ["playwright", ...args], {
2042
- cwd: appDir2,
1689
+ cwd: appDir,
2043
1690
  stdio: "inherit"
2044
1691
  });
2045
1692
  if (result.status !== 0) {
@@ -2074,11 +1721,12 @@ var test_default = defineCommand5({
2074
1721
  }
2075
1722
  },
2076
1723
  async run({ args }) {
1724
+ preflightNodeVersion("test");
2077
1725
  const suite = args.suite ?? "default";
2078
- const wranglerEnv2 = typeof args.env === "string" && args.env.trim() ? args.env.trim() : void 0;
1726
+ const wranglerEnv = typeof args.env === "string" && args.env.trim() ? args.env.trim() : void 0;
2079
1727
  const start = resolve5(".");
2080
- const appDir2 = findAppDir(start);
2081
- if (!appDir2) {
1728
+ const appDir = findAppDir(start);
1729
+ if (!appDir) {
2082
1730
  console.error(`No wrangler.toml found at or above ${start}.`);
2083
1731
  const children = findChildApps(start);
2084
1732
  if (children.length > 0) {
@@ -2089,14 +1737,14 @@ var test_default = defineCommand5({
2089
1737
  }
2090
1738
  process.exit(1);
2091
1739
  }
2092
- const worktreePort = args.port ? null : resolveWorktreePort(appDir2);
1740
+ const worktreePort = args.port ? null : resolveWorktreePort(appDir);
2093
1741
  const port = worktreePort ?? resolvePort(args.port);
2094
1742
  if (worktreePort && process.env.DEEPSPACE_PORT) {
2095
1743
  console.log(
2096
1744
  `Ignoring DEEPSPACE_PORT=${process.env.DEEPSPACE_PORT} inside a worktree \u2014 targeting per-worktree port ${port}. Pass --port to override.`
2097
1745
  );
2098
1746
  }
2099
- ensureInstallReady(appDir2);
1747
+ ensureInstallReady(appDir);
2100
1748
  let token;
2101
1749
  let ownerId;
2102
1750
  try {
@@ -2108,72 +1756,73 @@ var test_default = defineCommand5({
2108
1756
  process.exit(1);
2109
1757
  }
2110
1758
  let generatedSecretsCache;
2111
- try {
2112
- const linkedSecrets = await pullLinkedSecretsCache(DEPLOY_URL2, token, appDir2, wranglerEnv2);
2113
- if (linkedSecrets) {
2114
- generatedSecretsCache = renderSecretsCache(linkedSecrets.values, linkedSecrets);
2115
- console.log(
2116
- `Secrets: ${linkedSecrets.projectName}/${linkedSecrets.configName} (${Object.keys(linkedSecrets.values).length})`
1759
+ const appIdForSecrets = readAppId(appDir, wranglerEnv);
1760
+ if (appIdForSecrets) {
1761
+ try {
1762
+ const refreshed = await refreshSecretsCache(DEPLOY_URL2, token, appIdForSecrets, wranglerEnv);
1763
+ if (refreshed) {
1764
+ generatedSecretsCache = refreshed.rendered;
1765
+ console.log(refreshed.summary);
1766
+ }
1767
+ } catch (err) {
1768
+ console.error(
1769
+ `Failed to refresh app secrets: ${err instanceof Error ? err.message : String(err)}`
2117
1770
  );
1771
+ process.exit(1);
2118
1772
  }
2119
- } catch (err) {
2120
- console.error(
2121
- `Failed to refresh linked secrets: ${err instanceof Error ? err.message : String(err)}`
2122
- );
2123
- process.exit(1);
2124
1773
  }
2125
1774
  const sharedDevVarsCache = generatedSecretsCache !== void 0;
2126
- await writeDevVars(appDir2, ownerId, token, wranglerEnv2, {
1775
+ await writeDevVars(appDir, ownerId, token, wranglerEnv, {
2127
1776
  generatedSecretsCache,
2128
1777
  sharedDevVarsCache
2129
1778
  });
2130
1779
  if (suite !== "unit") {
2131
- preflightWindowsWorkerd(appDir2);
2132
- ensurePlaywright(appDir2);
1780
+ preflightWindowsWorkerd(appDir);
1781
+ ensurePlaywright(appDir);
2133
1782
  }
2134
1783
  let exitCode = 0;
2135
1784
  switch (suite) {
2136
1785
  case "smoke":
2137
1786
  exitCode = runPlaywright(
2138
- appDir2,
1787
+ appDir,
2139
1788
  ["tests/smoke.spec.ts"],
2140
1789
  port,
2141
- wranglerEnv2,
1790
+ wranglerEnv,
2142
1791
  sharedDevVarsCache
2143
1792
  );
2144
1793
  break;
2145
1794
  case "api":
2146
1795
  exitCode = runPlaywright(
2147
- appDir2,
1796
+ appDir,
2148
1797
  ["tests/api.spec.ts"],
2149
1798
  port,
2150
- wranglerEnv2,
1799
+ wranglerEnv,
2151
1800
  sharedDevVarsCache
2152
1801
  );
2153
1802
  break;
2154
1803
  case "e2e":
2155
- exitCode = runPlaywright(appDir2, [], port, wranglerEnv2, sharedDevVarsCache);
1804
+ exitCode = runPlaywright(appDir, [], port, wranglerEnv, sharedDevVarsCache);
2156
1805
  break;
2157
1806
  case "unit":
2158
- exitCode = runVitest(appDir2);
1807
+ exitCode = runVitest(appDir);
2159
1808
  break;
2160
1809
  case "all":
2161
- exitCode = runVitest(appDir2);
1810
+ exitCode = runVitest(appDir);
2162
1811
  if (exitCode === 0)
2163
- exitCode = runPlaywright(appDir2, [], port, wranglerEnv2, sharedDevVarsCache);
1812
+ exitCode = runPlaywright(appDir, [], port, wranglerEnv, sharedDevVarsCache);
2164
1813
  break;
2165
1814
  case "default":
2166
1815
  exitCode = runPlaywright(
2167
- appDir2,
1816
+ appDir,
2168
1817
  ["tests/smoke.spec.ts", "tests/api.spec.ts"],
2169
1818
  port,
2170
- wranglerEnv2,
1819
+ wranglerEnv,
2171
1820
  sharedDevVarsCache
2172
1821
  );
2173
1822
  break;
2174
1823
  default:
2175
1824
  if (suite.endsWith(".spec.ts")) {
2176
- exitCode = runPlaywright(appDir2, [suite], port, wranglerEnv2, sharedDevVarsCache);
1825
+ exitCode = runPlaywright(appDir, [suite], port, wranglerEnv, sharedDevVarsCache);
2177
1826
  } else {
2178
1827
  console.error(`Unknown test suite: ${suite}`);
2179
1828
  console.error("Available: smoke, api, e2e, unit, all");
@@ -2183,10 +1832,10 @@ var test_default = defineCommand5({
2183
1832
  process.exit(exitCode);
2184
1833
  }
2185
1834
  });
2186
- function runPlaywright(appDir2, testFiles, port, wranglerEnv2, sharedDevVarsCache = false) {
1835
+ function runPlaywright(appDir, testFiles, port, wranglerEnv, sharedDevVarsCache = false) {
2187
1836
  let wranglerConfig;
2188
1837
  try {
2189
- wranglerConfig = prepareWranglerEnvConfig(appDir2, wranglerEnv2, { sharedDevVarsCache });
1838
+ wranglerConfig = prepareWranglerEnvConfig(appDir, wranglerEnv, { sharedDevVarsCache });
2190
1839
  } catch (err) {
2191
1840
  console.error(err instanceof Error ? err.message : String(err));
2192
1841
  return 1;
@@ -2196,7 +1845,7 @@ function runPlaywright(appDir2, testFiles, port, wranglerEnv2, sharedDevVarsCach
2196
1845
  "npx",
2197
1846
  ["playwright", "test", "--config", "tests/playwright.config.ts", ...testFiles],
2198
1847
  {
2199
- cwd: appDir2,
1848
+ cwd: appDir,
2200
1849
  stdio: "inherit",
2201
1850
  env: wranglerViteEnv(process.env, wranglerConfig, { DEEPSPACE_PORT: String(port) })
2202
1851
  }
@@ -2206,9 +1855,9 @@ function runPlaywright(appDir2, testFiles, port, wranglerEnv2, sharedDevVarsCach
2206
1855
  wranglerConfig.cleanup();
2207
1856
  }
2208
1857
  }
2209
- function runVitest(appDir2) {
1858
+ function runVitest(appDir) {
2210
1859
  const result = spawnSync3("npx", ["vitest", "run", "--passWithNoTests"], {
2211
- cwd: appDir2,
1860
+ cwd: appDir,
2212
1861
  stdio: "inherit"
2213
1862
  });
2214
1863
  return result.status ?? 1;
@@ -2246,15 +1895,15 @@ var screenshot_default = defineCommand6({
2246
1895
  }
2247
1896
  },
2248
1897
  run({ args }) {
2249
- const appDir2 = resolve6(".");
2250
- ensurePlaywright(appDir2);
1898
+ const appDir = resolve6(".");
1899
+ ensurePlaywright(appDir);
2251
1900
  const playwrightArgs = ["playwright", "screenshot", args.url, args.output];
2252
1901
  if (args["full-page"]) playwrightArgs.push("--full-page");
2253
1902
  if (args["wait-for-timeout"]) {
2254
1903
  playwrightArgs.push("--wait-for-timeout", String(args["wait-for-timeout"]));
2255
1904
  }
2256
1905
  const result = spawnSync4("npx", playwrightArgs, {
2257
- cwd: appDir2,
1906
+ cwd: appDir,
2258
1907
  stdio: "inherit"
2259
1908
  });
2260
1909
  process.exit(result.status ?? 1);
@@ -2265,12 +1914,12 @@ var screenshot_default = defineCommand6({
2265
1914
  import { defineCommand as defineCommand7 } from "citty";
2266
1915
  import * as p3 from "@clack/prompts";
2267
1916
  import { readFileSync as readFileSync8, writeFileSync as writeFileSync8, existsSync as existsSync9, mkdirSync as mkdirSync4 } from "fs";
2268
- import { join as join9 } from "path";
1917
+ import { join as join10 } from "path";
2269
1918
  import { homedir as homedir4 } from "os";
2270
1919
  var SESSION_COOKIE2 = "__Secure-better-auth.session_token";
2271
1920
  var AUTH_URL4 = process.env.DEEPSPACE_AUTH_URL ?? PLATFORM_URLS.auth;
2272
- var DIR3 = join9(homedir4(), ".deepspace");
2273
- var ACCOUNTS_PATH = join9(DIR3, "test-accounts.json");
1921
+ var DIR3 = join10(homedir4(), ".deepspace");
1922
+ var ACCOUNTS_PATH = join10(DIR3, "test-accounts.json");
2274
1923
  function loadAccounts() {
2275
1924
  if (!existsSync9(ACCOUNTS_PATH)) return [];
2276
1925
  try {
@@ -2551,13 +2200,217 @@ var test_accounts_default = defineCommand7({
2551
2200
  });
2552
2201
 
2553
2202
  // src/cli/commands/deploy.ts
2554
- import { defineCommand as defineCommand8 } from "citty";
2555
- import { execSync as execSync2 } from "child_process";
2556
- import { randomUUID } from "crypto";
2557
- import { readFileSync as readFileSync9, readdirSync as readdirSync2, existsSync as existsSync10 } from "fs";
2558
- import { join as join10, resolve as resolve7, dirname as dirname2 } from "path";
2559
- import { pathToFileURL } from "url";
2560
- import * as p4 from "@clack/prompts";
2203
+ import { defineCommand as defineCommand8 } from "citty";
2204
+ import { execSync as execSync2 } from "child_process";
2205
+ import { randomUUID } from "crypto";
2206
+ import { readFileSync as readFileSync9, readdirSync as readdirSync3, existsSync as existsSync10, writeFileSync as writeFileSync9 } from "fs";
2207
+ import { join as join11, resolve as resolve7, dirname as dirname2 } from "path";
2208
+ import { pathToFileURL } from "url";
2209
+ import * as p4 from "@clack/prompts";
2210
+
2211
+ // src/server/rooms/binding-manifest.ts
2212
+ var AUTO_PROVISION_SENTINEL = "auto";
2213
+ var VECTORIZE_METRICS = /* @__PURE__ */ new Set(["cosine", "euclidean", "dot-product"]);
2214
+ var ALLOWED_BINDING_TYPES = /* @__PURE__ */ new Set([
2215
+ "vectorize",
2216
+ "ai",
2217
+ "r2_bucket",
2218
+ "kv_namespace",
2219
+ "d1",
2220
+ "queue",
2221
+ "browser_rendering",
2222
+ "analytics_engine",
2223
+ "hyperdrive"
2224
+ ]);
2225
+ var RESERVED_BINDING_NAMES = /* @__PURE__ */ new Set([
2226
+ "ASSETS",
2227
+ "PLATFORM_WORKER",
2228
+ "API_WORKER",
2229
+ "APP_NAME",
2230
+ "OWNER_USER_ID",
2231
+ "AUTH_JWT_PUBLIC_KEY",
2232
+ "AUTH_JWT_ISSUER",
2233
+ "AUTH_WORKER_URL",
2234
+ "APP_IDENTITY_TOKEN",
2235
+ "APP_OWNER_JWT",
2236
+ "USAGE_EVENTS"
2237
+ ]);
2238
+ function validateBindingManifest(manifest) {
2239
+ if (!Array.isArray(manifest)) {
2240
+ return { valid: false, errors: [{ reason: "Manifest must be an array" }] };
2241
+ }
2242
+ const errors = [];
2243
+ const seenNames = /* @__PURE__ */ new Set();
2244
+ for (const entry of manifest) {
2245
+ if (!entry || typeof entry !== "object") {
2246
+ errors.push({ reason: `Entry must be an object (got ${entry === null ? "null" : typeof entry})` });
2247
+ continue;
2248
+ }
2249
+ const e = entry;
2250
+ const type = e.type;
2251
+ const name = e.name;
2252
+ if (typeof type !== "string" || !ALLOWED_BINDING_TYPES.has(type)) {
2253
+ errors.push({
2254
+ reason: `Disallowed binding type: ${typeof type === "string" ? type : `(${typeof type})`}. Allowed: ${[...ALLOWED_BINDING_TYPES].sort().join(", ")}`
2255
+ });
2256
+ continue;
2257
+ }
2258
+ if (typeof name !== "string" || !name) {
2259
+ errors.push({ reason: `Binding of type "${type}" missing 'name' (string)` });
2260
+ continue;
2261
+ }
2262
+ if (RESERVED_BINDING_NAMES.has(name)) {
2263
+ errors.push({ reason: `Binding name "${name}" is reserved by the SDK` });
2264
+ continue;
2265
+ }
2266
+ if (seenNames.has(name)) {
2267
+ errors.push({ reason: `Duplicate binding name "${name}"` });
2268
+ continue;
2269
+ }
2270
+ seenNames.add(name);
2271
+ const binding = entry;
2272
+ const fieldErr = requiredFieldError(binding);
2273
+ if (fieldErr) errors.push({ binding, reason: fieldErr });
2274
+ }
2275
+ if (errors.length) return { valid: false, errors };
2276
+ return { valid: true, bindings: manifest };
2277
+ }
2278
+ function requiredFieldError(b) {
2279
+ switch (b.type) {
2280
+ case "vectorize": {
2281
+ if (!b.index_name) return `vectorize binding "${b.name}" missing index_name (or "auto")`;
2282
+ if (b.index_name === AUTO_PROVISION_SENTINEL) {
2283
+ if (typeof b.dimensions !== "number" || b.dimensions <= 0) {
2284
+ return `vectorize binding "${b.name}" with index_name="auto" requires "dimensions" (positive number)`;
2285
+ }
2286
+ if (!b.metric || !VECTORIZE_METRICS.has(b.metric)) {
2287
+ return `vectorize binding "${b.name}" with index_name="auto" requires "metric" (one of: ${[...VECTORIZE_METRICS].join(", ")})`;
2288
+ }
2289
+ }
2290
+ return null;
2291
+ }
2292
+ case "r2_bucket":
2293
+ return b.bucket_name ? null : `r2_bucket binding "${b.name}" missing bucket_name (or "auto")`;
2294
+ case "kv_namespace": {
2295
+ if (!b.namespace_id) return `kv_namespace binding "${b.name}" missing namespace_id (or "auto")`;
2296
+ if (b.namespace_id === AUTO_PROVISION_SENTINEL && !b.title) {
2297
+ return `kv_namespace binding "${b.name}" with namespace_id="auto" requires "title"`;
2298
+ }
2299
+ return null;
2300
+ }
2301
+ case "d1": {
2302
+ if (!b.id) return `d1 binding "${b.name}" missing id (or "auto")`;
2303
+ if (b.id === AUTO_PROVISION_SENTINEL && !b.database_name) {
2304
+ return `d1 binding "${b.name}" with id="auto" requires "database_name"`;
2305
+ }
2306
+ return null;
2307
+ }
2308
+ case "queue":
2309
+ return b.queue_name ? null : `queue binding "${b.name}" missing queue_name (or "auto")`;
2310
+ case "hyperdrive":
2311
+ if (!b.id) return `hyperdrive binding "${b.name}" missing id`;
2312
+ if (b.id === AUTO_PROVISION_SENTINEL) {
2313
+ return `hyperdrive binding "${b.name}": auto-provisioning not yet supported (provide a real id)`;
2314
+ }
2315
+ return null;
2316
+ case "ai":
2317
+ case "browser_rendering":
2318
+ case "analytics_engine":
2319
+ return null;
2320
+ // no required fields beyond `name`
2321
+ default: {
2322
+ const _exhaustive = b;
2323
+ void _exhaustive;
2324
+ return null;
2325
+ }
2326
+ }
2327
+ }
2328
+ function bindingManifestFromOutputConfig(outputConfig) {
2329
+ const out = [];
2330
+ const aiBinding = pluckString(outputConfig.ai, "binding");
2331
+ if (aiBinding) out.push({ type: "ai", name: aiBinding });
2332
+ for (const v of asObjectArray(outputConfig.vectorize)) {
2333
+ const name = pluckString(v, "binding");
2334
+ const index_name = pluckString(v, "index_name");
2335
+ if (!name || !index_name) continue;
2336
+ const dimensions = pluckNumber(v, "dimensions");
2337
+ const metric = pluckString(v, "metric");
2338
+ out.push({
2339
+ type: "vectorize",
2340
+ name,
2341
+ index_name,
2342
+ ...dimensions != null && { dimensions },
2343
+ ...metric && (metric === "cosine" || metric === "euclidean" || metric === "dot-product") && { metric }
2344
+ });
2345
+ }
2346
+ for (const r of asObjectArray(outputConfig.r2_buckets)) {
2347
+ const name = pluckString(r, "binding");
2348
+ const bucket_name = pluckString(r, "bucket_name");
2349
+ if (name && bucket_name) out.push({ type: "r2_bucket", name, bucket_name });
2350
+ }
2351
+ for (const k of asObjectArray(outputConfig.kv_namespaces)) {
2352
+ const name = pluckString(k, "binding");
2353
+ const namespace_id = pluckString(k, "id");
2354
+ if (!name || !namespace_id) continue;
2355
+ const title = pluckString(k, "title");
2356
+ out.push({
2357
+ type: "kv_namespace",
2358
+ name,
2359
+ namespace_id,
2360
+ ...title && { title }
2361
+ });
2362
+ }
2363
+ for (const d of asObjectArray(outputConfig.d1_databases)) {
2364
+ const name = pluckString(d, "binding");
2365
+ const id = pluckString(d, "database_id");
2366
+ if (!name || !id) continue;
2367
+ const database_name = pluckString(d, "database_name");
2368
+ out.push({
2369
+ type: "d1",
2370
+ name,
2371
+ id,
2372
+ ...database_name && { database_name }
2373
+ });
2374
+ }
2375
+ const queues = isObject(outputConfig.queues) ? outputConfig.queues : null;
2376
+ for (const q of asObjectArray(queues?.producers)) {
2377
+ const name = pluckString(q, "binding");
2378
+ const queue_name = pluckString(q, "queue");
2379
+ if (name && queue_name) out.push({ type: "queue", name, queue_name });
2380
+ }
2381
+ const browserBinding = pluckString(outputConfig.browser, "binding");
2382
+ if (browserBinding) out.push({ type: "browser_rendering", name: browserBinding });
2383
+ for (const a of asObjectArray(outputConfig.analytics_engine_datasets)) {
2384
+ const name = pluckString(a, "binding");
2385
+ const dataset = pluckString(a, "dataset");
2386
+ if (name) out.push({ type: "analytics_engine", name, dataset: dataset ?? void 0 });
2387
+ }
2388
+ for (const h of asObjectArray(outputConfig.hyperdrive)) {
2389
+ const name = pluckString(h, "binding");
2390
+ const id = pluckString(h, "id");
2391
+ if (name && id) out.push({ type: "hyperdrive", name, id });
2392
+ }
2393
+ return out;
2394
+ }
2395
+ function isObject(v) {
2396
+ return v !== null && typeof v === "object" && !Array.isArray(v);
2397
+ }
2398
+ function pluckString(v, key) {
2399
+ if (!isObject(v)) return null;
2400
+ const val = v[key];
2401
+ return typeof val === "string" && val.length > 0 ? val : null;
2402
+ }
2403
+ function pluckNumber(v, key) {
2404
+ if (!isObject(v)) return null;
2405
+ const val = v[key];
2406
+ return typeof val === "number" && Number.isFinite(val) ? val : null;
2407
+ }
2408
+ function asObjectArray(v) {
2409
+ if (!Array.isArray(v)) return [];
2410
+ return v.filter(isObject);
2411
+ }
2412
+
2413
+ // src/cli/commands/deploy.ts
2561
2414
  var DEPLOY_URL3 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
2562
2415
  var API_URL = process.env.DEEPSPACE_API_URL ?? PLATFORM_URLS.api;
2563
2416
  var CLOUDFLARE_DEPLOY_ERROR_HINT = "Deployment reached the DeepSpace deploy worker, but Cloudflare's deployment control-plane API failed. This is often caused by a Cloudflare Dashboard/API incident or Workers for Platforms entitlement outage. Already deployed apps should keep serving; wait for Cloudflare to recover and retry.";
@@ -2587,8 +2440,8 @@ function extractRunWorkerFirst(cfg) {
2587
2440
  function errMessage(err) {
2588
2441
  return err instanceof Error ? err.message : String(err);
2589
2442
  }
2590
- function isLikelyCloudflareDeployFailure(status2, error) {
2591
- if (status2 >= 500) return true;
2443
+ function isLikelyCloudflareDeployFailure(status3, error) {
2444
+ if (status3 >= 500) return true;
2592
2445
  if (!error) return false;
2593
2446
  return [
2594
2447
  "Upload session failed",
@@ -2598,9 +2451,9 @@ function isLikelyCloudflareDeployFailure(status2, error) {
2598
2451
  "Internal Server Error"
2599
2452
  ].some((needle) => error.includes(needle));
2600
2453
  }
2601
- function formatDeployWorkerError(status2, error) {
2602
- const detail = error ?? `Deployment error (${status2})`;
2603
- if (!isLikelyCloudflareDeployFailure(status2, error)) return detail;
2454
+ function formatDeployWorkerError(status3, error) {
2455
+ const detail = error ?? `Deployment error (${status3})`;
2456
+ if (!isLikelyCloudflareDeployFailure(status3, error)) return detail;
2604
2457
  return `${CLOUDFLARE_DEPLOY_ERROR_HINT}
2605
2458
 
2606
2459
  Underlying error: ${detail}`;
@@ -2652,10 +2505,10 @@ async function postWithRetry(url, makeInit, {
2652
2505
  if (lastRes) return lastRes;
2653
2506
  throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
2654
2507
  }
2655
- async function abortStagedUpload(appName, uploadId, totalGroups, token) {
2508
+ async function abortStagedUpload(appId, uploadId, totalGroups, token) {
2656
2509
  try {
2657
2510
  await fetch(
2658
- `${DEPLOY_URL3}/api/deploy/${appName}/assets?uploadId=${uploadId}&totalGroups=${totalGroups}`,
2511
+ `${DEPLOY_URL3}/api/deploy/${appId}/assets?uploadId=${uploadId}&totalGroups=${totalGroups}`,
2659
2512
  { method: "DELETE", headers: { Authorization: `Bearer ${token}` } }
2660
2513
  );
2661
2514
  } catch {
@@ -2676,18 +2529,24 @@ var deploy_default = defineCommand8({
2676
2529
  type: "string",
2677
2530
  description: "wrangler.toml [env.<name>] block to deploy (e.g. --env staging). Omit to deploy the top-level config.",
2678
2531
  required: false
2532
+ },
2533
+ rename: {
2534
+ type: "boolean",
2535
+ description: "Confirm that a changed wrangler `name` renames this app (its URL moves; data, secrets, and collaborators travel). Without this flag an interactive prompt asks.",
2536
+ default: false
2679
2537
  }
2680
2538
  },
2681
2539
  async run({ args }) {
2682
- const appDir2 = resolve7(args.dir ?? ".");
2540
+ preflightNodeVersion("deploy");
2541
+ const appDir = resolve7(args.dir ?? ".");
2683
2542
  const envName = typeof args.env === "string" && args.env.trim() ? args.env.trim() : void 0;
2684
- if (!hasWranglerConfig(appDir2)) {
2543
+ if (!hasWranglerConfig(appDir)) {
2685
2544
  console.error("No wrangler.toml found. Are you in a DeepSpace app directory?");
2686
2545
  process.exit(1);
2687
2546
  }
2688
- ensureInstallReady(appDir2);
2547
+ ensureInstallReady(appDir);
2689
2548
  p4.intro("Deploying DeepSpace app");
2690
- const wranglerConfig = readWranglerConfig(appDir2);
2549
+ const wranglerConfig = readWranglerConfig(appDir);
2691
2550
  const nameRes = resolveAppNameForEnv(wranglerConfig, envName);
2692
2551
  if (!nameRes.ok) {
2693
2552
  p4.cancel(nameRes.reason);
@@ -2702,7 +2561,14 @@ var deploy_default = defineCommand8({
2702
2561
  process.exit(1);
2703
2562
  }
2704
2563
  const appName = nameRes.name;
2564
+ let appId = readAppId(appDir, envName);
2565
+ if (!appId) {
2566
+ appId = mintAppId();
2567
+ writeAppId(appDir, appId, { wranglerEnv: envName });
2568
+ p4.log.info(`Minted app id ${appId} \u2014 commit wrangler.toml.`);
2569
+ }
2705
2570
  p4.log.info(envName ? `App: ${appName} (env: ${envName})` : `App: ${appName}`);
2571
+ p4.log.info(`Id: ${appId}`);
2706
2572
  let token;
2707
2573
  let ownerId;
2708
2574
  try {
@@ -2712,39 +2578,33 @@ var deploy_default = defineCommand8({
2712
2578
  p4.cancel(errMessage(err));
2713
2579
  process.exit(1);
2714
2580
  }
2715
- const localSecretsLink = resolveSecretsLink(appDir2, envName);
2716
- if (localSecretsLink.linked) {
2717
- try {
2718
- await setServerSecretsLink(DEPLOY_URL3, token, appName, localSecretsLink.projectName);
2719
- } catch {
2720
- }
2721
- }
2722
2581
  let linkedSecrets = null;
2723
2582
  try {
2724
- linkedSecrets = await pullLinkedSecretsCache(DEPLOY_URL3, token, appDir2, envName);
2725
- if (linkedSecrets) {
2726
- const rendered = renderSecretsCache(linkedSecrets.values, linkedSecrets);
2727
- await writeDevVars(appDir2, ownerId, token, envName, {
2728
- generatedSecretsCache: rendered,
2583
+ const refreshed = await refreshSecretsCache(DEPLOY_URL3, token, appId, envName);
2584
+ if (refreshed) {
2585
+ linkedSecrets = refreshed.pulled;
2586
+ await writeDevVars(appDir, ownerId, token, envName, {
2587
+ appId,
2588
+ generatedSecretsCache: refreshed.rendered,
2729
2589
  sharedDevVarsCache: true
2730
2590
  });
2731
- p4.log.info(
2732
- `Secrets cache: ${linkedSecrets.projectName}/${linkedSecrets.configName} (${Object.keys(linkedSecrets.values).length} secret${Object.keys(linkedSecrets.values).length === 1 ? "" : "s"})`
2733
- );
2591
+ p4.log.info(refreshed.summary);
2734
2592
  }
2735
2593
  } catch (err) {
2736
- p4.cancel(`Failed to refresh linked secrets before deploy: ${errMessage(err)}`);
2594
+ p4.cancel(`Failed to refresh app secrets before deploy: ${errMessage(err)}`);
2737
2595
  process.exit(1);
2738
2596
  }
2597
+ const junk = removeMacosJunk(appDir);
2598
+ if (junk > 0) p4.log.info(`Removed ${junk} macOS metadata file(s) (._*, .DS_Store)`);
2739
2599
  const s = createSpinner();
2740
2600
  s.start("Building...");
2741
2601
  let preparedWranglerConfig;
2742
2602
  try {
2743
- preparedWranglerConfig = prepareWranglerEnvConfig(appDir2, envName, {
2744
- sharedDevVarsCache: localSecretsLink.linked
2603
+ preparedWranglerConfig = prepareWranglerEnvConfig(appDir, envName, {
2604
+ sharedDevVarsCache: linkedSecrets !== null
2745
2605
  });
2746
2606
  execSync2("npx vite build", {
2747
- cwd: appDir2,
2607
+ cwd: appDir,
2748
2608
  stdio: "pipe",
2749
2609
  env: wranglerViteEnv(process.env, preparedWranglerConfig)
2750
2610
  });
@@ -2758,7 +2618,7 @@ var deploy_default = defineCommand8({
2758
2618
  preparedWranglerConfig?.cleanup();
2759
2619
  }
2760
2620
  s.stop("Built");
2761
- const deployConfigPath = join10(appDir2, ".wrangler", "deploy", "config.json");
2621
+ const deployConfigPath = join11(appDir, ".wrangler", "deploy", "config.json");
2762
2622
  if (!existsSync10(deployConfigPath)) {
2763
2623
  p4.cancel("Build output config not found at .wrangler/deploy/config.json");
2764
2624
  process.exit(1);
@@ -2778,7 +2638,7 @@ var deploy_default = defineCommand8({
2778
2638
  process.exit(1);
2779
2639
  }
2780
2640
  const workerDir = dirname2(outputWranglerPath);
2781
- const workerBundlePath = join10(workerDir, outputConfig.main);
2641
+ const workerBundlePath = join11(workerDir, outputConfig.main);
2782
2642
  const clientDir = outputConfig.assets?.directory ? resolve7(workerDir, outputConfig.assets.directory) : null;
2783
2643
  if (!existsSync10(workerBundlePath)) {
2784
2644
  p4.cancel(`Worker bundle not found at ${workerBundlePath}`);
@@ -2820,15 +2680,13 @@ var deploy_default = defineCommand8({
2820
2680
  `Custom bindings: ${customBindings.map((b) => `${b.name} (${b.type})`).join(", ")}`
2821
2681
  );
2822
2682
  }
2823
- const devVarsPath = devVarsPathFor(appDir2, envName, {
2824
- sharedDevVarsCache: localSecretsLink.linked
2683
+ const devVarsPath = devVarsPathFor(appDir, envName, {
2684
+ sharedDevVarsCache: linkedSecrets !== null
2825
2685
  });
2826
2686
  let userSecrets = {};
2827
2687
  if (existsSync10(devVarsPath)) {
2828
2688
  const rawDevVars = readFileSync9(devVarsPath, "utf-8");
2829
- const customSection = extractCustomDevVars(
2830
- linkedSecrets ? rawDevVars : stripGeneratedSecretsCache2(rawDevVars)
2831
- );
2689
+ const customSection = extractCustomDevVars(rawDevVars);
2832
2690
  if (customSection) {
2833
2691
  try {
2834
2692
  userSecrets = parseDevVars(customSection);
@@ -2859,10 +2717,8 @@ var deploy_default = defineCommand8({
2859
2717
  }
2860
2718
  if (userSecretNames.length) {
2861
2719
  p4.log.info(`App secrets (from .dev.vars): ${userSecretNames.join(", ")}`);
2862
- if (!localSecretsLink.linked) {
2863
- p4.log.info(
2864
- "Legacy pass-through; move these to managed secrets with `npx deepspace secrets setup`."
2865
- );
2720
+ if (!linkedSecrets) {
2721
+ p4.log.info("Not in the app store yet \u2014 migrating after this deploy.");
2866
2722
  }
2867
2723
  }
2868
2724
  s.start(`Deploying to ${appName}.app.space...`);
@@ -2884,7 +2740,7 @@ var deploy_default = defineCommand8({
2884
2740
  const groupJson = JSON.stringify(assetGroups[i]);
2885
2741
  s.message(`Uploading assets \u2014 group ${i + 1}/${totalGroups}...`);
2886
2742
  const groupRes = await postWithRetry(
2887
- `${DEPLOY_URL3}/api/deploy/${appName}/assets?uploadId=${uploadId}&groupIndex=${i}&totalGroups=${totalGroups}`,
2743
+ `${DEPLOY_URL3}/api/deploy/${appId}/assets?uploadId=${uploadId}&groupIndex=${i}&totalGroups=${totalGroups}`,
2888
2744
  () => ({
2889
2745
  method: "POST",
2890
2746
  headers: {
@@ -2896,7 +2752,7 @@ var deploy_default = defineCommand8({
2896
2752
  );
2897
2753
  if (!groupRes.ok) {
2898
2754
  const errBody = await groupRes.json().catch(() => ({}));
2899
- await abortStagedUpload(appName, uploadId, totalGroups, token);
2755
+ await abortStagedUpload(appId, uploadId, totalGroups, token);
2900
2756
  s.stop("Deploy failed");
2901
2757
  p4.cancel(
2902
2758
  `Asset upload failed on group ${i + 1}/${totalGroups}: ${errBody.error ?? `HTTP ${groupRes.status}`}`
@@ -2905,13 +2761,21 @@ var deploy_default = defineCommand8({
2905
2761
  }
2906
2762
  }
2907
2763
  } catch (err) {
2908
- await abortStagedUpload(appName, uploadId, totalGroups, token);
2764
+ await abortStagedUpload(appId, uploadId, totalGroups, token);
2909
2765
  s.stop("Deploy failed");
2910
2766
  p4.cancel(`Asset upload failed (network): ${errMessage(err)}`);
2911
2767
  process.exit(1);
2912
2768
  }
2913
2769
  s.message(`Deploying to ${appName}.app.space...`);
2914
2770
  }
2771
+ const commitUrl = `${DEPLOY_URL3}/api/deploy/${appId}`;
2772
+ let confirmRename = args.rename === true;
2773
+ const bailDeploy = async (message, stopLabel = "Deploy failed") => {
2774
+ if (uploadId) await abortStagedUpload(appId, uploadId, totalGroups, token);
2775
+ if (stopLabel !== null) s.stop(stopLabel);
2776
+ p4.cancel(message);
2777
+ process.exit(1);
2778
+ };
2915
2779
  const makeForm = () => {
2916
2780
  const form = new FormData();
2917
2781
  form.append("worker", new Blob([workerJs], { type: "application/javascript" }), "worker.js");
@@ -2933,38 +2797,45 @@ var deploy_default = defineCommand8({
2933
2797
  if (extraRoutes.length) {
2934
2798
  form.append("extraRunWorkerFirst", JSON.stringify(extraRoutes));
2935
2799
  }
2936
- if (localSecretsLink.linked) {
2937
- form.append("secretsProjectName", localSecretsLink.projectName);
2938
- }
2800
+ form.append("name", appName);
2801
+ if (confirmRename) form.append("confirmRename", "true");
2939
2802
  return form;
2940
2803
  };
2941
- const commitUrl = `${DEPLOY_URL3}/api/deploy/${appName}`;
2804
+ const postCommit = async () => postWithRetry(
2805
+ commitUrl,
2806
+ () => ({
2807
+ method: "POST",
2808
+ headers: { Authorization: `Bearer ${token}` },
2809
+ body: makeForm()
2810
+ }),
2811
+ // Commit runs a real deploy — don't retry its 5xx (could double-deploy);
2812
+ // only a thrown fetch (dropped connection) is retried, and the staged
2813
+ // groups survive on the worker until this request succeeds.
2814
+ { retryServerErrors: false }
2815
+ );
2942
2816
  let res;
2943
2817
  try {
2944
- res = await postWithRetry(
2945
- commitUrl,
2946
- () => ({
2947
- method: "POST",
2948
- headers: { Authorization: `Bearer ${token}` },
2949
- body: makeForm()
2950
- }),
2951
- // Commit runs a real deploy — don't retry its 5xx (could double-deploy);
2952
- // only a thrown fetch (dropped connection) is retried, and the staged
2953
- // groups survive on the worker until this request succeeds.
2954
- { retryServerErrors: false }
2955
- );
2818
+ res = await postCommit();
2956
2819
  } catch (err) {
2957
- if (uploadId) await abortStagedUpload(appName, uploadId, totalGroups, token);
2958
- s.stop("Deploy failed");
2959
- p4.cancel(`Deploy request failed: ${errMessage(err)}`);
2960
- process.exit(1);
2820
+ await bailDeploy(`Deploy request failed: ${errMessage(err)}`);
2821
+ throw err;
2822
+ }
2823
+ let body = await res.json().catch(() => ({}));
2824
+ if (res.status === 409 && body.code === "rename_required" && !confirmRename) {
2825
+ s.stop("Rename confirmation needed");
2826
+ const yes = await p4.confirm({
2827
+ message: `This deploy renames the app: ${body.fromHost} \u2192 ${body.toHost}. The URL changes and the old one stops serving right away; data, secrets, and collaborators travel with it. (Meant a separate app? Run \`deepspace init --new-id\` instead.) Rename?`
2828
+ });
2829
+ if (p4.isCancel(yes) || !yes) {
2830
+ await bailDeploy("Deploy cancelled.", null);
2831
+ }
2832
+ confirmRename = true;
2833
+ s.start(`Deploying to ${appName}.app.space...`);
2834
+ res = await postCommit();
2835
+ body = await res.json().catch(() => ({}));
2961
2836
  }
2962
- const body = await res.json().catch(() => ({}));
2963
2837
  if (!res.ok || !body.success) {
2964
- if (uploadId) await abortStagedUpload(appName, uploadId, totalGroups, token);
2965
- s.stop("Deploy failed");
2966
- p4.cancel(formatDeployWorkerError(res.status, body.error));
2967
- process.exit(1);
2838
+ await bailDeploy(formatDeployWorkerError(res.status, body.error));
2968
2839
  }
2969
2840
  if (body.onBehalfOfOwner) {
2970
2841
  p4.log.warn(`Deployed on behalf of owner ${body.onBehalfOfOwner}`);
@@ -2978,27 +2849,67 @@ var deploy_default = defineCommand8({
2978
2849
  "First requests may briefly hit the assets transitional page; retry in a minute."
2979
2850
  );
2980
2851
  p4.log.success(`Live at: ${body.url}`);
2981
- await syncSubscriptionPlans(appDir2, appName, token);
2982
- await syncOneTimeProducts(appDir2, appName, token);
2852
+ await migrateLegacyDevVarsSecrets({
2853
+ deployUrl: DEPLOY_URL3,
2854
+ token,
2855
+ appId,
2856
+ envName,
2857
+ devVarsPath,
2858
+ userSecrets,
2859
+ userSecretNames,
2860
+ hadStore: linkedSecrets !== null
2861
+ });
2862
+ await syncSubscriptionPlans(appDir, appId, token);
2863
+ await syncOneTimeProducts(appDir, appId, token);
2983
2864
  p4.outro("Done");
2984
2865
  return;
2985
2866
  }
2986
2867
  }
2987
2868
  s.stop("Deployed!");
2988
2869
  p4.log.success(`Live at: ${body.url}`);
2989
- await syncSubscriptionPlans(appDir2, appName, token);
2990
- await syncOneTimeProducts(appDir2, appName, token);
2870
+ await migrateLegacyDevVarsSecrets({
2871
+ deployUrl: DEPLOY_URL3,
2872
+ token,
2873
+ appId,
2874
+ envName,
2875
+ devVarsPath,
2876
+ userSecrets,
2877
+ userSecretNames,
2878
+ hadStore: linkedSecrets !== null
2879
+ });
2880
+ await syncSubscriptionPlans(appDir, appId, token);
2881
+ await syncOneTimeProducts(appDir, appId, token);
2991
2882
  p4.outro("Done");
2992
2883
  }
2993
2884
  });
2994
- async function syncSubscriptionPlans(appDir2, appName, token) {
2995
- const subsPath = join10(appDir2, "src", "subscriptions.ts");
2885
+ async function migrateLegacyDevVarsSecrets(opts) {
2886
+ const { deployUrl, token, appId, envName, devVarsPath, userSecrets, userSecretNames, hadStore } = opts;
2887
+ if (hadStore || userSecretNames.length === 0) return;
2888
+ const configName = defaultConfigNameForEnv(envName);
2889
+ try {
2890
+ await uploadSecrets(deployUrl, token, appId, configName, userSecrets, false);
2891
+ if (existsSync10(devVarsPath)) {
2892
+ const cleaned = removeDevVarsKeys(readFileSync9(devVarsPath, "utf-8"), new Set(userSecretNames));
2893
+ writeFileSync9(devVarsPath, cleaned.endsWith("\n") ? cleaned : `${cleaned}
2894
+ `);
2895
+ }
2896
+ p4.log.success(
2897
+ `Migrated ${userSecretNames.length} secret${userSecretNames.length === 1 ? "" : "s"} from .dev.vars into the app store (config ${configName}) \u2014 now managed by \`deepspace secrets\`.`
2898
+ );
2899
+ } catch (err) {
2900
+ p4.log.warn(
2901
+ `Couldn't migrate .dev.vars secrets into the app store (${errMessage(err)}); they deployed as bindings for now and the next deploy will retry.`
2902
+ );
2903
+ }
2904
+ }
2905
+ async function syncSubscriptionPlans(appDir, appName, token) {
2906
+ const subsPath = join11(appDir, "src", "subscriptions.ts");
2996
2907
  if (!existsSync10(subsPath)) return;
2997
2908
  let plans;
2998
2909
  try {
2999
2910
  const esbuild = await import("esbuild");
3000
- const outDir = join10(appDir2, ".wrangler", "deploy");
3001
- const outFile = join10(outDir, "subscriptions.bundle.mjs");
2911
+ const outDir = join11(appDir, ".wrangler", "deploy");
2912
+ const outFile = join11(outDir, "subscriptions.bundle.mjs");
3002
2913
  await esbuild.build({
3003
2914
  entryPoints: [subsPath],
3004
2915
  bundle: true,
@@ -3030,7 +2941,7 @@ async function syncSubscriptionPlans(appDir2, appName, token) {
3030
2941
  Authorization: `Bearer ${token}`,
3031
2942
  "Content-Type": "application/json"
3032
2943
  },
3033
- body: JSON.stringify({ appName, plans })
2944
+ body: JSON.stringify({ appId: appName, plans })
3034
2945
  });
3035
2946
  } catch (err) {
3036
2947
  s.stop("Plan sync skipped");
@@ -3139,14 +3050,14 @@ current billing period? (Skip to leave them grandfathered.)`
3139
3050
  }
3140
3051
  }
3141
3052
  }
3142
- async function syncOneTimeProducts(appDir2, appName, token) {
3143
- const productsPath = join10(appDir2, "src", "products.ts");
3053
+ async function syncOneTimeProducts(appDir, appName, token) {
3054
+ const productsPath = join11(appDir, "src", "products.ts");
3144
3055
  if (!existsSync10(productsPath)) return;
3145
3056
  let products;
3146
3057
  try {
3147
3058
  const esbuild = await import("esbuild");
3148
- const outDir = join10(appDir2, ".wrangler", "deploy");
3149
- const outFile = join10(outDir, "products.bundle.mjs");
3059
+ const outDir = join11(appDir, ".wrangler", "deploy");
3060
+ const outFile = join11(outDir, "products.bundle.mjs");
3150
3061
  await esbuild.build({
3151
3062
  entryPoints: [productsPath],
3152
3063
  bundle: true,
@@ -3178,7 +3089,7 @@ async function syncOneTimeProducts(appDir2, appName, token) {
3178
3089
  Authorization: `Bearer ${token}`,
3179
3090
  "Content-Type": "application/json"
3180
3091
  },
3181
- body: JSON.stringify({ appName, products })
3092
+ body: JSON.stringify({ appId: appName, products })
3182
3093
  });
3183
3094
  } catch (err) {
3184
3095
  s?.stop("Product sync skipped");
@@ -3219,9 +3130,9 @@ async function waitForAssetsReady(url, timeoutMs) {
3219
3130
  function collectAssets(dir) {
3220
3131
  const assets = [];
3221
3132
  function walk(d, prefix) {
3222
- for (const entry of readdirSync2(d, { withFileTypes: true })) {
3133
+ for (const entry of readdirSync3(d, { withFileTypes: true })) {
3223
3134
  if (entry.name === ".assetsignore") continue;
3224
- const full = join10(d, entry.name);
3135
+ const full = join11(d, entry.name);
3225
3136
  const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
3226
3137
  if (entry.isDirectory()) {
3227
3138
  walk(full, rel);
@@ -3249,7 +3160,7 @@ var undeploy_default = defineCommand9({
3249
3160
  args: {
3250
3161
  name: {
3251
3162
  type: "positional",
3252
- description: "App name to undeploy (reads from wrangler.toml if omitted)",
3163
+ description: "App id to undeploy (reads DEEPSPACE_APP_ID from wrangler.toml if omitted)",
3253
3164
  required: false
3254
3165
  },
3255
3166
  env: {
@@ -3259,21 +3170,17 @@ var undeploy_default = defineCommand9({
3259
3170
  }
3260
3171
  },
3261
3172
  async run({ args }) {
3262
- let appName = args.name;
3263
3173
  const envName = typeof args.env === "string" && args.env.trim() ? args.env.trim() : void 0;
3264
- if (!appName) {
3265
- if (hasWranglerConfig(process.cwd())) {
3266
- const config = readWranglerConfig(process.cwd());
3267
- const nameRes = resolveAppNameForEnv(config, envName);
3268
- if (!nameRes.ok) {
3269
- p5.cancel(nameRes.reason);
3270
- process.exit(1);
3271
- }
3272
- appName = nameRes.name;
3273
- }
3174
+ let appId = args.name;
3175
+ if (appId && !APP_ID_RE.test(appId)) {
3176
+ p5.cancel(`"${appId}" is not an app id (app_\u2026). Undeploy targets the id \u2014 run from the app directory, or find it in wrangler.toml.`);
3177
+ process.exit(1);
3274
3178
  }
3275
- if (!appName) {
3276
- p5.cancel("Provide an app name or run from a DeepSpace app directory.");
3179
+ if (!appId && hasWranglerConfig(process.cwd())) {
3180
+ appId = readAppId(process.cwd(), envName) ?? void 0;
3181
+ }
3182
+ if (!appId) {
3183
+ p5.cancel("Provide an app id or run from a DeepSpace app directory with a DEEPSPACE_APP_ID.");
3277
3184
  process.exit(1);
3278
3185
  }
3279
3186
  let token;
@@ -3283,10 +3190,10 @@ var undeploy_default = defineCommand9({
3283
3190
  p5.cancel(err instanceof Error ? err.message : String(err));
3284
3191
  process.exit(1);
3285
3192
  }
3286
- p5.intro(`Undeploying ${appName}`);
3193
+ p5.intro(`Undeploying ${appId}`);
3287
3194
  const s = createSpinner();
3288
3195
  s.start("Removing...");
3289
- const res = await fetch(`${DEPLOY_URL4}/api/deploy/${appName}`, {
3196
+ const res = await fetch(`${DEPLOY_URL4}/api/deploy/${appId}`, {
3290
3197
  method: "DELETE",
3291
3198
  headers: { Authorization: `Bearer ${token}` }
3292
3199
  });
@@ -3297,7 +3204,10 @@ var undeploy_default = defineCommand9({
3297
3204
  process.exit(1);
3298
3205
  }
3299
3206
  s.stop("Removed");
3300
- p5.outro(`${appName}.app.space has been taken down`);
3207
+ const hosts = body.releasedHosts ?? [];
3208
+ p5.outro(
3209
+ hosts.length ? `${hosts.join(", ")} taken down. The app keeps its id \u2014 redeploy to bring it back.` : `App ${appId} taken down.`
3210
+ );
3301
3211
  }
3302
3212
  });
3303
3213
 
@@ -3360,12 +3270,12 @@ var whoami_default = defineCommand10({
3360
3270
  // src/cli/commands/add.ts
3361
3271
  import { defineCommand as defineCommand11 } from "citty";
3362
3272
  import { existsSync as existsSync11 } from "fs";
3363
- import { resolve as resolve8, join as join11 } from "path";
3273
+ import { resolve as resolve8, join as join12 } from "path";
3364
3274
  import { execSync as execSync3 } from "child_process";
3365
3275
  function findScript(from) {
3366
- const inNodeModules = join11(from, "node_modules", "deepspace", "scripts", "add-feature.cjs");
3276
+ const inNodeModules = join12(from, "node_modules", "deepspace", "scripts", "add-feature.cjs");
3367
3277
  if (existsSync11(inNodeModules)) return inNodeModules;
3368
- const inDotDeepspace = join11(from, ".deepspace", "scripts", "add-feature.cjs");
3278
+ const inDotDeepspace = join12(from, ".deepspace", "scripts", "add-feature.cjs");
3369
3279
  if (existsSync11(inDotDeepspace)) return inDotDeepspace;
3370
3280
  return null;
3371
3281
  }
@@ -3398,14 +3308,14 @@ var add_default = defineCommand11({
3398
3308
  }
3399
3309
  },
3400
3310
  run({ args }) {
3401
- const appDir2 = resolve8(args.dir ?? ".");
3402
- ensureInstallReady(appDir2);
3403
- const script = findScript(appDir2);
3311
+ const appDir = resolve8(args.dir ?? ".");
3312
+ ensureInstallReady(appDir);
3313
+ const script = findScript(appDir);
3404
3314
  if (!script) {
3405
3315
  console.error("Could not find the deepspace feature installer.");
3406
3316
  console.error("Looked in:");
3407
- console.error(` ${join11(appDir2, "node_modules", "deepspace", "scripts", "add-feature.cjs")}`);
3408
- console.error(` ${join11(appDir2, ".deepspace", "scripts", "add-feature.cjs")}`);
3317
+ console.error(` ${join12(appDir, "node_modules", "deepspace", "scripts", "add-feature.cjs")}`);
3318
+ console.error(` ${join12(appDir, ".deepspace", "scripts", "add-feature.cjs")}`);
3409
3319
  console.error("Are you in a DeepSpace app directory? Try `npm install` first.");
3410
3320
  process.exit(1);
3411
3321
  }
@@ -3415,13 +3325,13 @@ var add_default = defineCommand11({
3415
3325
  } else if (args.info) {
3416
3326
  passArgs.push("--info", args.info);
3417
3327
  } else if (args.feature) {
3418
- passArgs.push(args.feature, appDir2);
3328
+ passArgs.push(args.feature, appDir);
3419
3329
  } else {
3420
3330
  passArgs.push("--help");
3421
3331
  }
3422
3332
  execSync3(`node ${JSON.stringify(script)} ${passArgs.map((a) => JSON.stringify(a)).join(" ")}`, {
3423
3333
  stdio: "inherit",
3424
- cwd: appDir2
3334
+ cwd: appDir
3425
3335
  });
3426
3336
  }
3427
3337
  });
@@ -3430,26 +3340,26 @@ var add_default = defineCommand11({
3430
3340
  import { defineCommand as defineCommand12 } from "citty";
3431
3341
  import spawn2 from "cross-spawn";
3432
3342
  import { mkdirSync as mkdirSync5 } from "fs";
3433
- import { join as join12 } from "path";
3343
+ import { join as join13 } from "path";
3434
3344
  import { homedir as homedir5 } from "os";
3435
3345
  var SKILL_REPO = "deepdotspace/deepspace-skill";
3436
3346
  function runSkillsAdd(extraArgs) {
3437
- return new Promise((resolve11) => {
3347
+ return new Promise((resolve10) => {
3438
3348
  const child = spawn2(
3439
3349
  "npx",
3440
3350
  ["-y", "skills@latest", "add", SKILL_REPO, ...extraArgs],
3441
3351
  { stdio: "inherit" }
3442
3352
  );
3443
- child.on("close", (code) => resolve11(code ?? 1));
3353
+ child.on("close", (code) => resolve10(code ?? 1));
3444
3354
  child.on("error", (err) => {
3445
3355
  console.error(`Failed to invoke npx: ${err.message}`);
3446
3356
  console.error("Is Node.js installed and on PATH?");
3447
- resolve11(1);
3357
+ resolve10(1);
3448
3358
  });
3449
3359
  });
3450
3360
  }
3451
3361
  function precreateClaudeDir(baseDir) {
3452
- mkdirSync5(join12(baseDir, ".claude"), { recursive: true });
3362
+ mkdirSync5(join13(baseDir, ".claude"), { recursive: true });
3453
3363
  }
3454
3364
  var add = defineCommand12({
3455
3365
  meta: {
@@ -3507,6 +3417,13 @@ var skills_default = defineCommand12({
3507
3417
  import { defineCommand as defineCommand13 } from "citty";
3508
3418
 
3509
3419
  // src/cli/lib/api.ts
3420
+ var ApiError = class extends Error {
3421
+ constructor(message, status3, code) {
3422
+ super(message);
3423
+ this.status = status3;
3424
+ this.code = code;
3425
+ }
3426
+ };
3510
3427
  async function apiFetch(baseUrl, token, path, init) {
3511
3428
  const res = await fetch(`${baseUrl}${path}`, {
3512
3429
  ...init,
@@ -3519,13 +3436,16 @@ async function apiFetch(baseUrl, token, path, init) {
3519
3436
  const text2 = await res.text();
3520
3437
  if (!res.ok) {
3521
3438
  let msg = text2;
3439
+ let code;
3522
3440
  try {
3523
- msg = JSON.parse(text2).error ?? text2;
3441
+ const body = JSON.parse(text2);
3442
+ msg = body.error ?? text2;
3443
+ code = body.code;
3524
3444
  } catch {
3525
3445
  }
3526
- throw new Error(`API ${path} (${res.status}): ${msg}`);
3446
+ throw new ApiError(`API ${path} (${res.status}): ${msg}`, res.status, code);
3527
3447
  }
3528
- return JSON.parse(text2);
3448
+ return text2 ? JSON.parse(text2) : {};
3529
3449
  }
3530
3450
 
3531
3451
  // src/cli/commands/domain.ts
@@ -3556,11 +3476,11 @@ async function confirm3(message) {
3556
3476
  throw new Error(`${message} \u2014 pass --yes to confirm non-interactively`);
3557
3477
  }
3558
3478
  process.stdout.write(`${message} [y/N] `);
3559
- return new Promise((resolve11) => {
3479
+ return new Promise((resolve10) => {
3560
3480
  process.stdin.setEncoding("utf-8");
3561
3481
  process.stdin.once("data", (data) => {
3562
3482
  const answer = data.toString().trim().toLowerCase();
3563
- resolve11(answer === "y" || answer === "yes");
3483
+ resolve10(answer === "y" || answer === "yes");
3564
3484
  });
3565
3485
  });
3566
3486
  }
@@ -3637,7 +3557,7 @@ var buy = defineCommand13({
3637
3557
  }
3638
3558
  const checkout = await api(token, "/api/domains/checkout", {
3639
3559
  method: "POST",
3640
- body: JSON.stringify({ domain: args.domain, appName })
3560
+ body: JSON.stringify({ domain: args.domain, appId: appName })
3641
3561
  });
3642
3562
  if (args.json) {
3643
3563
  process.stdout.write(
@@ -3781,7 +3701,7 @@ var attach = defineCommand13({
3781
3701
  `/api/domains/${found.id}/reattach`,
3782
3702
  {
3783
3703
  method: "POST",
3784
- body: JSON.stringify({ appName: targetApp })
3704
+ body: JSON.stringify({ appId: targetApp })
3785
3705
  }
3786
3706
  );
3787
3707
  if (args.json) {
@@ -3856,36 +3776,27 @@ var domain_default = defineCommand13({
3856
3776
 
3857
3777
  // src/cli/commands/collaborators.ts
3858
3778
  import { defineCommand as defineCommand14 } from "citty";
3779
+
3780
+ // src/server/auth/testAccounts.ts
3781
+ var TEST_ACCOUNT_EMAIL_SUFFIX = "@deepspace.test";
3782
+ function isTestAccountEmail(email) {
3783
+ return !!email && email.trim().toLowerCase().endsWith(TEST_ACCOUNT_EMAIL_SUFFIX);
3784
+ }
3785
+
3786
+ // src/cli/commands/collaborators.ts
3859
3787
  var API_URL3 = process.env.DEEPSPACE_API_URL ?? PLATFORM_URLS.api;
3860
- async function api2(token, path, init) {
3861
- const res = await fetch(`${API_URL3}${path}`, {
3862
- ...init,
3863
- headers: {
3864
- Authorization: `Bearer ${token}`,
3865
- "Content-Type": "application/json",
3866
- ...init?.headers ?? {}
3867
- }
3868
- });
3869
- const text2 = await res.text();
3870
- if (!res.ok) {
3871
- let msg = text2;
3872
- try {
3873
- msg = JSON.parse(text2).error ?? text2;
3874
- } catch {
3875
- }
3876
- throw new Error(`API ${path} (${res.status}): ${msg}`);
3877
- }
3878
- return text2 ? JSON.parse(text2) : {};
3788
+ function api2(token, path, init) {
3789
+ return apiFetch(API_URL3, token, path, init);
3879
3790
  }
3880
3791
  var list3 = defineCommand14({
3881
3792
  meta: { name: "list", description: "List collaborators on your app" },
3882
3793
  args: {
3883
- app: { type: "string", description: "App name (defaults to ./wrangler.toml)" },
3794
+ app: { type: "string", alias: "a", description: "App id (defaults to ./wrangler.toml)" },
3884
3795
  json: { type: "boolean", description: "Emit JSON" }
3885
3796
  },
3886
3797
  async run({ args }) {
3887
3798
  const token = await ensureToken();
3888
- const app = requireAppName(args.app);
3799
+ const app = requireAppIdArg(args.app);
3889
3800
  const { collaborators } = await api2(
3890
3801
  token,
3891
3802
  `/api/app-collaborators/${encodeURIComponent(app)}`
@@ -3911,12 +3822,18 @@ var add2 = defineCommand14({
3911
3822
  description: "Collaborator email (must already be a DeepSpace user)",
3912
3823
  required: true
3913
3824
  },
3914
- app: { type: "string", description: "App name (defaults to ./wrangler.toml)" },
3825
+ app: { type: "string", alias: "a", description: "App id (defaults to ./wrangler.toml)" },
3915
3826
  json: { type: "boolean", description: "Emit JSON" }
3916
3827
  },
3917
3828
  async run({ args }) {
3918
3829
  const token = await ensureToken();
3919
- const app = requireAppName(args.app);
3830
+ const app = requireAppIdArg(args.app);
3831
+ if (isTestAccountEmail(args.email)) {
3832
+ console.error(
3833
+ "Test accounts (@deepspace.test) cannot be added as collaborators. Use a real DeepSpace account."
3834
+ );
3835
+ process.exit(1);
3836
+ }
3920
3837
  const { collaborator } = await api2(
3921
3838
  token,
3922
3839
  `/api/app-collaborators/${encodeURIComponent(app)}`,
@@ -3933,12 +3850,12 @@ var remove = defineCommand14({
3933
3850
  meta: { name: "remove", description: "Remove a collaborator from your app" },
3934
3851
  args: {
3935
3852
  email: { type: "positional", description: "Collaborator email", required: true },
3936
- app: { type: "string", description: "App name (defaults to ./wrangler.toml)" },
3853
+ app: { type: "string", alias: "a", description: "App id (defaults to ./wrangler.toml)" },
3937
3854
  json: { type: "boolean", description: "Emit JSON" }
3938
3855
  },
3939
3856
  async run({ args }) {
3940
3857
  const token = await ensureToken();
3941
- const app = requireAppName(args.app);
3858
+ const app = requireAppIdArg(args.app);
3942
3859
  const { collaborators } = await api2(
3943
3860
  token,
3944
3861
  `/api/app-collaborators/${encodeURIComponent(app)}`
@@ -3966,12 +3883,86 @@ var collaborators_default = defineCommand14({
3966
3883
  subCommands: { list: list3, add: add2, remove }
3967
3884
  });
3968
3885
 
3969
- // src/cli/commands/integrations.ts
3886
+ // src/cli/commands/transfer.ts
3970
3887
  import { defineCommand as defineCommand15 } from "citty";
3888
+ var API_URL4 = process.env.DEEPSPACE_API_URL ?? PLATFORM_URLS.api;
3889
+ var DEPLOY_URL5 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
3890
+ var offer = defineCommand15({
3891
+ meta: { name: "offer", description: "Offer this app to another DeepSpace user" },
3892
+ args: {
3893
+ email: { type: "positional", description: "Recipient email", required: true },
3894
+ app: { type: "string", alias: "a", description: "App id (defaults to ./wrangler.toml)" }
3895
+ },
3896
+ async run({ args }) {
3897
+ const token = await ensureToken();
3898
+ const app = requireAppIdArg(args.app);
3899
+ const { expiresAt } = await apiFetch(
3900
+ API_URL4,
3901
+ token,
3902
+ `/api/app-transfers/${encodeURIComponent(app)}`,
3903
+ { method: "POST", body: JSON.stringify({ email: args.email }) }
3904
+ );
3905
+ console.log(
3906
+ `\u2713 Offered ${app} to ${args.email} (expires ${expiresAt}).
3907
+ They accept with: deepspace transfer accept --app ${app}`
3908
+ );
3909
+ }
3910
+ });
3911
+ var status2 = defineCommand15({
3912
+ meta: { name: "status", description: "Show the pending transfer offer, if any" },
3913
+ args: {
3914
+ app: { type: "string", alias: "a", description: "App id (defaults to ./wrangler.toml)" }
3915
+ },
3916
+ async run({ args }) {
3917
+ const token = await ensureToken();
3918
+ const app = requireAppIdArg(args.app);
3919
+ const { transfer } = await apiFetch(API_URL4, token, `/api/app-transfers/${encodeURIComponent(app)}`);
3920
+ if (!transfer) {
3921
+ console.log(`No pending transfer for ${app}.`);
3922
+ return;
3923
+ }
3924
+ console.log(`Pending: ${app} \u2192 ${transfer.toEmailDisplay} (expires ${transfer.expiresAt}).`);
3925
+ }
3926
+ });
3927
+ var accept = defineCommand15({
3928
+ meta: { name: "accept", description: "Accept a transfer offered to you" },
3929
+ args: {
3930
+ app: { type: "string", alias: "a", description: "App id of the offered app", required: true }
3931
+ },
3932
+ async run({ args }) {
3933
+ const token = await ensureToken();
3934
+ const app = requireAppIdArg(args.app);
3935
+ await apiFetch(DEPLOY_URL5, token, `/api/apps/${encodeURIComponent(app)}/transfer/accept`, {
3936
+ method: "POST"
3937
+ });
3938
+ console.log(
3939
+ `\u2713 You now own ${app}. Run \`deepspace init\` in a fresh clone (or set DEEPSPACE_APP_ID = "${app}" in wrangler.toml) and deploy.`
3940
+ );
3941
+ }
3942
+ });
3943
+ var cancel4 = defineCommand15({
3944
+ meta: { name: "cancel", description: "Cancel/decline the pending offer (either party)" },
3945
+ args: {
3946
+ app: { type: "string", alias: "a", description: "App id (defaults to ./wrangler.toml)" }
3947
+ },
3948
+ async run({ args }) {
3949
+ const token = await ensureToken();
3950
+ const app = requireAppIdArg(args.app);
3951
+ await apiFetch(API_URL4, token, `/api/app-transfers/${encodeURIComponent(app)}`, { method: "DELETE" });
3952
+ console.log(`\u2713 Transfer offer for ${app} cancelled.`);
3953
+ }
3954
+ });
3955
+ var transfer_default = defineCommand15({
3956
+ meta: { name: "transfer", description: "Transfer app ownership to another user" },
3957
+ subCommands: { offer, status: status2, accept, cancel: cancel4 }
3958
+ });
3959
+
3960
+ // src/cli/commands/integrations.ts
3961
+ import { defineCommand as defineCommand16 } from "citty";
3971
3962
 
3972
3963
  // src/cli/commands/_invoke-impl.ts
3973
3964
  import { readFileSync as readFileSync10 } from "fs";
3974
- var API_URL4 = process.env.DEEPSPACE_API_URL ?? PLATFORM_URLS.api;
3965
+ var API_URL5 = process.env.DEEPSPACE_API_URL ?? PLATFORM_URLS.api;
3975
3966
  var DEFAULT_TIMEOUT_MS = 12e4;
3976
3967
  function parseTarget(target) {
3977
3968
  if (!target || typeof target !== "string") {
@@ -4013,7 +4004,7 @@ function flushAndExit(code) {
4013
4004
  });
4014
4005
  }
4015
4006
  async function fetchCatalog(opts = {}) {
4016
- const url = opts.summary ? `${API_URL4}/api/integrations?summary=1` : `${API_URL4}/api/integrations`;
4007
+ const url = opts.summary ? `${API_URL5}/api/integrations?summary=1` : `${API_URL5}/api/integrations`;
4017
4008
  const res = await fetch(url);
4018
4009
  if (!res.ok) {
4019
4010
  throw new Error(`Failed to fetch integration catalog (${res.status})`);
@@ -4101,7 +4092,7 @@ async function runInvoke(args) {
4101
4092
  const startedAt = Date.now();
4102
4093
  let res;
4103
4094
  try {
4104
- res = await fetch(`${API_URL4}/api/integrations/${integration}/${endpoint}`, {
4095
+ res = await fetch(`${API_URL5}/api/integrations/${integration}/${endpoint}`, {
4105
4096
  method: "POST",
4106
4097
  headers: {
4107
4098
  "Content-Type": "application/json",
@@ -4152,7 +4143,7 @@ async function runInvoke(args) {
4152
4143
  }
4153
4144
 
4154
4145
  // src/cli/commands/integrations.ts
4155
- var list4 = defineCommand15({
4146
+ var list4 = defineCommand16({
4156
4147
  meta: {
4157
4148
  name: "list",
4158
4149
  description: "List all available integrations"
@@ -4173,7 +4164,7 @@ var list4 = defineCommand15({
4173
4164
  }
4174
4165
  }
4175
4166
  });
4176
- var info = defineCommand15({
4167
+ var info = defineCommand16({
4177
4168
  meta: {
4178
4169
  name: "info",
4179
4170
  description: "Print schema + example body for an integration endpoint"
@@ -4199,7 +4190,7 @@ var info = defineCommand15({
4199
4190
  }
4200
4191
  }
4201
4192
  });
4202
- var invoke = defineCommand15({
4193
+ var invoke = defineCommand16({
4203
4194
  meta: {
4204
4195
  name: "invoke",
4205
4196
  description: "Invoke a platform integration as the logged-in user"
@@ -4252,7 +4243,7 @@ var invoke = defineCommand15({
4252
4243
  }
4253
4244
  }
4254
4245
  });
4255
- var integrations_default = defineCommand15({
4246
+ var integrations_default = defineCommand16({
4256
4247
  meta: {
4257
4248
  name: "integrations",
4258
4249
  description: "List, inspect, and invoke platform integrations"
@@ -4265,8 +4256,8 @@ var integrations_default = defineCommand15({
4265
4256
  });
4266
4257
 
4267
4258
  // src/cli/commands/invoke.ts
4268
- import { defineCommand as defineCommand16 } from "citty";
4269
- var invoke_default = defineCommand16({
4259
+ import { defineCommand as defineCommand17 } from "citty";
4260
+ var invoke_default = defineCommand17({
4270
4261
  meta: {
4271
4262
  name: "invoke",
4272
4263
  description: "Invoke a platform integration as the logged-in user"
@@ -4339,13 +4330,13 @@ var invoke_default = defineCommand16({
4339
4330
  });
4340
4331
 
4341
4332
  // src/cli/commands/library.ts
4342
- import { defineCommand as defineCommand17 } from "citty";
4333
+ import { defineCommand as defineCommand18 } from "citty";
4343
4334
  import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
4344
- import { join as join13, resolve as resolve9 } from "path";
4345
- import { parse as parseToml3 } from "smol-toml";
4335
+ import { join as join14, resolve as resolve9 } from "path";
4336
+ import { parse as parseToml4 } from "smol-toml";
4346
4337
  import * as p6 from "@clack/prompts";
4347
4338
  var DEFAULT_LIBRARY_APP = "deepdotspace-site";
4348
- var publish = defineCommand17({
4339
+ var publish = defineCommand18({
4349
4340
  meta: {
4350
4341
  name: "publish",
4351
4342
  description: "Publish the current app to the DeepSpace library"
@@ -4418,12 +4409,12 @@ var publish = defineCommand17({
4418
4409
  intro6("Publishing to the DeepSpace library");
4419
4410
  let appName = typeof args.app === "string" && args.app.trim() ? args.app.trim() : void 0;
4420
4411
  if (!appName) {
4421
- const appDir2 = resolve9(typeof args.dir === "string" && args.dir ? args.dir : ".");
4422
- const wranglerPath = join13(appDir2, "wrangler.toml");
4412
+ const appDir = resolve9(typeof args.dir === "string" && args.dir ? args.dir : ".");
4413
+ const wranglerPath = join14(appDir, "wrangler.toml");
4423
4414
  if (!existsSync12(wranglerPath)) {
4424
4415
  return die("No --app provided and no wrangler.toml found. Either pass --app <name> or run from your app's directory.");
4425
4416
  }
4426
- const cfg = parseToml3(readFileSync11(wranglerPath, "utf-8"));
4417
+ const cfg = parseToml4(readFileSync11(wranglerPath, "utf-8"));
4427
4418
  const nameRes = resolveAppName(cfg.name);
4428
4419
  if (!nameRes.ok) return die(`wrangler.toml: ${nameRes.reason}`);
4429
4420
  appName = nameRes.name;
@@ -4484,7 +4475,7 @@ var publish = defineCommand17({
4484
4475
  p6.outro("Done");
4485
4476
  }
4486
4477
  });
4487
- var unpublish = defineCommand17({
4478
+ var unpublish = defineCommand18({
4488
4479
  meta: {
4489
4480
  name: "unpublish",
4490
4481
  description: "Remove a library entry (owner or admin only)"
@@ -4543,7 +4534,7 @@ var unpublish = defineCommand17({
4543
4534
  p6.outro("Done");
4544
4535
  }
4545
4536
  });
4546
- var library_default = defineCommand17({
4537
+ var library_default = defineCommand18({
4547
4538
  meta: {
4548
4539
  name: "library",
4549
4540
  description: "Manage your DeepSpace library entries"
@@ -4555,13 +4546,13 @@ var library_default = defineCommand17({
4555
4546
  });
4556
4547
 
4557
4548
  // src/cli/commands/feedback.ts
4558
- import { defineCommand as defineCommand18 } from "citty";
4549
+ import { defineCommand as defineCommand19 } from "citty";
4559
4550
  import * as p7 from "@clack/prompts";
4560
4551
  import { existsSync as existsSync13, readFileSync as readFileSync12 } from "fs";
4561
- import { dirname as dirname3, join as join14 } from "path";
4552
+ import { dirname as dirname3, join as join15 } from "path";
4562
4553
  import { fileURLToPath } from "url";
4563
4554
  import { release } from "os";
4564
- var API_URL5 = process.env.DEEPSPACE_API_URL ?? PLATFORM_URLS.api;
4555
+ var API_URL6 = process.env.DEEPSPACE_API_URL ?? PLATFORM_URLS.api;
4565
4556
  var TYPES = ["bug", "feature", "other"];
4566
4557
  var TYPE_LABELS = {
4567
4558
  bug: "Bug report",
@@ -4571,7 +4562,7 @@ var TYPE_LABELS = {
4571
4562
  var MAX_TITLE_LEN = 200;
4572
4563
  var MAX_BODY_LEN = 1e4;
4573
4564
  var MAX_CONTEXT_LEN = 200;
4574
- var api3 = (token, path, init) => apiFetch(API_URL5, token, path, init);
4565
+ var api3 = (token, path, init) => apiFetch(API_URL6, token, path, init);
4575
4566
  function normalizeType(raw) {
4576
4567
  const value = (raw ?? "bug").toLowerCase();
4577
4568
  if (TYPES.includes(value)) return value;
@@ -4581,8 +4572,8 @@ function readCliVersion() {
4581
4572
  try {
4582
4573
  const here = dirname3(fileURLToPath(import.meta.url));
4583
4574
  const candidates = [
4584
- join14(here, "..", "package.json"),
4585
- join14(here, "..", "..", "..", "package.json")
4575
+ join15(here, "..", "package.json"),
4576
+ join15(here, "..", "..", "..", "package.json")
4586
4577
  ];
4587
4578
  for (const path of candidates) {
4588
4579
  if (!existsSync13(path)) continue;
@@ -4633,7 +4624,7 @@ function buildFeedbackPayload(input) {
4633
4624
  ...input.context
4634
4625
  };
4635
4626
  }
4636
- var feedback_default = defineCommand18({
4627
+ var feedback_default = defineCommand19({
4637
4628
  meta: {
4638
4629
  name: "feedback",
4639
4630
  description: "Submit a bug report, feature request, or feedback to DeepSpace"
@@ -4669,7 +4660,7 @@ var feedback_default = defineCommand18({
4669
4660
  },
4670
4661
  async run({ args }) {
4671
4662
  const interactive = process.stdin.isTTY && !args.json;
4672
- const fail = (message) => {
4663
+ const fail2 = (message) => {
4673
4664
  if (args.json) {
4674
4665
  process.stdout.write(JSON.stringify({ ok: false, error: message }) + "\n");
4675
4666
  } else {
@@ -4681,12 +4672,12 @@ var feedback_default = defineCommand18({
4681
4672
  try {
4682
4673
  type = normalizeType(args.type ? String(args.type) : void 0);
4683
4674
  } catch (err) {
4684
- return fail(err.message);
4675
+ return fail2(err.message);
4685
4676
  }
4686
4677
  let title = args.title ? String(args.title) : "";
4687
4678
  let body = args.message ? String(args.message) : "";
4688
4679
  if ((!title || !body) && !interactive) {
4689
- return fail(
4680
+ return fail2(
4690
4681
  'Provide a title and --message to submit non-interactively, e.g. `deepspace feedback "Title" --message "Details"`.'
4691
4682
  );
4692
4683
  }
@@ -4748,7 +4739,7 @@ var feedback_default = defineCommand18({
4748
4739
  context: collectContext()
4749
4740
  });
4750
4741
  } catch (err) {
4751
- return fail(err.message);
4742
+ return fail2(err.message);
4752
4743
  }
4753
4744
  if (interactive && !args.yes) {
4754
4745
  p7.note(
@@ -4761,1011 +4752,395 @@ Context: deepspace ${payload.cliVersion} \xB7 ${payload.nodeVersion} \xB7 ${payl
4761
4752
  "Review"
4762
4753
  );
4763
4754
  const ok = await p7.confirm({ message: "Send this to the DeepSpace team?" });
4764
- if (p7.isCancel(ok) || !ok) {
4765
- p7.cancel("Not sent.");
4766
- process.exit(0);
4767
- }
4768
- }
4769
- let token;
4770
- try {
4771
- token = await ensureToken();
4772
- } catch (err) {
4773
- return fail(err.message ?? "Not signed in. Run `deepspace login`.");
4774
- }
4775
- let result;
4776
- try {
4777
- result = await api3(token, "/api/feedback", {
4778
- method: "POST",
4779
- body: JSON.stringify(payload)
4780
- });
4781
- } catch (err) {
4782
- return fail(err.message);
4783
- }
4784
- if (args.json) {
4785
- process.stdout.write(JSON.stringify({ ok: true, report: result.report }) + "\n");
4786
- return;
4787
- }
4788
- if (interactive) {
4789
- p7.outro(`Thanks! Submitted as ${result.report.id}.`);
4790
- } else {
4791
- console.log(`Submitted feedback ${result.report.id} (${result.report.type}).`);
4792
- }
4793
- console.log(`Track triage status at ${DASHBOARD_URL}.`);
4794
- }
4795
- });
4796
-
4797
- // src/cli/commands/secrets.ts
4798
- import { defineCommand as defineCommand19 } from "citty";
4799
- import { existsSync as existsSync14, readFileSync as readFileSync13, rmSync as rmSync3 } from "fs";
4800
- import { join as join15, resolve as resolve10 } from "path";
4801
- var DEPLOY_URL5 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
4802
- function appDir(args) {
4803
- const dir = typeof args.dir === "string" && args.dir ? args.dir : typeof args.scope === "string" && args.scope ? args.scope : ".";
4804
- return resolve10(dir);
4805
- }
4806
- function wranglerEnv(args) {
4807
- return typeof args.env === "string" && args.env.trim() ? args.env.trim() : void 0;
4808
- }
4809
- function targetFromArgs(args) {
4810
- const dir = appDir(args);
4811
- const target = resolveSecretsTarget(dir, {
4812
- project: typeof args.project === "string" && args.project ? args.project : void 0,
4813
- config: typeof args.config === "string" && args.config ? args.config : void 0,
4814
- wranglerEnv: wranglerEnv(args)
4815
- });
4816
- return { appDir: dir, target };
4817
- }
4818
- function scopedArgs() {
4819
- return {
4820
- dir: { type: "string", description: "App directory (default: current directory)", required: false },
4821
- scope: { type: "string", description: "Directory scope; Doppler-compatible alias for --dir", required: false },
4822
- env: { type: "string", alias: "e", description: "Advanced: wrangler.toml [env.<name>] cache/link slot", required: false },
4823
- json: { type: "boolean", description: "Emit JSON", default: false }
4824
- };
4825
- }
4826
- function commonArgs() {
4827
- return {
4828
- ...scopedArgs(),
4829
- project: { type: "string", alias: "p", description: "Secrets project (default: linked project or wrangler name; does not link)", required: false },
4830
- config: { type: "string", alias: "c", description: "Secrets config (Doppler-compatible; default: linked config or prd)", required: false }
4831
- };
4832
- }
4833
- function setupArgs() {
4834
- return {
4835
- ...scopedArgs(),
4836
- project: { type: "string", alias: "p", description: "Secrets project to link (default: linked project or wrangler name)", required: false },
4837
- config: { type: "string", alias: "c", description: "Secrets config to link (default: linked config or prd)", required: false },
4838
- "no-interactive": { type: "boolean", description: "Accepted for Doppler CLI compatibility; DeepSpace setup never prompts", default: false },
4839
- "no-save-token": { type: "boolean", description: "Accepted for Doppler CLI compatibility; DeepSpace does not save a token here", default: false }
4840
- };
4841
- }
4842
- async function refreshCache(token, dir, target) {
4843
- const values = await fetchSecretsValues(
4844
- DEPLOY_URL5,
4845
- token,
4846
- target.projectName,
4847
- target.configName,
4848
- appScopedSecretsApiOptions(dir, target)
4849
- );
4850
- writeSecretsCacheFile(dir, values, target);
4851
- return Object.keys(values).length;
4852
- }
4853
- async function refreshCacheIfLinked(token, dir, target) {
4854
- if (!secretsTargetMatchesLink(dir, target)) return null;
4855
- return refreshCache(token, dir, target);
4856
- }
4857
- function writeLinkForTarget(dir, target, projectOverride) {
4858
- writeWranglerSecretsLink(dir, target, { projectOverride });
4859
- }
4860
- function redeployReminder(target) {
4861
- const envFlag = target.wranglerEnv ? ` --env ${target.wranglerEnv}` : "";
4862
- console.log(`Redeploy for production to receive these secrets: npx deepspace deploy${envFlag}`);
4863
- }
4864
- function unlinkedTargetNotice(target) {
4865
- const envFlag = target.wranglerEnv ? ` --env ${target.wranglerEnv}` : "";
4866
- console.log(
4867
- `Not linked for dev/test/deploy; run npx deepspace secrets setup --project ${target.projectName} --config ${target.configName}${envFlag}.`
4868
- );
4869
- }
4870
- function cacheJson(count) {
4871
- return { linked: count !== null, cachedSecrets: count };
4872
- }
4873
- function updateRequiredSecretsIfTopLevelLinked(dir, target, keys, opts = {}) {
4874
- if (target.wranglerEnv || !secretsTargetMatchesLink(dir, target)) return null;
4875
- return updateWranglerRequiredSecrets(dir, keys, opts);
4876
- }
4877
- function removeRequiredSecretsIfTopLevelLinked(dir, target, keys) {
4878
- if (target.wranglerEnv || !secretsTargetMatchesLink(dir, target)) return null;
4879
- return removeWranglerRequiredSecrets(dir, keys);
4880
- }
4881
- function requiredSecretsJson(keys) {
4882
- return keys ? { requiredSecrets: keys } : {};
4883
- }
4884
- function requiredSecretsNotice(keys) {
4885
- if (!keys) return;
4886
- console.log(`Updated wrangler.toml detected-secret advisory (${keys.length} name${keys.length === 1 ? "" : "s"}).`);
4887
- }
4888
- async function ensureLinkedRemoteTarget(dir, target) {
4889
- let token;
4890
- try {
4891
- token = await ensureToken();
4892
- } catch (err) {
4893
- return {
4894
- status: "unchecked",
4895
- message: err instanceof Error ? err.message : String(err)
4896
- };
4897
- }
4898
- try {
4899
- const configs2 = await listConfigs(
4900
- DEPLOY_URL5,
4901
- token,
4902
- target.projectName,
4903
- appScopedSecretsApiOptions(dir, target)
4904
- );
4905
- if (configs2.some((config) => config.name === target.configName)) {
4906
- return { status: "ok" };
4907
- }
4908
- } catch (err) {
4909
- if (!(err instanceof SecretsApiError && err.status === 404)) {
4910
- return {
4911
- status: "unchecked",
4912
- message: err instanceof Error ? err.message : String(err)
4913
- };
4914
- }
4915
- }
4916
- try {
4917
- await createConfig(
4918
- DEPLOY_URL5,
4919
- token,
4920
- target.projectName,
4921
- target.configName,
4922
- void 0,
4923
- appScopedSecretsApiOptions(dir, target)
4924
- );
4925
- return { status: "created" };
4926
- } catch (err) {
4927
- return {
4928
- status: "unchecked",
4929
- message: `Remote config ${target.projectName}/${target.configName} was not found and could not be created: ${err instanceof Error ? err.message : String(err)}`
4930
- };
4931
- }
4932
- }
4933
- async function bestEffortSetServerSecretsLink(dir, target) {
4934
- try {
4935
- const token = await ensureToken();
4936
- await setServerSecretsLink(
4937
- DEPLOY_URL5,
4938
- token,
4939
- resolveSecretsAppName(dir, target.wranglerEnv),
4940
- target.projectName
4941
- );
4942
- } catch {
4943
- }
4944
- }
4945
- async function bestEffortClearServerSecretsLink(dir, targetEnv) {
4946
- try {
4947
- const token = await ensureToken();
4948
- await clearServerSecretsLink(DEPLOY_URL5, token, resolveSecretsAppName(dir, targetEnv));
4949
- } catch {
4950
- }
4951
- }
4952
- async function importLegacyDevVarsSecrets(dir, target, remoteCheck) {
4953
- if (remoteCheck.status === "unchecked") return null;
4954
- const legacyPath = target.wranglerEnv ? join15(dir, `.dev.vars.${target.wranglerEnv}`) : join15(dir, ".dev.vars");
4955
- if (!existsSync14(legacyPath)) return null;
4956
- try {
4957
- const custom = extractCustomDevVars(
4958
- stripGeneratedSecretsCache2(readFileSync13(legacyPath, "utf-8"))
4959
- );
4960
- if (!custom.trim()) return null;
4961
- const parsed = parseDevVars(custom);
4962
- const secrets = {};
4963
- const skipped = [];
4964
- for (const [key, value] of Object.entries(parsed)) {
4965
- try {
4966
- validateSecretName(key);
4967
- validateSecretValue(key, value);
4968
- secrets[key] = value;
4969
- } catch {
4970
- skipped.push(key);
4971
- }
4972
- }
4973
- const keys = Object.keys(secrets).sort();
4974
- if (keys.length === 0) return null;
4975
- const token = await ensureToken();
4976
- if (remoteCheck.status === "ok") {
4977
- const existing = await listSecrets(
4978
- DEPLOY_URL5,
4979
- token,
4980
- target.projectName,
4981
- target.configName,
4982
- appScopedSecretsApiOptions(dir, target)
4983
- );
4984
- if (existing.secrets.length > 0) {
4985
- return { status: "config-not-empty", existingCount: existing.secrets.length, keys };
4986
- }
4987
- }
4988
- await uploadSecrets(
4989
- DEPLOY_URL5,
4990
- token,
4991
- target.projectName,
4992
- target.configName,
4993
- secrets,
4994
- false,
4995
- appScopedSecretsApiOptions(dir, target)
4996
- );
4997
- let cleanedLegacyFile = false;
4998
- try {
4999
- const cleaned = removeDevVarsKeys(
5000
- stripGeneratedSecretsCache2(readFileSync13(legacyPath, "utf-8")),
5001
- new Set(keys)
5002
- );
5003
- if (target.wranglerEnv && !extractCustomDevVars(cleaned).trim()) {
5004
- rmSync3(legacyPath, { force: true });
5005
- } else {
5006
- writeSecretFileSync(legacyPath, cleaned ? `${cleaned}
5007
- ` : "");
4755
+ if (p7.isCancel(ok) || !ok) {
4756
+ p7.cancel("Not sent.");
4757
+ process.exit(0);
5008
4758
  }
5009
- cleanedLegacyFile = true;
5010
- } catch {
5011
4759
  }
5012
- await refreshCacheIfLinked(token, dir, target);
5013
- const requiredSecrets = updateRequiredSecretsIfTopLevelLinked(dir, target, keys);
5014
- return { status: "imported", keys, skipped: skipped.sort(), requiredSecrets, cleanedLegacyFile };
5015
- } catch (err) {
5016
- return { status: "error", message: err instanceof Error ? err.message : String(err) };
5017
- }
5018
- }
5019
- function printLegacySecretsImport(result, target) {
5020
- if (!result) return;
5021
- const configRef = `${target.projectName}/${target.configName}`;
5022
- if (result.status === "imported") {
5023
- console.log(
5024
- `Imported ${result.keys.length} legacy secret${result.keys.length === 1 ? "" : "s"} from .dev.vars into ${configRef}: ${result.keys.join(", ")}`
5025
- );
5026
- if (result.cleanedLegacyFile) {
5027
- console.log("Removed the migrated lines from the local file; the remote store owns them now.");
4760
+ let token;
4761
+ try {
4762
+ token = await ensureToken();
4763
+ } catch (err) {
4764
+ return fail2(err.message ?? "Not signed in. Run `deepspace login`.");
5028
4765
  }
5029
- if (result.skipped.length > 0) {
5030
- console.log(`Skipped SDK-managed/invalid keys (left in place): ${result.skipped.join(", ")}`);
4766
+ let result;
4767
+ try {
4768
+ result = await api3(token, "/api/feedback", {
4769
+ method: "POST",
4770
+ body: JSON.stringify(payload)
4771
+ });
4772
+ } catch (err) {
4773
+ return fail2(err.message);
5031
4774
  }
5032
- requiredSecretsNotice(result.requiredSecrets);
5033
- return;
5034
- }
5035
- if (result.status === "config-not-empty") {
5036
- console.log(
5037
- `Found ${result.keys.length} legacy secret${result.keys.length === 1 ? "" : "s"} in .dev.vars, but ${configRef} already has ${result.existingCount} \u2014 not imported. Merge manually with npx deepspace secrets upload.`
5038
- );
5039
- return;
5040
- }
5041
- console.warn(`Warning: legacy .dev.vars secrets were not imported: ${result.message}`);
5042
- }
5043
- function printLinkRemoteWarning(check) {
5044
- if (check.status !== "unchecked") return;
5045
- console.warn(`Warning: Linked locally, but the remote config was not checked: ${check.message}`);
5046
- }
5047
- function positionals(args, firstName) {
5048
- const raw = Array.isArray(args._) ? args._ : [];
5049
- if (raw.length > 0) return raw.map(String);
5050
- const first = args[firstName];
5051
- return typeof first === "string" && first ? [first] : [];
5052
- }
5053
- function rawPositionals(rawArgs, argsDef) {
5054
- const camel = (name) => name.replace(/-([a-z0-9])/g, (_, ch) => ch.toUpperCase());
5055
- const stringFlags = /* @__PURE__ */ new Set();
5056
- const booleanFlags = /* @__PURE__ */ new Set();
5057
- for (const [name, def] of Object.entries(argsDef)) {
5058
- if (def.type === "positional") continue;
5059
- const flags = def.type === "boolean" ? booleanFlags : stringFlags;
5060
- flags.add(name);
5061
- flags.add(camel(name));
5062
- for (const alias of Array.isArray(def.alias) ? def.alias : def.alias ? [def.alias] : []) {
5063
- flags.add(alias);
5064
- flags.add(camel(alias));
4775
+ if (args.json) {
4776
+ process.stdout.write(JSON.stringify({ ok: true, report: result.report }) + "\n");
4777
+ return;
5065
4778
  }
5066
- }
5067
- const out = [];
5068
- for (let i = 0; i < rawArgs.length; i++) {
5069
- const token = rawArgs[i];
5070
- if (token === "--") {
5071
- out.push(...rawArgs.slice(i + 1));
5072
- break;
5073
- }
5074
- if (token.length > 1 && token.startsWith("-")) {
5075
- const body = token.startsWith("--") ? token.slice(2) : token.slice(1);
5076
- const eq = body.indexOf("=");
5077
- const name = eq === -1 ? body : body.slice(0, eq);
5078
- if (booleanFlags.has(name) || name.startsWith("no-") && booleanFlags.has(name.slice(3))) {
5079
- continue;
5080
- }
5081
- if (stringFlags.has(name)) {
5082
- if (eq === -1) i++;
5083
- continue;
5084
- }
4779
+ if (interactive) {
4780
+ p7.outro(`Thanks! Submitted as ${result.report.id}.`);
4781
+ } else {
4782
+ console.log(`Submitted feedback ${result.report.id} (${result.report.type}).`);
5085
4783
  }
5086
- out.push(token);
5087
- }
5088
- return out;
5089
- }
5090
- function hasRawFlag(rawArgs, name) {
5091
- if (!Array.isArray(rawArgs)) return false;
5092
- for (const token of rawArgs) {
5093
- if (token === "--") return false;
5094
- if (token === `--${name}`) return true;
4784
+ console.log(`Track triage status at ${DASHBOARD_URL}.`);
5095
4785
  }
5096
- return false;
5097
- }
5098
- function downloadFormat(value) {
5099
- const raw = typeof value === "string" && value ? value : "json";
5100
- if (raw === "json" || raw === "dotnet-json" || raw === "env" || raw === "yaml" || raw === "docker" || raw === "env-no-quotes") {
5101
- return raw;
5102
- }
5103
- throw new Error("Unsupported format. Use one of: json, dotnet-json, env, yaml, docker, env-no-quotes.");
5104
- }
5105
- function resolveSecretsSetupArgs(args) {
5106
- const dir = appDir(args);
5107
- const project = typeof args.project === "string" && args.project ? args.project : void 0;
5108
- const config = typeof args.config === "string" && args.config ? args.config : void 0;
5109
- const target = resolveSecretsTarget(dir, {
5110
- project,
5111
- config,
5112
- wranglerEnv: wranglerEnv(args)
5113
- });
5114
- return {
5115
- appDir: dir,
5116
- target,
5117
- projectOverride: !!project && !!target.wranglerEnv
5118
- };
5119
- }
5120
- function resolveConfigureUnsetArgs(args) {
5121
- const names = positionals(args, "name");
5122
- if (names.length === 0) {
5123
- throw new Error("At least one setting is required. Use `npx deepspace secrets configure unset project config`.");
4786
+ });
4787
+
4788
+ // src/cli/commands/secrets.ts
4789
+ import { defineCommand as defineCommand20 } from "citty";
4790
+ import { readFileSync as readFileSync13 } from "fs";
4791
+ var DEPLOY_URL6 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
4792
+ var COMMON_ARGS = {
4793
+ app: {
4794
+ type: "string",
4795
+ alias: "a",
4796
+ description: "App id (default: DEEPSPACE_APP_ID from the nearest wrangler.toml)",
4797
+ required: false
4798
+ },
4799
+ config: {
4800
+ type: "string",
4801
+ alias: "c",
4802
+ description: 'Config name (default: the --env name, or "prd")',
4803
+ required: false
4804
+ },
4805
+ env: {
4806
+ type: "string",
4807
+ alias: "e",
4808
+ description: "wrangler.toml [env.<name>] slot \u2014 selects that env\u2019s app id and config",
4809
+ required: false
5124
4810
  }
5125
- const unset = [];
5126
- for (const name of names) {
5127
- if (name !== "project" && name !== "config") {
5128
- throw new Error(`Unsupported configure unset option "${name}". Use project and/or config.`);
4811
+ };
4812
+ async function resolveTarget(args) {
4813
+ const wranglerEnv = args.env?.trim() || void 0;
4814
+ let appId = args.app?.trim();
4815
+ if (appId && !APP_ID_RE.test(appId)) {
4816
+ throw new Error(`Invalid app id "${appId}" \u2014 expected app_<26 chars>.`);
4817
+ }
4818
+ if (!appId) {
4819
+ const appDir = findAppDir();
4820
+ appId = appDir && readAppId(appDir, wranglerEnv) || void 0;
4821
+ if (!appId) {
4822
+ throw new Error(
4823
+ "No app id. Run from an app directory whose wrangler.toml carries DEEPSPACE_APP_ID (`deepspace init` mints one), or pass --app <appId>."
4824
+ );
5129
4825
  }
5130
- if (!unset.includes(name)) unset.push(name);
5131
4826
  }
5132
- return {
5133
- appDir: appDir(args),
5134
- unset,
5135
- wranglerEnv: wranglerEnv(args)
5136
- };
4827
+ const configName = validateConfigName(args.config?.trim() || defaultConfigNameForEnv(wranglerEnv));
4828
+ const token = await ensureToken();
4829
+ return { appId, configName, token };
4830
+ }
4831
+ function fail(err) {
4832
+ console.error(err instanceof Error ? err.message : String(err));
4833
+ process.exit(1);
5137
4834
  }
5138
- var list5 = defineCommand19({
4835
+ var list5 = defineCommand20({
5139
4836
  meta: { name: "list", description: "List masked secrets in a config" },
5140
4837
  args: {
5141
- ...commonArgs(),
5142
- "only-names": { type: "boolean", description: "Only print secret names; omit values and metadata", default: false },
5143
- raw: { type: "boolean", description: "Accepted for Doppler CLI compatibility; DeepSpace has no variable interpolation", default: false },
5144
- type: { type: "boolean", description: "Accepted for Doppler CLI compatibility", default: false },
5145
- visibility: { type: "boolean", description: "Accepted for Doppler CLI compatibility", default: false }
4838
+ ...COMMON_ARGS,
4839
+ "only-names": {
4840
+ type: "boolean",
4841
+ description: "Only print secret names; omit values and metadata",
4842
+ default: false
4843
+ },
4844
+ json: { type: "boolean", description: "Machine-readable output", default: false }
5146
4845
  },
5147
4846
  async run({ args }) {
5148
4847
  try {
5149
- const token = await ensureToken();
5150
- const { appDir: dir, target } = targetFromArgs(args);
5151
- const result = await listSecrets(
5152
- DEPLOY_URL5,
5153
- token,
5154
- target.projectName,
5155
- target.configName,
5156
- appScopedSecretsApiOptions(dir, target)
5157
- );
4848
+ const t = await resolveTarget(args);
4849
+ const { secrets } = await listSecrets(DEPLOY_URL6, t.token, t.appId, t.configName);
5158
4850
  if (args.json) {
5159
- process.stdout.write(JSON.stringify(result, null, 2) + "\n");
4851
+ console.log(JSON.stringify({ appId: t.appId, config: t.configName, secrets }, null, 2));
5160
4852
  return;
5161
4853
  }
5162
- if (result.secrets.length === 0) {
5163
- console.log(`No secrets in ${target.projectName}/${target.configName}.`);
4854
+ if (secrets.length === 0) {
4855
+ console.log(`No secrets in ${t.configName}.`);
5164
4856
  return;
5165
4857
  }
5166
- if (args.onlyNames) {
5167
- for (const item of result.secrets) console.log(item.key);
5168
- return;
5169
- }
5170
- for (const item of result.secrets) {
5171
- console.log(`${item.key.padEnd(32)} ${item.masked} v${item.version} ${item.updatedAt}`);
4858
+ for (const s of secrets) {
4859
+ console.log(args["only-names"] ? s.key : `${s.key} (v${s.version}, ${s.valueBytes}B, ${s.updatedAt})`);
5172
4860
  }
5173
4861
  } catch (err) {
5174
- console.error(err instanceof Error ? err.message : String(err));
5175
- process.exit(1);
4862
+ fail(err);
5176
4863
  }
5177
4864
  }
5178
4865
  });
5179
- var SET_ARGS = {
5180
- ...commonArgs(),
5181
- secret: { type: "positional", description: "KEY=value or KEY value", required: true },
5182
- "no-interactive": { type: "boolean", description: "Accepted for Doppler CLI compatibility", default: false },
5183
- raw: { type: "boolean", description: "Accepted for Doppler CLI compatibility; DeepSpace has no variable interpolation", default: false },
5184
- type: { type: "string", description: "Accepted for Doppler CLI compatibility", required: false },
5185
- visibility: { type: "string", description: "Accepted for Doppler CLI compatibility", required: false }
5186
- };
5187
- var set = defineCommand19({
5188
- meta: { name: "set", description: "Set remote secrets from KEY=value or KEY value" },
5189
- args: SET_ARGS,
5190
- async run({ args, rawArgs }) {
4866
+ var set = defineCommand20({
4867
+ meta: { name: "set", description: "Set secrets from KEY=value pairs" },
4868
+ args: {
4869
+ ...COMMON_ARGS,
4870
+ secret: { type: "positional", description: "KEY=value (repeatable)", required: true }
4871
+ },
4872
+ async run({ args }) {
5191
4873
  try {
5192
- const token = await ensureToken();
5193
- const { appDir: dir, target } = targetFromArgs(args);
5194
- const inputs = Array.isArray(rawArgs) ? rawPositionals(rawArgs, SET_ARGS) : positionals(args, "secret");
5195
- const secrets = parseSetInputs(inputs);
5196
- await uploadSecrets(
5197
- DEPLOY_URL5,
5198
- token,
5199
- target.projectName,
5200
- target.configName,
5201
- secrets,
5202
- false,
5203
- appScopedSecretsApiOptions(dir, target)
4874
+ const t = await resolveTarget(args);
4875
+ const pairs = [args.secret, ...args._ ?? []].filter(
4876
+ (v) => typeof v === "string" && v.length > 0
5204
4877
  );
5205
- const count = await refreshCacheIfLinked(token, dir, target);
5206
- const keys = Object.keys(secrets).sort();
5207
- const requiredSecrets = updateRequiredSecretsIfTopLevelLinked(dir, target, keys);
5208
- if (args.json) {
5209
- process.stdout.write(JSON.stringify({ ok: true, keys, target, ...cacheJson(count), ...requiredSecretsJson(requiredSecrets) }) + "\n");
5210
- return;
4878
+ const secrets = {};
4879
+ for (const pair of pairs) {
4880
+ const eq = pair.indexOf("=");
4881
+ if (eq <= 0) throw new Error(`Expected KEY=value, got "${pair}"`);
4882
+ const key = validateSecretName(pair.slice(0, eq));
4883
+ const value = pair.slice(eq + 1);
4884
+ validateSecretValue(key, value);
4885
+ secrets[key] = value;
5211
4886
  }
5212
- console.log(`Set ${keys.length} secret${keys.length === 1 ? "" : "s"} in ${target.projectName}/${target.configName}.`);
5213
- requiredSecretsNotice(requiredSecrets);
5214
- if (count === null) {
5215
- unlinkedTargetNotice(target);
4887
+ if (Object.keys(secrets).length === 1) {
4888
+ const [[k, v]] = Object.entries(secrets);
4889
+ await setSecret(DEPLOY_URL6, t.token, t.appId, t.configName, k, v);
5216
4890
  } else {
5217
- console.log(`Refreshed ${count} cached secret${count === 1 ? "" : "s"}.`);
5218
- redeployReminder(target);
4891
+ await uploadSecrets(DEPLOY_URL6, t.token, t.appId, t.configName, secrets, false);
5219
4892
  }
4893
+ console.log(`Set ${Object.keys(secrets).length} secret${Object.keys(secrets).length === 1 ? "" : "s"} in ${t.configName}.`);
5220
4894
  } catch (err) {
5221
- console.error(err instanceof Error ? err.message : String(err));
5222
- process.exit(1);
4895
+ fail(err);
5223
4896
  }
5224
4897
  }
5225
4898
  });
5226
- var get = defineCommand19({
5227
- meta: { name: "get", description: "Get one or more secrets; pass --plain to print values" },
4899
+ var get = defineCommand20({
4900
+ meta: { name: "get", description: "Get a secret; pass --plain to print the value" },
5228
4901
  args: {
5229
- ...commonArgs(),
5230
- key: { type: "positional", description: "Secret key", required: true },
5231
- plain: { type: "boolean", description: "Print plaintext value", default: false },
5232
- "no-exit-on-missing-secret": { type: "boolean", description: "Do not exit when a requested secret is missing", default: false },
5233
- raw: { type: "boolean", description: "Accepted for Doppler CLI compatibility; DeepSpace has no variable interpolation", default: false },
5234
- copy: { type: "boolean", description: "Accepted for Doppler CLI compatibility; clipboard copy is not implemented", default: false },
5235
- type: { type: "boolean", description: "Accepted for Doppler CLI compatibility", default: false },
5236
- visibility: { type: "boolean", description: "Accepted for Doppler CLI compatibility", default: false }
4902
+ ...COMMON_ARGS,
4903
+ key: { type: "positional", description: "Secret name", required: true },
4904
+ plain: { type: "boolean", description: "Print the plaintext value", default: false }
5237
4905
  },
5238
4906
  async run({ args }) {
5239
4907
  try {
5240
- const token = await ensureToken();
5241
- const { appDir: dir, target } = targetFromArgs(args);
5242
- const keys = positionals(args, "key").map((key) => validateSecretName(key));
4908
+ const t = await resolveTarget(args);
4909
+ const key = validateSecretName(args.key);
5243
4910
  if (args.plain) {
5244
- const values = {};
5245
- const missing2 = [];
5246
- for (const key of keys) {
5247
- try {
5248
- values[key] = await getSecretPlain(
5249
- DEPLOY_URL5,
5250
- token,
5251
- target.projectName,
5252
- target.configName,
5253
- key,
5254
- appScopedSecretsApiOptions(dir, target)
5255
- );
5256
- } catch (err) {
5257
- if (!args.noExitOnMissingSecret) throw err;
5258
- missing2.push(key);
5259
- }
5260
- }
5261
- if (args.json) {
5262
- process.stdout.write(JSON.stringify({ secrets: values, missing: missing2 }, null, 2) + "\n");
5263
- return;
5264
- }
5265
- for (const key of keys) if (Object.prototype.hasOwnProperty.call(values, key)) process.stdout.write(values[key] + "\n");
5266
- return;
5267
- }
5268
- const result = await listSecrets(
5269
- DEPLOY_URL5,
5270
- token,
5271
- target.projectName,
5272
- target.configName,
5273
- appScopedSecretsApiOptions(dir, target)
5274
- );
5275
- const found = result.secrets.filter((item) => keys.includes(item.key));
5276
- const foundKeys = new Set(found.map((item) => item.key));
5277
- const missing = keys.filter((key) => !foundKeys.has(key));
5278
- if (missing.length > 0 && !args.noExitOnMissingSecret) {
5279
- throw new Error(`Secret "${missing[0]}" not found in ${target.projectName}/${target.configName}.`);
5280
- }
5281
- if (args.json) {
5282
- process.stdout.write(JSON.stringify({ secrets: found, missing }, null, 2) + "\n");
4911
+ const { value } = await getSecretPlain(DEPLOY_URL6, t.token, t.appId, t.configName, key);
4912
+ process.stdout.write(value + "\n");
5283
4913
  return;
5284
4914
  }
5285
- for (const item of found) process.stdout.write(`${item.key} ${item.masked} v${item.version}
5286
- `);
4915
+ const { secrets } = await listSecrets(DEPLOY_URL6, t.token, t.appId, t.configName);
4916
+ const item = secrets.find((s) => s.key === key);
4917
+ if (!item) fail(new Error(`Secret "${key}" not found in ${t.configName}`));
4918
+ console.log(`${item.key} (v${item.version}, ${item.valueBytes}B, ${item.updatedAt})`);
5287
4919
  } catch (err) {
5288
- console.error(err instanceof Error ? err.message : String(err));
5289
- process.exit(1);
4920
+ fail(err);
5290
4921
  }
5291
4922
  }
5292
4923
  });
5293
- var del2 = defineCommand19({
5294
- meta: { name: "delete", description: "Delete one or more remote secrets" },
4924
+ var del2 = defineCommand20({
4925
+ meta: { name: "delete", description: "Delete one or more secrets" },
5295
4926
  args: {
5296
- ...commonArgs(),
5297
- key: { type: "positional", description: "Secret key", required: true },
5298
- yes: { type: "boolean", alias: "y", description: "Proceed without confirmation; accepted for Doppler CLI compatibility", default: false },
5299
- raw: { type: "boolean", description: "Accepted for Doppler CLI compatibility; DeepSpace has no variable interpolation", default: false }
4927
+ ...COMMON_ARGS,
4928
+ key: { type: "positional", description: "Secret name (repeatable)", required: true }
5300
4929
  },
5301
4930
  async run({ args }) {
5302
4931
  try {
5303
- const token = await ensureToken();
5304
- const { appDir: dir, target } = targetFromArgs(args);
5305
- const keys = positionals(args, "key").map((key) => validateSecretName(key));
5306
- for (const key of keys) {
5307
- await deleteSecret(
5308
- DEPLOY_URL5,
5309
- token,
5310
- target.projectName,
5311
- target.configName,
5312
- key,
5313
- appScopedSecretsApiOptions(dir, target)
5314
- );
5315
- }
5316
- const count = await refreshCacheIfLinked(token, dir, target);
5317
- const requiredSecrets = removeRequiredSecretsIfTopLevelLinked(dir, target, keys);
5318
- if (args.json) {
5319
- process.stdout.write(JSON.stringify({ ok: true, deleted: keys, target, ...cacheJson(count), ...requiredSecretsJson(requiredSecrets) }) + "\n");
5320
- return;
5321
- }
5322
- console.log(`Deleted ${keys.length} secret${keys.length === 1 ? "" : "s"} from ${target.projectName}/${target.configName}.`);
5323
- requiredSecretsNotice(requiredSecrets);
5324
- if (count === null) {
5325
- unlinkedTargetNotice(target);
5326
- } else {
5327
- console.log(`Refreshed ${count} cached secret${count === 1 ? "" : "s"}.`);
5328
- redeployReminder(target);
5329
- }
5330
- } catch (err) {
5331
- console.error(err instanceof Error ? err.message : String(err));
5332
- process.exit(1);
5333
- }
5334
- }
5335
- });
5336
- var pull = defineCommand19({
5337
- meta: { name: "pull", description: "Refresh .dev.vars* only when the selected config is linked" },
5338
- args: commonArgs(),
5339
- async run({ args }) {
5340
- try {
5341
- const token = await ensureToken();
5342
- const { appDir: dir, target } = targetFromArgs(args);
5343
- const values = await fetchSecretsValues(
5344
- DEPLOY_URL5,
5345
- token,
5346
- target.projectName,
5347
- target.configName,
5348
- appScopedSecretsApiOptions(dir, target)
4932
+ const t = await resolveTarget(args);
4933
+ const keys = [args.key, ...args._ ?? []].filter(
4934
+ (v) => typeof v === "string" && v.length > 0
5349
4935
  );
5350
- const count = Object.keys(values).length;
5351
- const linked = secretsTargetMatchesLink(dir, target);
5352
- if (linked) writeSecretsCacheFile(dir, values, target);
5353
- if (args.json) {
5354
- process.stdout.write(JSON.stringify({ ok: true, target, linked, cachedSecrets: linked ? count : null }) + "\n");
5355
- return;
4936
+ for (const key of keys) {
4937
+ await deleteSecret(DEPLOY_URL6, t.token, t.appId, t.configName, validateSecretName(key));
5356
4938
  }
5357
- console.log(`${linked ? "Pulled" : "Read"} ${count} secret${count === 1 ? "" : "s"} from ${target.projectName}/${target.configName}.`);
5358
- if (!linked) unlinkedTargetNotice(target);
4939
+ console.log(`Deleted ${keys.length} secret${keys.length === 1 ? "" : "s"} from ${t.configName}.`);
5359
4940
  } catch (err) {
5360
- console.error(err instanceof Error ? err.message : String(err));
5361
- process.exit(1);
4941
+ fail(err);
5362
4942
  }
5363
4943
  }
5364
4944
  });
5365
- var download = defineCommand19({
5366
- meta: { name: "download", description: "Download a config's secrets in a Doppler-compatible format" },
4945
+ var upload = defineCommand20({
4946
+ meta: { name: "upload", description: "Upload secrets from a dotenv or JSON file" },
5367
4947
  args: {
5368
- ...commonArgs(),
5369
- file: { type: "positional", description: "Output filepath; omit when using --no-file", required: false },
5370
- format: { type: "string", description: "Output format: json, dotnet-json, env, yaml, docker, env-no-quotes", required: false },
5371
- "no-file": { type: "boolean", description: "Print secrets to stdout instead of writing a file", default: false },
5372
- raw: { type: "boolean", description: "Accepted for Doppler CLI compatibility; DeepSpace has no variable interpolation", default: false },
5373
- "no-cache": { type: "boolean", description: "Accepted for Doppler CLI compatibility; DeepSpace does not use a download fallback cache", default: false },
5374
- "no-fallback": { type: "boolean", description: "Accepted for Doppler CLI compatibility; DeepSpace does not use fallback files", default: false },
5375
- offline: { type: "boolean", description: "Accepted for Doppler CLI compatibility; offline fallback files are not supported", default: false }
5376
- },
5377
- async run({ args, rawArgs }) {
5378
- try {
5379
- const token = await ensureToken();
5380
- const { appDir: dir, target } = targetFromArgs(args);
5381
- const values = await fetchSecretsValues(
5382
- DEPLOY_URL5,
5383
- token,
5384
- target.projectName,
5385
- target.configName,
5386
- appScopedSecretsApiOptions(dir, target)
5387
- );
5388
- const output = formatSecretsDownload(values, downloadFormat(args.format));
5389
- const file = typeof args.file === "string" && args.file ? args.file : void 0;
5390
- const noFile = Boolean(args.noFile) || hasRawFlag(rawArgs, "no-file");
5391
- if (noFile || !file) {
5392
- if (!noFile && !file) {
5393
- throw new Error("Provide an output filepath or pass --no-file to print to stdout.");
5394
- }
5395
- process.stdout.write(output);
5396
- return;
5397
- }
5398
- writeSecretFileSync(resolve10(file), output);
5399
- if (args.json) {
5400
- process.stdout.write(JSON.stringify({ ok: true, path: resolve10(file), target, secretCount: Object.keys(values).length }) + "\n");
5401
- return;
5402
- }
5403
- console.log(`Downloaded ${Object.keys(values).length} secret${Object.keys(values).length === 1 ? "" : "s"} to ${file}.`);
5404
- } catch (err) {
5405
- console.error(err instanceof Error ? err.message : String(err));
5406
- process.exit(1);
4948
+ ...COMMON_ARGS,
4949
+ file: { type: "positional", description: "Path to a dotenv or JSON file (- for stdin)", required: true },
4950
+ replace: {
4951
+ type: "boolean",
4952
+ description: "Replace the whole config (delete keys missing from the file)",
4953
+ default: false
5407
4954
  }
5408
- }
5409
- });
5410
- var upload = defineCommand19({
5411
- meta: { name: "upload", description: "Upload remote secrets from a dotenv or JSON file" },
5412
- args: {
5413
- ...commonArgs(),
5414
- file: { type: "positional", description: "Path to dotenv/JSON file", required: true },
5415
- replace: { type: "boolean", description: "Delete secrets absent from the upload", default: false },
5416
- raw: { type: "boolean", description: "Accepted for Doppler CLI compatibility; DeepSpace has no variable interpolation", default: false }
5417
4955
  },
5418
4956
  async run({ args }) {
5419
4957
  try {
5420
- const token = await ensureToken();
5421
- const { appDir: dir, target } = targetFromArgs(args);
5422
- const file = String(args.file);
5423
- const content = readExistingFile(file);
4958
+ const t = await resolveTarget(args);
4959
+ const content = args.file === "-" ? readFileSync13(0, "utf-8") : readFileSync13(args.file, "utf-8");
5424
4960
  const secrets = parseSecretsUpload(content);
5425
- await uploadSecrets(
5426
- DEPLOY_URL5,
5427
- token,
5428
- target.projectName,
5429
- target.configName,
5430
- secrets,
5431
- !!args.replace,
5432
- appScopedSecretsApiOptions(dir, target)
5433
- );
5434
- const count = await refreshCacheIfLinked(token, dir, target);
5435
- const requiredSecrets = updateRequiredSecretsIfTopLevelLinked(dir, target, Object.keys(secrets), {
5436
- replace: !!args.replace
5437
- });
5438
- if (args.json) {
5439
- process.stdout.write(JSON.stringify({ ok: true, uploaded: Object.keys(secrets), target, ...cacheJson(count), ...requiredSecretsJson(requiredSecrets) }) + "\n");
5440
- return;
5441
- }
5442
- console.log(`Uploaded ${Object.keys(secrets).length} secret${Object.keys(secrets).length === 1 ? "" : "s"} to ${target.projectName}/${target.configName}.`);
5443
- requiredSecretsNotice(requiredSecrets);
5444
- if (count === null) {
5445
- unlinkedTargetNotice(target);
5446
- } else {
5447
- console.log(`Refreshed ${count} cached secret${count === 1 ? "" : "s"}.`);
5448
- redeployReminder(target);
5449
- }
5450
- } catch (err) {
5451
- console.error(err instanceof Error ? err.message : String(err));
5452
- process.exit(1);
5453
- }
5454
- }
5455
- });
5456
- var configsList = defineCommand19({
5457
- meta: { name: "list", description: "List configs in a secrets project" },
5458
- args: commonArgs(),
5459
- async run({ args }) {
5460
- try {
5461
- const token = await ensureToken();
5462
- const { appDir: dir, target } = targetFromArgs(args);
5463
- const configs2 = await listConfigs(
5464
- DEPLOY_URL5,
5465
- token,
5466
- target.projectName,
5467
- appScopedSecretsApiOptions(dir, target)
5468
- );
5469
- if (args.json) {
5470
- process.stdout.write(JSON.stringify({ projectName: target.projectName, configs: configs2 }, null, 2) + "\n");
5471
- return;
5472
- }
5473
- if (configs2.length === 0) {
5474
- console.log(`No configs in ${target.projectName}.`);
5475
- return;
5476
- }
5477
- for (const cfg of configs2) {
5478
- console.log(`${cfg.name.padEnd(24)} ${cfg.secretCount ?? 0} secret${cfg.secretCount === 1 ? "" : "s"} ${cfg.updatedAt}`);
5479
- }
4961
+ if (Object.keys(secrets).length === 0) fail(new Error("No secrets found in the input."));
4962
+ await uploadSecrets(DEPLOY_URL6, t.token, t.appId, t.configName, secrets, args.replace);
4963
+ console.log(`Uploaded ${Object.keys(secrets).length} secrets to ${t.configName}.`);
5480
4964
  } catch (err) {
5481
- console.error(err instanceof Error ? err.message : String(err));
5482
- process.exit(1);
4965
+ fail(err);
5483
4966
  }
5484
4967
  }
5485
4968
  });
5486
- var configsCreate = defineCommand19({
5487
- meta: { name: "create", description: "Create a remote config without linking it" },
4969
+ var download = defineCommand20({
4970
+ meta: { name: "download", description: "Download a config\u2019s secrets (dotenv/json/shell)" },
5488
4971
  args: {
5489
- ...commonArgs(),
5490
- configName: { type: "positional", description: "Config name", required: false },
5491
- name: { type: "string", description: "Config name; Doppler-compatible --name flag", required: false }
4972
+ ...COMMON_ARGS,
4973
+ format: {
4974
+ type: "string",
4975
+ description: "dotenv (default) | json | shell",
4976
+ default: "dotenv"
4977
+ }
5492
4978
  },
5493
4979
  async run({ args }) {
5494
4980
  try {
5495
- if (args.copyFrom !== void 0 || args["copy-from"] !== void 0) {
5496
- throw new Error("Unsupported flag: --copy-from. Use `npx deepspace secrets configs clone <source-config> --name <new-config>`.");
5497
- }
5498
- const token = await ensureToken();
5499
- const dir = appDir(args);
5500
- const projectName = validateProjectName(args.project || resolveSecretsTarget(dir, { wranglerEnv: wranglerEnv(args) }).projectName);
5501
- const rawName = typeof args.config === "string" && args.config ? args.config : typeof args.configName === "string" && args.configName ? args.configName : args.name;
5502
- if (typeof rawName !== "string" || !rawName) throw new Error("Config name required");
5503
- const configName = validateConfigName(rawName);
5504
- const target = { projectName, configName, wranglerEnv: wranglerEnv(args) };
5505
- const result = await createConfig(
5506
- DEPLOY_URL5,
5507
- token,
5508
- projectName,
5509
- configName,
5510
- void 0,
5511
- appScopedSecretsApiOptions(dir, target)
5512
- );
5513
- const count = await refreshCacheIfLinked(token, dir, target);
5514
- if (args.json) {
5515
- process.stdout.write(JSON.stringify({ ...result, target, ...cacheJson(count) }) + "\n");
5516
- return;
5517
- }
5518
- if (result.created) {
5519
- console.log(`Created ${projectName}/${configName}.`);
5520
- } else {
5521
- console.log(`Config ${projectName}/${configName} already exists.`);
5522
- }
5523
- if (count === null) {
5524
- unlinkedTargetNotice(target);
5525
- } else {
5526
- console.log(`Refreshed ${count} cached secret${count === 1 ? "" : "s"}.`);
4981
+ const t = await resolveTarget(args);
4982
+ const format = args.format;
4983
+ if (!["dotenv", "json", "shell"].includes(format)) {
4984
+ fail(new Error(`Unknown format "${args.format}" \u2014 use dotenv, json, or shell.`));
5527
4985
  }
4986
+ const { secrets } = await fetchSecretsValues(DEPLOY_URL6, t.token, t.appId, t.configName);
4987
+ process.stdout.write(formatSecretsDownload(secrets, format));
5528
4988
  } catch (err) {
5529
- console.error(err instanceof Error ? err.message : String(err));
5530
- process.exit(1);
4989
+ fail(err);
5531
4990
  }
5532
4991
  }
5533
4992
  });
5534
- function resolveConfigCloneArgs(args) {
5535
- const dir = appDir(args);
5536
- const baseTarget = resolveSecretsTarget(dir, {
5537
- project: typeof args.project === "string" && args.project ? args.project : void 0,
5538
- wranglerEnv: wranglerEnv(args)
5539
- });
5540
- const sources = positionals(args, "sourceConfig");
5541
- if (sources.length > 1) throw new Error("Only one source config can be provided.");
5542
- const sourceConfigName = validateConfigName(
5543
- sources[0] ?? (typeof args.config === "string" && args.config ? args.config : baseTarget.configName)
5544
- );
5545
- if (typeof args.name !== "string" || !args.name) {
5546
- throw new Error("Destination config name required; pass --name <new-config>.");
5547
- }
5548
- const target = {
5549
- projectName: baseTarget.projectName,
5550
- configName: validateConfigName(args.name),
5551
- wranglerEnv: baseTarget.wranglerEnv
5552
- };
5553
- return { appDir: dir, sourceConfigName, target };
5554
- }
5555
- var configsClone = defineCommand19({
5556
- meta: { name: "clone", description: "Create a remote config by copying another config server-side" },
5557
- args: {
5558
- ...commonArgs(),
5559
- sourceConfig: { type: "positional", description: "Source config name (default: --config or linked/default config)", required: false },
5560
- name: { type: "string", description: "Destination config name", required: true }
5561
- },
4993
+ var pull = defineCommand20({
4994
+ meta: { name: "pull", description: "Refresh the .dev.vars cache from the app store" },
4995
+ args: { ...COMMON_ARGS },
5562
4996
  async run({ args }) {
5563
4997
  try {
5564
- const token = await ensureToken();
5565
- const { appDir: dir, sourceConfigName, target } = resolveConfigCloneArgs(args);
5566
- const result = await createConfig(
5567
- DEPLOY_URL5,
5568
- token,
5569
- target.projectName,
5570
- target.configName,
5571
- sourceConfigName,
5572
- appScopedSecretsApiOptions(dir, target)
5573
- );
5574
- const count = await refreshCacheIfLinked(token, dir, target);
5575
- if (args.json) {
5576
- process.stdout.write(JSON.stringify({ ...result, target, sourceConfigName, ...cacheJson(count) }) + "\n");
4998
+ const wranglerEnv = args.env?.trim() || void 0;
4999
+ const appDir = findAppDir();
5000
+ if (!appDir) fail(new Error("Run from a DeepSpace app directory (one containing wrangler.toml)."));
5001
+ const t = await resolveTarget(args);
5002
+ const ownerId = decodeJwtPayload(t.token).sub;
5003
+ const pulled = await pullAppSecretsCache(DEPLOY_URL6, t.token, t.appId, t.configName);
5004
+ if (!pulled) {
5005
+ console.log(`No secrets in ${t.configName} yet \u2014 nothing to pull.`);
5577
5006
  return;
5578
5007
  }
5579
- if (result.created) {
5580
- const copiedSecrets = result.copiedSecrets ?? 0;
5581
- console.log(
5582
- `Created ${target.projectName}/${target.configName} from ${sourceConfigName} (${copiedSecrets} secret${copiedSecrets === 1 ? "" : "s"}).`
5583
- );
5584
- } else {
5585
- console.log(`Config ${target.projectName}/${target.configName} already exists.`);
5586
- }
5587
- if (count === null) {
5588
- unlinkedTargetNotice(target);
5589
- } else {
5590
- console.log(`Refreshed ${count} cached secret${count === 1 ? "" : "s"}.`);
5591
- }
5008
+ await writeDevVars(appDir, ownerId, t.token, wranglerEnv, {
5009
+ appId: t.appId,
5010
+ generatedSecretsCache: renderSecretsCache(pulled.values, pulled),
5011
+ sharedDevVarsCache: true
5012
+ });
5013
+ console.log(
5014
+ `Pulled ${Object.keys(pulled.values).length} secrets (${t.configName}) into .dev.vars.`
5015
+ );
5592
5016
  } catch (err) {
5593
- console.error(err instanceof Error ? err.message : String(err));
5594
- process.exit(1);
5017
+ fail(err);
5595
5018
  }
5596
5019
  }
5597
5020
  });
5598
- var configsDelete = defineCommand19({
5599
- meta: { name: "delete", description: "Delete a config and all of its secrets" },
5600
- args: {
5601
- ...commonArgs(),
5602
- name: { type: "positional", description: "Config name", required: false }
5603
- },
5021
+ var configsList = defineCommand20({
5022
+ meta: { name: "list", description: "List the app\u2019s configs" },
5023
+ args: { ...COMMON_ARGS },
5604
5024
  async run({ args }) {
5605
5025
  try {
5606
- const token = await ensureToken();
5607
- const dir = appDir(args);
5608
- const { target } = targetFromArgs(args);
5609
- const rawName = typeof args.name === "string" && args.name ? args.name : args.config;
5610
- if (typeof rawName !== "string" || !rawName) throw new Error("Config name required");
5611
- const configName = validateConfigName(rawName);
5612
- const link = resolveSecretsLink(dir, target.wranglerEnv);
5613
- const deletingLinked = link.linked && link.projectName === target.projectName && link.configName === configName;
5614
- await deleteConfig(
5615
- DEPLOY_URL5,
5616
- token,
5617
- target.projectName,
5618
- configName,
5619
- appScopedSecretsApiOptions(dir, target)
5620
- );
5621
- if (deletingLinked) {
5622
- removeWranglerSecretsLink(dir, target.wranglerEnv);
5623
- clearSecretsCacheFile(dir);
5624
- }
5625
- if (args.json) {
5626
- process.stdout.write(JSON.stringify({
5627
- ok: true,
5628
- deleted: configName,
5629
- projectName: target.projectName,
5630
- unlinked: deletingLinked,
5631
- cachedSecrets: deletingLinked ? 0 : null
5632
- }) + "\n");
5026
+ const t = await resolveTarget(args);
5027
+ const { configs: configs2 } = await listConfigs(DEPLOY_URL6, t.token, t.appId);
5028
+ if (configs2.length === 0) {
5029
+ console.log("No configs yet \u2014 the first `secrets set` creates one.");
5633
5030
  return;
5634
5031
  }
5635
- console.log(`Deleted ${target.projectName}/${configName}.`);
5636
- if (deletingLinked) {
5637
- console.log("Deleted config was linked; removed the local link and cleared generated cache.");
5638
- redeployReminder({ ...target, configName });
5032
+ for (const cfg of configs2) {
5033
+ console.log(`${cfg.name} (${cfg.secretCount ?? 0} secrets, updated ${cfg.updatedAt})`);
5639
5034
  }
5640
5035
  } catch (err) {
5641
- console.error(err instanceof Error ? err.message : String(err));
5642
- process.exit(1);
5036
+ fail(err);
5643
5037
  }
5644
5038
  }
5645
5039
  });
5646
- var setup = defineCommand19({
5647
- meta: { name: "setup", description: "Link this app/env to a secrets project config" },
5648
- args: setupArgs(),
5040
+ var configsCreate = defineCommand20({
5041
+ meta: { name: "create", description: "Create a config (optionally copying another)" },
5042
+ args: {
5043
+ ...COMMON_ARGS,
5044
+ name: { type: "positional", description: "Config name", required: true },
5045
+ "copy-from": { type: "string", description: "Copy secrets from this config", required: false }
5046
+ },
5649
5047
  async run({ args }) {
5650
5048
  try {
5651
- const { appDir: dir, target, projectOverride } = resolveSecretsSetupArgs(args);
5652
- writeLinkForTarget(dir, target, projectOverride);
5653
- await bestEffortSetServerSecretsLink(dir, target);
5654
- const remoteCheck = await ensureLinkedRemoteTarget(dir, target);
5655
- const migration = await importLegacyDevVarsSecrets(dir, target, remoteCheck);
5656
- if (args.json) {
5657
- process.stdout.write(
5658
- JSON.stringify({ ok: true, target, remoteCheck, ...migration && { migration } }) + "\n"
5659
- );
5660
- return;
5661
- }
5662
- console.log(`Linked ${target.wranglerEnv ? `[env.${target.wranglerEnv}]` : "top-level app"} to ${target.projectName}/${target.configName}.`);
5663
- if (remoteCheck.status === "created") {
5664
- console.log(`Created remote config ${target.projectName}/${target.configName}.`);
5665
- }
5666
- printLegacySecretsImport(migration, target);
5667
- printLinkRemoteWarning(remoteCheck);
5049
+ const t = await resolveTarget(args);
5050
+ const name = validateConfigName(args.name);
5051
+ await createConfig(DEPLOY_URL6, t.token, t.appId, name, args["copy-from"] || void 0);
5052
+ console.log(`Created ${name}.`);
5668
5053
  } catch (err) {
5669
- console.error(err instanceof Error ? err.message : String(err));
5670
- process.exit(1);
5054
+ fail(err);
5671
5055
  }
5672
5056
  }
5673
5057
  });
5674
- var configureUnset = defineCommand19({
5675
- meta: { name: "unset", description: "Unset linked secrets project/config settings" },
5058
+ var configsDelete = defineCommand20({
5059
+ meta: { name: "delete", description: "Delete a config and all of its secrets" },
5676
5060
  args: {
5677
- ...scopedArgs(),
5678
- name: { type: "positional", description: "Setting names to unset: project and/or config", required: false }
5061
+ ...COMMON_ARGS,
5062
+ name: { type: "positional", description: "Config name", required: true }
5679
5063
  },
5680
5064
  async run({ args }) {
5681
5065
  try {
5682
- const { appDir: dir, unset, wranglerEnv: targetEnv } = resolveConfigureUnsetArgs(args);
5683
- removeWranglerSecretsLinkKeys(dir, targetEnv, unset);
5684
- clearSecretsCacheFile(dir);
5685
- if (unset.includes("project")) {
5686
- await bestEffortClearServerSecretsLink(dir, targetEnv);
5687
- }
5688
- if (args.json) {
5689
- process.stdout.write(JSON.stringify({ ok: true, unset, env: targetEnv ?? null, cachedSecrets: 0 }) + "\n");
5690
- return;
5691
- }
5692
- console.log(`Unset ${unset.join(", ")} for ${targetEnv ? `[env.${targetEnv}]` : "top-level app"} secrets config.`);
5693
- console.log("Cleared generated secrets cache for that target.");
5066
+ const t = await resolveTarget(args);
5067
+ const name = validateConfigName(args.name);
5068
+ await deleteConfig(DEPLOY_URL6, t.token, t.appId, name);
5069
+ console.log(`Deleted ${name}.`);
5694
5070
  } catch (err) {
5695
- console.error(err instanceof Error ? err.message : String(err));
5696
- process.exit(1);
5071
+ fail(err);
5697
5072
  }
5698
5073
  }
5699
5074
  });
5700
- var configure = defineCommand19({
5701
- meta: { name: "configure", description: "Manage local secrets CLI configuration" },
5702
- subCommands: {
5703
- unset: configureUnset
5704
- }
5705
- });
5706
- var configs = defineCommand19({
5707
- meta: { name: "configs", description: "Manage remote secrets configs" },
5708
- subCommands: {
5709
- list: configsList,
5710
- create: configsCreate,
5711
- clone: configsClone,
5712
- delete: configsDelete
5713
- }
5075
+ var configs = defineCommand20({
5076
+ meta: { name: "configs", description: "Manage the app\u2019s secrets configs" },
5077
+ subCommands: { list: configsList, create: configsCreate, delete: configsDelete }
5714
5078
  });
5715
- var secrets_default = defineCommand19({
5716
- meta: { name: "secrets", description: "Manage remote DeepSpace app secrets" },
5079
+ var secrets_default = defineCommand20({
5080
+ meta: { name: "secrets", description: "Manage the app\u2019s secrets store" },
5717
5081
  subCommands: {
5718
5082
  list: list5,
5719
5083
  set,
5720
5084
  get,
5721
5085
  delete: del2,
5722
- pull,
5723
- download,
5724
5086
  upload,
5725
- setup,
5726
- configure,
5087
+ download,
5088
+ pull,
5727
5089
  configs
5728
5090
  }
5729
5091
  });
5730
- function parseSetInputs(inputs) {
5731
- if (inputs.length === 0) throw new Error("At least one secret is required");
5732
- const out = {};
5733
- if (inputs.every((input) => input.includes("="))) {
5734
- for (const raw of inputs) {
5735
- const eq = raw.indexOf("=");
5736
- const key = validateSecretName(raw.slice(0, eq));
5737
- const value = raw.slice(eq + 1);
5738
- validateSecretValue(key, value);
5739
- out[key] = value;
5092
+
5093
+ // src/cli/commands/init.ts
5094
+ import { defineCommand as defineCommand21 } from "citty";
5095
+ var init_default = defineCommand21({
5096
+ meta: {
5097
+ name: "init",
5098
+ description: "Mint this app\u2019s immutable DEEPSPACE_APP_ID into wrangler.toml"
5099
+ },
5100
+ args: {
5101
+ "new-id": {
5102
+ type: "boolean",
5103
+ description: "Replace the existing id \u2014 forks this repo as a SEPARATE app (new data, new secrets, new registration). The original app keeps running.",
5104
+ default: false
5105
+ },
5106
+ env: {
5107
+ type: "string",
5108
+ alias: "e",
5109
+ description: "wrangler.toml [env.<name>] block to stamp (each env is its own app)",
5110
+ required: false
5111
+ }
5112
+ },
5113
+ async run({ args }) {
5114
+ const appDir = findAppDir();
5115
+ if (!appDir) {
5116
+ console.error("Run from a DeepSpace app directory (one containing wrangler.toml).");
5117
+ process.exit(1);
5118
+ }
5119
+ const envName = args.env || void 0;
5120
+ const existing = readAppId(appDir, envName);
5121
+ if (existing && !args["new-id"]) {
5122
+ console.log(`Already initialized: ${existing}${envName ? ` (env: ${envName})` : ""}`);
5123
+ return;
5124
+ }
5125
+ const appId = mintAppId();
5126
+ writeAppId(appDir, appId, { wranglerEnv: envName, force: args["new-id"] });
5127
+ if (existing) {
5128
+ console.log(`Forked: ${existing} \u2192 ${appId}${envName ? ` (env: ${envName})` : ""}`);
5129
+ console.log("The next deploy registers this as a NEW app; the original is untouched.");
5130
+ } else {
5131
+ console.log(`Minted ${appId}${envName ? ` (env: ${envName})` : ""} \u2014 commit wrangler.toml.`);
5132
+ console.log("The first deploy registers it and claims the `name` subdomain.");
5740
5133
  }
5741
- return out;
5742
- }
5743
- if (inputs.length === 1) {
5744
- const raw = inputs[0];
5745
- throw new Error(`Secret value required for "${raw}". Use KEY=value or KEY value.`);
5746
- }
5747
- if (inputs.length === 2 && !inputs[0].includes("=")) {
5748
- const key = validateSecretName(inputs[0]);
5749
- const value = inputs[1];
5750
- validateSecretValue(key, value);
5751
- out[key] = value;
5752
- return out;
5753
5134
  }
5754
- throw new Error("Use KEY=value for multiple secrets, or KEY value for one secret.");
5755
- }
5756
- function readExistingFile(path) {
5757
- const full = resolve10(path);
5758
- if (!existsSync14(full)) throw new Error(`File not found: ${path}`);
5759
- return readFileSync13(full, "utf-8");
5760
- }
5135
+ });
5761
5136
 
5762
5137
  // src/cli/cli.ts
5763
5138
  var __dirname = dirname4(fileURLToPath2(import.meta.url));
5764
5139
  var pkg = JSON.parse(readFileSync14(join16(__dirname, "..", "package.json"), "utf-8"));
5765
- if (process.platform === "linux" && !process.env.SSL_CERT_FILE && existsSync15("/etc/ssl/certs/ca-certificates.crt")) {
5140
+ if (process.platform === "linux" && !process.env.SSL_CERT_FILE && existsSync14("/etc/ssl/certs/ca-certificates.crt")) {
5766
5141
  process.env.SSL_CERT_FILE = "/etc/ssl/certs/ca-certificates.crt";
5767
5142
  }
5768
- var create2 = defineCommand20({
5143
+ var create2 = defineCommand22({
5769
5144
  meta: {
5770
5145
  name: "create",
5771
5146
  description: "Create a new DeepSpace app"
@@ -5784,13 +5159,14 @@ var create2 = defineCommand20({
5784
5159
  execSync4(`npm create deepspace@latest${name}`, { stdio: "inherit" });
5785
5160
  }
5786
5161
  });
5787
- var main = defineCommand20({
5162
+ var main = defineCommand22({
5788
5163
  meta: {
5789
5164
  name: "deepspace",
5790
5165
  version: pkg.version,
5791
5166
  description: "DeepSpace SDK CLI"
5792
5167
  },
5793
5168
  subCommands: {
5169
+ init: init_default,
5794
5170
  create: create2,
5795
5171
  login: login_default,
5796
5172
  logout: logout_default,
@@ -5808,6 +5184,7 @@ var main = defineCommand20({
5808
5184
  undeploy: undeploy_default,
5809
5185
  domain: domain_default,
5810
5186
  collaborators: collaborators_default,
5187
+ transfer: transfer_default,
5811
5188
  library: library_default,
5812
5189
  feedback: feedback_default,
5813
5190
  secrets: secrets_default
@@ -5817,5 +5194,5 @@ var main = defineCommand20({
5817
5194
  // command. Citty's default behavior when no subcommand matches is to print
5818
5195
  // help, which is what we want for `deepspace` with no args.
5819
5196
  });
5820
- runMain(main);
5197
+ runMain(wrapCommandErrors(main));
5821
5198
  //# sourceMappingURL=cli.js.map