deepspace 0.5.4 → 0.5.6

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
@@ -2,15 +2,15 @@
2
2
 
3
3
  // src/cli/cli.ts
4
4
  import { defineCommand as defineCommand22, runMain } from "citty";
5
- import { execSync as execSync4 } from "child_process";
6
- import { readFileSync as readFileSync14, existsSync as existsSync14 } from "fs";
7
- import { dirname as dirname4, join as join16 } from "path";
8
- import { fileURLToPath as fileURLToPath2 } from "url";
5
+ import { spawnSync as spawnSync6 } from "child_process";
6
+ import { readFileSync as readFileSync17, existsSync as existsSync14 } from "fs";
7
+ import { dirname as dirname5, join as join15 } from "path";
8
+ import { fileURLToPath as fileURLToPath3 } from "url";
9
9
 
10
10
  // src/cli/lib/cli-errors.ts
11
11
  var API_ERROR_HINTS = {
12
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.",
13
+ app_not_found: "App not found. Check the app id \u2014 the DEEPSPACE_APP_ID value in wrangler.toml, usually `app_\u2026` (a legacy app's id is its name). List your apps with `deepspace apps`. (`domain` commands take the deployed app *name* instead.)",
14
14
  not_app_owner_or_collaborator: "You must be the app owner or a collaborator to do this.",
15
15
  test_account_cannot_be_collaborator: "Test accounts cannot be added as collaborators. Use a real DeepSpace account.",
16
16
  user_not_found: "No DeepSpace user with that email. They need to log in to DeepSpace at least once (`deepspace login`, or sign in to any app) before they can be referenced by email."
@@ -25,8 +25,11 @@ ${hint}` : message;
25
25
  }
26
26
  function renderCliError(err) {
27
27
  console.error(formatCliError(err));
28
- if (process.env.DEBUG && err instanceof Error && err.stack) {
29
- console.error("\n" + err.stack);
28
+ if (process.env.DEBUG) {
29
+ const { apiPath, status: status3 } = err;
30
+ if (apiPath) console.error(`
31
+ API ${apiPath}${status3 ? ` (${status3})` : ""}`);
32
+ if (err instanceof Error && err.stack) console.error("\n" + err.stack);
30
33
  }
31
34
  process.exit(1);
32
35
  }
@@ -51,7 +54,7 @@ function wrapCommandErrors(cmd) {
51
54
 
52
55
  // src/cli/commands/login.ts
53
56
  import { defineCommand } from "citty";
54
- import { writeFileSync as writeFileSync5, mkdirSync } from "fs";
57
+ import { writeFileSync as writeFileSync5, mkdirSync, readFileSync as readFileSync4 } from "fs";
55
58
  import { join as join3 } from "path";
56
59
  import { homedir } from "os";
57
60
  import { randomBytes, createHash } from "crypto";
@@ -77,8 +80,215 @@ function createSpinner() {
77
80
  // src/cli/env.ts
78
81
  import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
79
82
 
83
+ // src/server/rooms/binding-manifest.ts
84
+ var AUTO_PROVISION_SENTINEL = "auto";
85
+ var VECTORIZE_METRICS = /* @__PURE__ */ new Set(["cosine", "euclidean", "dot-product"]);
86
+ var ALLOWED_BINDING_TYPES = /* @__PURE__ */ new Set([
87
+ "vectorize",
88
+ "ai",
89
+ "r2_bucket",
90
+ "kv_namespace",
91
+ "d1",
92
+ "queue",
93
+ "browser_rendering",
94
+ "analytics_engine",
95
+ "hyperdrive"
96
+ ]);
97
+ var RESERVED_BINDING_NAMES = /* @__PURE__ */ new Set([
98
+ "ASSETS",
99
+ "PLATFORM_WORKER",
100
+ "API_WORKER",
101
+ "APP_NAME",
102
+ "OWNER_USER_ID",
103
+ "AUTH_JWT_PUBLIC_KEY",
104
+ "AUTH_JWT_ISSUER",
105
+ "AUTH_WORKER_URL",
106
+ "APP_IDENTITY_TOKEN",
107
+ "APP_OWNER_JWT",
108
+ "USAGE_EVENTS"
109
+ ]);
110
+ function validateBindingManifest(manifest) {
111
+ if (!Array.isArray(manifest)) {
112
+ return { valid: false, errors: [{ reason: "Manifest must be an array" }] };
113
+ }
114
+ const errors = [];
115
+ const seenNames = /* @__PURE__ */ new Set();
116
+ for (const entry of manifest) {
117
+ if (!entry || typeof entry !== "object") {
118
+ errors.push({ reason: `Entry must be an object (got ${entry === null ? "null" : typeof entry})` });
119
+ continue;
120
+ }
121
+ const e = entry;
122
+ const type = e.type;
123
+ const name = e.name;
124
+ if (typeof type !== "string" || !ALLOWED_BINDING_TYPES.has(type)) {
125
+ errors.push({
126
+ reason: `Disallowed binding type: ${typeof type === "string" ? type : `(${typeof type})`}. Allowed: ${[...ALLOWED_BINDING_TYPES].sort().join(", ")}`
127
+ });
128
+ continue;
129
+ }
130
+ if (typeof name !== "string" || !name) {
131
+ errors.push({ reason: `Binding of type "${type}" missing 'name' (string)` });
132
+ continue;
133
+ }
134
+ if (RESERVED_BINDING_NAMES.has(name)) {
135
+ errors.push({ reason: `Binding name "${name}" is reserved by the SDK` });
136
+ continue;
137
+ }
138
+ if (seenNames.has(name)) {
139
+ errors.push({ reason: `Duplicate binding name "${name}"` });
140
+ continue;
141
+ }
142
+ seenNames.add(name);
143
+ const binding = entry;
144
+ const fieldErr = requiredFieldError(binding);
145
+ if (fieldErr) errors.push({ binding, reason: fieldErr });
146
+ }
147
+ if (errors.length) return { valid: false, errors };
148
+ return { valid: true, bindings: manifest };
149
+ }
150
+ function requiredFieldError(b) {
151
+ switch (b.type) {
152
+ case "vectorize": {
153
+ if (!b.index_name) return `vectorize binding "${b.name}" missing index_name (or "auto")`;
154
+ if (b.index_name === AUTO_PROVISION_SENTINEL) {
155
+ if (typeof b.dimensions !== "number" || b.dimensions <= 0) {
156
+ return `vectorize binding "${b.name}" with index_name="auto" requires "dimensions" (positive number)`;
157
+ }
158
+ if (!b.metric || !VECTORIZE_METRICS.has(b.metric)) {
159
+ return `vectorize binding "${b.name}" with index_name="auto" requires "metric" (one of: ${[...VECTORIZE_METRICS].join(", ")})`;
160
+ }
161
+ }
162
+ return null;
163
+ }
164
+ case "r2_bucket":
165
+ return b.bucket_name ? null : `r2_bucket binding "${b.name}" missing bucket_name (or "auto")`;
166
+ case "kv_namespace": {
167
+ if (!b.namespace_id) return `kv_namespace binding "${b.name}" missing namespace_id (or "auto")`;
168
+ if (b.namespace_id === AUTO_PROVISION_SENTINEL && !b.title) {
169
+ return `kv_namespace binding "${b.name}" with namespace_id="auto" requires "title"`;
170
+ }
171
+ return null;
172
+ }
173
+ case "d1": {
174
+ if (!b.id) return `d1 binding "${b.name}" missing id (or "auto")`;
175
+ if (b.id === AUTO_PROVISION_SENTINEL && !b.database_name) {
176
+ return `d1 binding "${b.name}" with id="auto" requires "database_name"`;
177
+ }
178
+ return null;
179
+ }
180
+ case "queue":
181
+ return b.queue_name ? null : `queue binding "${b.name}" missing queue_name (or "auto")`;
182
+ case "hyperdrive":
183
+ if (!b.id) return `hyperdrive binding "${b.name}" missing id`;
184
+ if (b.id === AUTO_PROVISION_SENTINEL) {
185
+ return `hyperdrive binding "${b.name}": auto-provisioning not yet supported (provide a real id)`;
186
+ }
187
+ return null;
188
+ case "ai":
189
+ case "browser_rendering":
190
+ case "analytics_engine":
191
+ return null;
192
+ // no required fields beyond `name`
193
+ default: {
194
+ const _exhaustive = b;
195
+ void _exhaustive;
196
+ return null;
197
+ }
198
+ }
199
+ }
200
+ function bindingManifestFromOutputConfig(outputConfig) {
201
+ const out = [];
202
+ const aiBinding = pluckString(outputConfig.ai, "binding");
203
+ if (aiBinding) out.push({ type: "ai", name: aiBinding });
204
+ for (const v of asObjectArray(outputConfig.vectorize)) {
205
+ const name = pluckString(v, "binding");
206
+ const index_name = pluckString(v, "index_name");
207
+ if (!name || !index_name) continue;
208
+ const dimensions = pluckNumber(v, "dimensions");
209
+ const metric = pluckString(v, "metric");
210
+ out.push({
211
+ type: "vectorize",
212
+ name,
213
+ index_name,
214
+ ...dimensions != null && { dimensions },
215
+ ...metric && (metric === "cosine" || metric === "euclidean" || metric === "dot-product") && { metric }
216
+ });
217
+ }
218
+ for (const r of asObjectArray(outputConfig.r2_buckets)) {
219
+ const name = pluckString(r, "binding");
220
+ const bucket_name = pluckString(r, "bucket_name");
221
+ if (name && bucket_name) out.push({ type: "r2_bucket", name, bucket_name });
222
+ }
223
+ for (const k of asObjectArray(outputConfig.kv_namespaces)) {
224
+ const name = pluckString(k, "binding");
225
+ const namespace_id = pluckString(k, "id");
226
+ if (!name || !namespace_id) continue;
227
+ const title = pluckString(k, "title");
228
+ out.push({
229
+ type: "kv_namespace",
230
+ name,
231
+ namespace_id,
232
+ ...title && { title }
233
+ });
234
+ }
235
+ for (const d of asObjectArray(outputConfig.d1_databases)) {
236
+ const name = pluckString(d, "binding");
237
+ const id = pluckString(d, "database_id");
238
+ if (!name || !id) continue;
239
+ const database_name = pluckString(d, "database_name");
240
+ out.push({
241
+ type: "d1",
242
+ name,
243
+ id,
244
+ ...database_name && { database_name }
245
+ });
246
+ }
247
+ const queues = isObject(outputConfig.queues) ? outputConfig.queues : null;
248
+ for (const q of asObjectArray(queues?.producers)) {
249
+ const name = pluckString(q, "binding");
250
+ const queue_name = pluckString(q, "queue");
251
+ if (name && queue_name) out.push({ type: "queue", name, queue_name });
252
+ }
253
+ const browserBinding = pluckString(outputConfig.browser, "binding");
254
+ if (browserBinding) out.push({ type: "browser_rendering", name: browserBinding });
255
+ for (const a of asObjectArray(outputConfig.analytics_engine_datasets)) {
256
+ const name = pluckString(a, "binding");
257
+ const dataset = pluckString(a, "dataset");
258
+ if (name) out.push({ type: "analytics_engine", name, dataset: dataset ?? void 0 });
259
+ }
260
+ for (const h of asObjectArray(outputConfig.hyperdrive)) {
261
+ const name = pluckString(h, "binding");
262
+ const id = pluckString(h, "id");
263
+ if (name && id) out.push({ type: "hyperdrive", name, id });
264
+ }
265
+ return out;
266
+ }
267
+ function isObject(v) {
268
+ return v !== null && typeof v === "object" && !Array.isArray(v);
269
+ }
270
+ function pluckString(v, key) {
271
+ if (!isObject(v)) return null;
272
+ const val = v[key];
273
+ return typeof val === "string" && val.length > 0 ? val : null;
274
+ }
275
+ function pluckNumber(v, key) {
276
+ if (!isObject(v)) return null;
277
+ const val = v[key];
278
+ return typeof val === "number" && Number.isFinite(val) ? val : null;
279
+ }
280
+ function asObjectArray(v) {
281
+ if (!Array.isArray(v)) return [];
282
+ return v.filter(isObject);
283
+ }
284
+
80
285
  // src/cli/lib/secrets.ts
81
286
  var GENERATED_SECRETS_DIVIDER = "# --- DeepSpace secrets cache: generated by `npx deepspace secrets`; do not edit manually ---";
287
+ var RESERVED_SECRET_NAMES = /* @__PURE__ */ new Set([
288
+ ...RESERVED_BINDING_NAMES,
289
+ "API_WORKER_URL",
290
+ "PLATFORM_WORKER_URL"
291
+ ]);
82
292
  var SECRET_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
83
293
  var CONFIG_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
84
294
  var MAX_SECRET_VALUE_BYTES = 32 * 1024;
@@ -96,6 +306,11 @@ function validateSecretName(name) {
96
306
  `Invalid secret name "${name}". Use letters, numbers, and underscores; start with a letter or underscore.`
97
307
  );
98
308
  }
309
+ if (RESERVED_SECRET_NAMES.has(name)) {
310
+ throw new Error(
311
+ `"${name}" is managed by the DeepSpace SDK and can't be set as an app secret \u2014 the platform injects it at deploy time.`
312
+ );
313
+ }
99
314
  return name;
100
315
  }
101
316
  function validateSecretValue(key, value) {
@@ -117,7 +332,10 @@ async function secretsApi(deployUrl, token, path, init = {}) {
117
332
  });
118
333
  const body = await res.json().catch(() => ({}));
119
334
  if (!res.ok) {
120
- throw new Error(`Secrets API ${path} (${res.status}): ${body.error ?? "request failed"}`);
335
+ const err = new Error(body.error ?? `Secrets request failed (${res.status})`);
336
+ err.status = res.status;
337
+ err.apiPath = path;
338
+ throw err;
121
339
  }
122
340
  return body;
123
341
  }
@@ -177,8 +395,7 @@ async function pullAppSecretsCache(deployUrl, token, appId, configName) {
177
395
  const { secrets } = await fetchSecretsValues(deployUrl, token, appId, configName);
178
396
  return { appId, configName, values: secrets };
179
397
  } catch (err) {
180
- const msg = err instanceof Error ? err.message : String(err);
181
- if (msg.includes("(404)")) return null;
398
+ if (err?.status === 404) return null;
182
399
  throw err;
183
400
  }
184
401
  }
@@ -223,22 +440,56 @@ function parseSecretsUpload(content) {
223
440
  return out2;
224
441
  }
225
442
  const out = {};
226
- for (const rawLine of trimmed.split("\n")) {
227
- const line = rawLine.trim();
443
+ const lines = trimmed.split(/\r?\n/);
444
+ for (let i = 0; i < lines.length; i++) {
445
+ const line = lines[i].trim();
228
446
  if (!line || line.startsWith("#")) continue;
229
447
  const eq = line.indexOf("=");
230
448
  if (eq <= 0) continue;
231
449
  const key = line.slice(0, eq).trim();
232
450
  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));
451
+ validateSecretName(key);
452
+ const raw = line.slice(eq + 1).trim();
453
+ const quote = raw[0] === '"' || raw[0] === "'" ? raw[0] : null;
454
+ let value;
455
+ if (!quote) {
456
+ value = raw;
457
+ } else if (endsWithUnescapedQuote(raw.slice(1), quote)) {
458
+ const inner = raw.slice(1, -1);
459
+ value = quote === '"' ? unescapeDotenv(inner) : inner;
460
+ } else {
461
+ const startLine = i + 1;
462
+ const parts = [raw.slice(1)];
463
+ let closed = false;
464
+ while (++i < lines.length) {
465
+ const cont = lines[i].trimEnd();
466
+ if (endsWithUnescapedQuote(cont, quote)) {
467
+ parts.push(cont.slice(0, -1));
468
+ closed = true;
469
+ break;
470
+ }
471
+ parts.push(lines[i]);
472
+ }
473
+ if (!closed) {
474
+ throw new Error(
475
+ `Unterminated quoted value for "${key}" (opens on line ${startLine}). Close the ${quote === '"' ? "double" : "single"} quote, or escape newlines as \\n on one line.`
476
+ );
477
+ }
478
+ value = parts.join("\n");
479
+ if (quote === '"') value = unescapeDotenv(value);
236
480
  }
237
481
  validateSecretValue(key, value);
238
482
  out[key] = value;
239
483
  }
240
484
  return out;
241
485
  }
486
+ function endsWithUnescapedQuote(s, q) {
487
+ if (!s.endsWith(q)) return false;
488
+ if (q === "'") return true;
489
+ let backslashes = 0;
490
+ for (let j = s.length - 2; j >= 0 && s[j] === "\\"; j--) backslashes++;
491
+ return backslashes % 2 === 0;
492
+ }
242
493
  function unescapeDotenv(v) {
243
494
  let out = "";
244
495
  for (let i = 0; i < v.length; i++) {
@@ -251,13 +502,16 @@ function unescapeDotenv(v) {
251
502
  }
252
503
  return out;
253
504
  }
505
+ function shellSingleQuote(value) {
506
+ return `'${value.replace(/'/g, "'\\''")}'`;
507
+ }
254
508
  function formatSecretsDownload(secrets, format) {
255
509
  const entries = Object.entries(secrets).sort(([a], [b]) => a.localeCompare(b));
256
510
  if (format === "json") {
257
511
  return JSON.stringify(Object.fromEntries(entries), null, 2) + "\n";
258
512
  }
259
513
  if (format === "shell") {
260
- return entries.map(([k, v]) => `export ${k}=${quoteDotenvValue(v)}`).join("\n") + "\n";
514
+ return entries.map(([k, v]) => `export ${k}=${shellSingleQuote(v)}`).join("\n") + "\n";
261
515
  }
262
516
  return entries.map(([k, v]) => `${k}=${quoteDotenvValue(v)}`).join("\n") + "\n";
263
517
  }
@@ -690,38 +944,6 @@ function extractCustomDevVars(content) {
690
944
  while (out.length && out[out.length - 1].trim() === "") out.pop();
691
945
  return out.join("\n");
692
946
  }
693
- function removeDevVarsKeys(content, keys) {
694
- const lines = content.split("\n");
695
- const out = [];
696
- let i = 0;
697
- while (i < lines.length) {
698
- const line = lines[i];
699
- const eq = line.indexOf("=");
700
- const keyMatch = eq >= 0 ? line.slice(0, eq).match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*$/) : null;
701
- if (!keyMatch) {
702
- out.push(line);
703
- i++;
704
- continue;
705
- }
706
- const key = keyMatch[1];
707
- const valStart = line.slice(eq + 1);
708
- const block = [line];
709
- if (valStart.startsWith('"') && !hasUnescapedQuote(valStart.slice(1))) {
710
- i++;
711
- while (i < lines.length) {
712
- block.push(lines[i]);
713
- const closed = hasUnescapedQuote(lines[i]);
714
- i++;
715
- if (closed) break;
716
- }
717
- } else {
718
- i++;
719
- }
720
- if (!keys.has(key)) out.push(...block);
721
- }
722
- while (out.length && out[out.length - 1].trim() === "") out.pop();
723
- return out.join("\n");
724
- }
725
947
  function hasUnescapedQuote(s) {
726
948
  let escaped = false;
727
949
  for (const ch of s) {
@@ -820,8 +1042,10 @@ async function writeDevVars(appDir, ownerId, callerJwt, wranglerEnv, opts = {})
820
1042
  `OWNER_USER_ID=${ownerId}`,
821
1043
  `APP_OWNER_JWT=${appOwnerJwt}`,
822
1044
  ...appIdentityToken ? [`APP_IDENTITY_TOKEN=${appIdentityToken}`] : [],
823
- // Open /api/debug/* (proxied to RecordRoom) for dev + tests only.
824
- // Production secrets never set this, so deployed apps 404 the route.
1045
+ // Open /api/debug/* (proxied to RecordRoom) locally. This is the SDK's
1046
+ // dev-only auto-value; the deploy ships the remote store, not this
1047
+ // `.dev.vars` section, so it never reaches prod. To enable it on a deployed
1048
+ // env, `deepspace secrets set ALLOW_DEBUG_ROUTES=true`.
825
1049
  `ALLOW_DEBUG_ROUTES=true`
826
1050
  ].join("\n");
827
1051
  const useSharedDevVarsCache = opts.sharedDevVarsCache ?? opts.generatedSecretsCache !== void 0;
@@ -884,19 +1108,38 @@ var login_default = defineCommand({
884
1108
  args: {
885
1109
  email: {
886
1110
  type: "string",
887
- description: "Email address (non-interactive mode)",
1111
+ description: "Email address (non-interactive mode; or $DEEPSPACE_EMAIL)",
888
1112
  required: false
889
1113
  },
890
1114
  password: {
891
1115
  type: "string",
892
- description: "Password (non-interactive mode)",
1116
+ description: "Password (discouraged \u2014 visible in ps/history; prefer --password-stdin or $DEEPSPACE_PASSWORD)",
893
1117
  required: false
1118
+ },
1119
+ "password-stdin": {
1120
+ type: "boolean",
1121
+ description: "Read the password from stdin instead of --password",
1122
+ default: false
894
1123
  }
895
1124
  },
896
1125
  async run({ args }) {
897
- if (args.email && args.password) {
898
- console.log(`Signing in as ${args.email}...`);
899
- await doEmailLogin(args.email, args.password);
1126
+ const interactive = Boolean(process.stdin.isTTY);
1127
+ const { email, password } = resolveLoginCredentials({
1128
+ emailArg: args.email,
1129
+ passwordArg: args.password,
1130
+ envEmail: interactive ? void 0 : process.env.DEEPSPACE_EMAIL,
1131
+ envPassword: interactive ? void 0 : process.env.DEEPSPACE_PASSWORD,
1132
+ passwordStdin: args["password-stdin"] ? readPasswordFromStdin() : void 0
1133
+ });
1134
+ const passwordIntent = Boolean(args["password-stdin"]) || args.password !== void 0 || password !== void 0;
1135
+ const decision = loginModeDecision({ email, password, passwordIntent });
1136
+ if (decision.mode === "error") {
1137
+ console.error(decision.message);
1138
+ process.exit(1);
1139
+ }
1140
+ if (decision.mode === "password") {
1141
+ console.log(`Signing in as ${email}...`);
1142
+ await doEmailLogin(email, password);
900
1143
  console.log("Logged in");
901
1144
  return;
902
1145
  }
@@ -947,6 +1190,34 @@ plans on the web dashboard:
947
1190
  p2.outro("Done");
948
1191
  }
949
1192
  });
1193
+ function resolveLoginCredentials(opts) {
1194
+ const email = opts.emailArg || opts.envEmail || void 0;
1195
+ const password = opts.passwordStdin !== void 0 ? opts.passwordStdin : opts.passwordArg || opts.envPassword || void 0;
1196
+ return { email, password };
1197
+ }
1198
+ function loginModeDecision(opts) {
1199
+ if (opts.email && opts.password) return { mode: "password" };
1200
+ if (!opts.passwordIntent) return { mode: "oauth" };
1201
+ if (!opts.email) {
1202
+ return {
1203
+ mode: "error",
1204
+ message: "Non-interactive login needs an email: --email <you@example.com> (or $DEEPSPACE_EMAIL)."
1205
+ };
1206
+ }
1207
+ return {
1208
+ mode: "error",
1209
+ message: "Non-interactive login needs a non-empty password: --password-stdin, $DEEPSPACE_PASSWORD, or --password."
1210
+ };
1211
+ }
1212
+ function readPasswordFromStdin() {
1213
+ if (process.stdin.isTTY) {
1214
+ console.error(
1215
+ '--password-stdin expects the password piped on stdin, e.g. `printf %s "$PW" | deepspace login --email you@x.com --password-stdin`.'
1216
+ );
1217
+ process.exit(1);
1218
+ }
1219
+ return readFileSync4(0, "utf-8").replace(/\r?\n$/, "");
1220
+ }
950
1221
  async function pollForCompletion(sessionId, codeVerifier) {
951
1222
  const maxAttempts = 120;
952
1223
  const interval = 5e3;
@@ -1017,7 +1288,7 @@ async function doEmailLogin(email, password) {
1017
1288
 
1018
1289
  // src/cli/commands/logout.ts
1019
1290
  import { defineCommand as defineCommand2 } from "citty";
1020
- import { existsSync as existsSync4, readFileSync as readFileSync4, rmSync as rmSync2 } from "fs";
1291
+ import { existsSync as existsSync4, readFileSync as readFileSync5, rmSync as rmSync2 } from "fs";
1021
1292
  import { join as join4 } from "path";
1022
1293
  import { homedir as homedir2 } from "os";
1023
1294
  var AUTH_URL2 = process.env.DEEPSPACE_AUTH_URL ?? PLATFORM_URLS.auth;
@@ -1030,7 +1301,7 @@ var logout_default = defineCommand2({
1030
1301
  description: "Sign out and remove cached credentials"
1031
1302
  },
1032
1303
  async run() {
1033
- const sessionToken = existsSync4(SESSION_PATH) ? readFileSync4(SESSION_PATH, "utf-8").trim() : null;
1304
+ const sessionToken = existsSync4(SESSION_PATH) ? readFileSync5(SESSION_PATH, "utf-8").trim() : null;
1034
1305
  if (sessionToken) {
1035
1306
  try {
1036
1307
  await fetch(`${AUTH_URL2}/api/auth/sign-out`, {
@@ -1060,11 +1331,11 @@ var logout_default = defineCommand2({
1060
1331
 
1061
1332
  // src/cli/commands/dev.ts
1062
1333
  import { defineCommand as defineCommand3 } from "citty";
1063
- import { resolve as resolve4, basename, join as join9 } from "path";
1334
+ import { resolve as resolve4, basename as basename2, join as join9 } from "path";
1064
1335
  import spawn from "cross-spawn";
1065
1336
 
1066
1337
  // src/cli/auth.ts
1067
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync6, existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
1338
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync6, existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
1068
1339
  import { join as join5 } from "path";
1069
1340
  import { homedir as homedir3 } from "os";
1070
1341
 
@@ -1084,7 +1355,7 @@ var SESSION_PATH2 = join5(DIR2, "session");
1084
1355
  var TOKEN_PATH2 = join5(DIR2, "token");
1085
1356
  async function ensureToken() {
1086
1357
  if (existsSync5(TOKEN_PATH2)) {
1087
- const existing = readFileSync5(TOKEN_PATH2, "utf-8").trim();
1358
+ const existing = readFileSync6(TOKEN_PATH2, "utf-8").trim();
1088
1359
  if (isTokenValid(existing)) {
1089
1360
  return existing;
1090
1361
  }
@@ -1092,7 +1363,7 @@ async function ensureToken() {
1092
1363
  if (!existsSync5(SESSION_PATH2)) {
1093
1364
  throw new Error("Not logged in. Run `deepspace login` first.");
1094
1365
  }
1095
- const sessionToken = readFileSync5(SESSION_PATH2, "utf-8").trim();
1366
+ const sessionToken = readFileSync6(SESSION_PATH2, "utf-8").trim();
1096
1367
  const token = await exchangeSession(AUTH_URL3, sessionToken);
1097
1368
  if (!token) {
1098
1369
  throw new Error("Session expired. Run `deepspace login` to re-authenticate.");
@@ -1113,20 +1384,56 @@ function isTokenValid(jwt) {
1113
1384
  // src/cli/lib/app-context.ts
1114
1385
  import {
1115
1386
  existsSync as existsSync6,
1116
- readFileSync as readFileSync6,
1387
+ readFileSync as readFileSync7,
1117
1388
  readdirSync,
1118
1389
  writeFileSync as writeFileSync7,
1119
1390
  mkdirSync as mkdirSync3,
1120
1391
  renameSync,
1121
1392
  unlinkSync
1122
1393
  } from "fs";
1123
- import { join as join6, resolve as resolve2, dirname, isAbsolute, sep } from "path";
1394
+ import { join as join6, resolve as resolve2, dirname, basename, isAbsolute, sep } from "path";
1124
1395
  import { parse as parseToml3 } from "smol-toml";
1396
+
1397
+ // src/cli/lib/api.ts
1398
+ var ApiError = class extends Error {
1399
+ constructor(message, status3, code, apiPath) {
1400
+ super(message);
1401
+ this.status = status3;
1402
+ this.code = code;
1403
+ this.apiPath = apiPath;
1404
+ }
1405
+ };
1406
+ async function apiFetch(baseUrl, token, path, init) {
1407
+ const res = await fetch(`${baseUrl}${path}`, {
1408
+ ...init,
1409
+ headers: {
1410
+ Authorization: `Bearer ${token}`,
1411
+ "Content-Type": "application/json",
1412
+ ...init?.headers ?? {}
1413
+ }
1414
+ });
1415
+ const text2 = await res.text();
1416
+ if (!res.ok) {
1417
+ let msg = text2;
1418
+ let code;
1419
+ try {
1420
+ const body = JSON.parse(text2);
1421
+ msg = body.error ?? text2;
1422
+ code = body.code;
1423
+ } catch {
1424
+ }
1425
+ throw new ApiError(msg || `Request failed (${res.status})`, res.status, code, path);
1426
+ }
1427
+ return text2 ? JSON.parse(text2) : {};
1428
+ }
1429
+
1430
+ // src/cli/lib/app-context.ts
1431
+ var STRICT_APP_ID_RE = /^app_[0-9A-HJKMNP-TV-Z]{26}$/;
1125
1432
  function detectAppName(cwd = process.cwd()) {
1126
1433
  const wranglerPath = join6(resolve2(cwd), "wrangler.toml");
1127
1434
  if (!existsSync6(wranglerPath)) return null;
1128
1435
  try {
1129
- const cfg = parseToml3(readFileSync6(wranglerPath, "utf-8"));
1436
+ const cfg = parseToml3(readFileSync7(wranglerPath, "utf-8"));
1130
1437
  return typeof cfg.name === "string" && cfg.name.length > 0 ? cfg.name : null;
1131
1438
  } catch (err) {
1132
1439
  throw new Error(
@@ -1175,18 +1482,42 @@ function writeLaunchFile(launchPath, config) {
1175
1482
  function readLaunchFile(launchPath) {
1176
1483
  const empty = { version: "0.0.1", configurations: [] };
1177
1484
  if (!existsSync6(launchPath)) return empty;
1178
- const raw = readFileSync6(launchPath, "utf-8").trim();
1485
+ const raw = readFileSync7(launchPath, "utf-8").trim();
1179
1486
  if (!raw) return empty;
1180
1487
  const parsed = JSON.parse(raw);
1181
1488
  if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.configurations)) return null;
1182
1489
  return parsed;
1183
1490
  }
1184
- function writeLaunchConfigIfMissing(appDir, appName, port) {
1491
+ function writeLaunchConfigIfMissing(appDir, appName, port, opts = {}) {
1185
1492
  const launchPath = join6(appDir, ".claude", "launch.json");
1186
1493
  try {
1187
1494
  const config = readLaunchFile(launchPath);
1188
1495
  if (!config) return;
1189
- if (config.configurations.some((c) => c?.name === appName)) return;
1496
+ const existing = config.configurations.find((c) => c?.name === appName);
1497
+ if (existing) {
1498
+ if (!opts.updatePort) return;
1499
+ let changed = false;
1500
+ if (Number(existing.port) !== port) {
1501
+ existing.port = port;
1502
+ changed = true;
1503
+ }
1504
+ if (Array.isArray(existing.runtimeArgs)) {
1505
+ const ra = existing.runtimeArgs.map(String);
1506
+ const pi = ra.indexOf("--port");
1507
+ if (pi >= 0 && pi + 1 < ra.length) {
1508
+ if (ra[pi + 1] !== String(port)) {
1509
+ ra[pi + 1] = String(port);
1510
+ changed = true;
1511
+ }
1512
+ } else {
1513
+ ra.push("--port", String(port));
1514
+ changed = true;
1515
+ }
1516
+ if (changed) existing.runtimeArgs = ra;
1517
+ }
1518
+ if (changed) writeLaunchFile(launchPath, config);
1519
+ return;
1520
+ }
1190
1521
  config.configurations.push({
1191
1522
  name: appName,
1192
1523
  runtimeExecutable: "npx",
@@ -1197,6 +1528,18 @@ function writeLaunchConfigIfMissing(appDir, appName, port) {
1197
1528
  } catch {
1198
1529
  }
1199
1530
  }
1531
+ function resolveAppLaunchPort(appDir) {
1532
+ try {
1533
+ const config = readLaunchFile(join6(appDir, ".claude", "launch.json"));
1534
+ if (!config) return null;
1535
+ const appName = detectAppName(appDir) ?? basename(appDir);
1536
+ const entry = config.configurations.find((c) => c?.name === appName) ?? (config.configurations.length === 1 ? config.configurations[0] : void 0);
1537
+ const port = Number(entry?.port);
1538
+ return Number.isInteger(port) && port > 0 ? port : null;
1539
+ } catch {
1540
+ return null;
1541
+ }
1542
+ }
1200
1543
  function detectClaudeWorktree(appDir) {
1201
1544
  const segments = resolve2(appDir).split(sep);
1202
1545
  for (let i = segments.length - 3; i >= 1; i--) {
@@ -1232,7 +1575,7 @@ function upsertWorktreeLaunchConfig(mainRepoRoot, worktreeName, appDir, opts) {
1232
1575
  let port = opts.port;
1233
1576
  if (opts.probePort) {
1234
1577
  const taken = new Set(
1235
- config.configurations.map((c) => Number(c?.port)).filter((p8) => Number.isInteger(p8) && p8 > 0)
1578
+ config.configurations.map((c) => Number(c?.port)).filter((p10) => Number.isInteger(p10) && p10 > 0)
1236
1579
  );
1237
1580
  for (let i = 0; i < WORKTREE_PORT_RANGE && taken.has(port); i++) {
1238
1581
  port = WORKTREE_PORT_MIN + (port - WORKTREE_PORT_MIN + 1) % WORKTREE_PORT_RANGE;
@@ -1285,9 +1628,36 @@ function requireAppIdArg(explicit) {
1285
1628
  }
1286
1629
  return id;
1287
1630
  }
1631
+ function matchAppSelector(apps, selector) {
1632
+ const byId = apps.find((a) => a.appId === selector);
1633
+ if (byId) return byId.appId;
1634
+ const byName = apps.find((a) => a.name === selector);
1635
+ if (byName) return byName.appId;
1636
+ return STRICT_APP_ID_RE.test(selector) ? selector : null;
1637
+ }
1638
+ async function resolveAppSelector(deployUrl, token, selector) {
1639
+ if (STRICT_APP_ID_RE.test(selector)) return selector;
1640
+ const { apps } = await apiFetch(deployUrl, token, "/api/apps");
1641
+ const id = matchAppSelector(apps, selector);
1642
+ if (!id) {
1643
+ throw new Error(`No app "${selector}" in your account. Run \`deepspace apps\` to list your apps.`);
1644
+ }
1645
+ return id;
1646
+ }
1647
+ async function resolveAppTarget(deployUrl, token, explicit) {
1648
+ if (explicit) return resolveAppSelector(deployUrl, token, explicit);
1649
+ const appDir = findAppDir();
1650
+ const id = appDir ? readAppId(appDir) : null;
1651
+ if (!id) {
1652
+ throw new Error(
1653
+ "No app id. Run from an app directory whose wrangler.toml carries DEEPSPACE_APP_ID, or pass --app <id or name>."
1654
+ );
1655
+ }
1656
+ return id;
1657
+ }
1288
1658
 
1289
1659
  // src/cli/lib/install-status.ts
1290
- import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
1660
+ import { existsSync as existsSync7, readFileSync as readFileSync8 } from "fs";
1291
1661
  import { join as join7 } from "path";
1292
1662
  function ensureInstallReady(appDir) {
1293
1663
  if (existsSync7(join7(appDir, "node_modules", "deepspace", "package.json"))) return;
@@ -1298,13 +1668,19 @@ function ensureInstallReady(appDir) {
1298
1668
  const logPath = join7(sentinelDir, "install.log");
1299
1669
  if (existsSync7(errPath)) {
1300
1670
  console.error("Background install failed:");
1301
- console.error(readFileSync7(errPath, "utf-8").trimEnd());
1671
+ console.error(readFileSync8(errPath, "utf-8").trimEnd());
1302
1672
  if (existsSync7(logPath)) console.error(`
1303
1673
  Full log: ${logPath}`);
1304
1674
  console.error("\nRun `npm install` (or `bun install`) manually, then retry.");
1305
1675
  process.exit(1);
1306
1676
  }
1307
1677
  if (existsSync7(startedPath) && !existsSync7(donePath)) {
1678
+ if (!installWorkerAlive(sentinelDir)) {
1679
+ console.error("The background install is no longer running and never finished.");
1680
+ if (existsSync7(logPath)) console.error(`See what happened: ${logPath}`);
1681
+ console.error("Run `npm install` (or `bun install`) manually, then retry.");
1682
+ process.exit(1);
1683
+ }
1308
1684
  console.error("Dependencies are still installing in the background.");
1309
1685
  if (existsSync7(logPath)) console.error(`Tail progress: tail -f ${logPath}`);
1310
1686
  console.error("Retry once it finishes.");
@@ -1313,6 +1689,18 @@ Full log: ${logPath}`);
1313
1689
  console.error("Dependencies not installed. Run `npm install` (or `bun install`) first.");
1314
1690
  process.exit(1);
1315
1691
  }
1692
+ function installWorkerAlive(sentinelDir) {
1693
+ const pidPath = join7(sentinelDir, "install.pid");
1694
+ if (!existsSync7(pidPath)) return true;
1695
+ const pid = Number.parseInt(readFileSync8(pidPath, "utf-8").trim(), 10);
1696
+ if (!Number.isFinite(pid) || pid <= 0) return true;
1697
+ try {
1698
+ process.kill(pid, 0);
1699
+ return true;
1700
+ } catch (err) {
1701
+ return err.code === "EPERM";
1702
+ }
1703
+ }
1316
1704
 
1317
1705
  // src/cli/lib/preflight.ts
1318
1706
  import { execFileSync } from "child_process";
@@ -1390,7 +1778,16 @@ function removeMacosJunk(dir) {
1390
1778
  }
1391
1779
 
1392
1780
  // src/cli/lib/port.ts
1781
+ import { createServer } from "net";
1393
1782
  var DEFAULT_PORT = 5173;
1783
+ function checkPortAvailable(port, host = "0.0.0.0") {
1784
+ return new Promise((resolve10) => {
1785
+ const srv = createServer();
1786
+ srv.once("error", () => resolve10(false));
1787
+ srv.once("listening", () => srv.close(() => resolve10(true)));
1788
+ srv.listen(port, host);
1789
+ });
1790
+ }
1394
1791
  function resolvePort(arg) {
1395
1792
  const raw = arg ?? process.env.DEEPSPACE_PORT;
1396
1793
  if (!raw) return DEFAULT_PORT;
@@ -1417,6 +1814,7 @@ var dev_default = defineCommand3({
1417
1814
  },
1418
1815
  env: {
1419
1816
  type: "string",
1817
+ alias: "e",
1420
1818
  description: "wrangler.toml [env.<name>] block to run (e.g. --env staging). Applies the env's overrides at build time.",
1421
1819
  required: false
1422
1820
  },
@@ -1474,7 +1872,9 @@ var dev_default = defineCommand3({
1474
1872
  );
1475
1873
  }
1476
1874
  }
1477
- writeLaunchConfigIfMissing(appDir, detectAppName(appDir) ?? basename(appDir), port);
1875
+ writeLaunchConfigIfMissing(appDir, detectAppName(appDir) ?? basename2(appDir), port, {
1876
+ updatePort: explicitPort
1877
+ });
1478
1878
  ensureInstallReady(appDir);
1479
1879
  let token;
1480
1880
  try {
@@ -1522,6 +1922,12 @@ var dev_default = defineCommand3({
1522
1922
  process.exit(1);
1523
1923
  }
1524
1924
  preflightWindowsWorkerd(appDir);
1925
+ if (!await checkPortAvailable(port)) {
1926
+ console.error(`Port ${port} is already in use.`);
1927
+ const killCmd = port === DEFAULT_PORT ? "deepspace kill" : `deepspace kill --port ${port}`;
1928
+ console.error(`Free it with \`${killCmd}\`, or start on another port: \`deepspace dev --port <other>\`.`);
1929
+ process.exit(1);
1930
+ }
1525
1931
  console.log("Starting dev server...\n");
1526
1932
  const vite = spawn("npx", ["vite", "--port", String(port), "--strictPort", "--host"], {
1527
1933
  cwd: appDir,
@@ -1544,9 +1950,16 @@ var dev_default = defineCommand3({
1544
1950
  // src/cli/commands/kill.ts
1545
1951
  import { defineCommand as defineCommand4 } from "citty";
1546
1952
  import { spawnSync } from "child_process";
1953
+ import { readFileSync as readFileSync9, readdirSync as readdirSync3, readlinkSync } from "fs";
1547
1954
  import { setTimeout as wait } from "timers/promises";
1548
1955
  var SIGTERM_GRACE_MS = 1500;
1549
1956
  var IS_WIN = process.platform === "win32";
1957
+ function pickKillPort(opts) {
1958
+ if (opts.explicit != null) return opts.explicit;
1959
+ if (opts.worktree != null) return opts.worktree;
1960
+ if (opts.env != null) return opts.env;
1961
+ return opts.appLaunch ?? DEFAULT_PORT;
1962
+ }
1550
1963
  var kill_default = defineCommand4({
1551
1964
  meta: {
1552
1965
  name: "kill",
@@ -1565,21 +1978,38 @@ var kill_default = defineCommand4({
1565
1978
  }
1566
1979
  },
1567
1980
  async run({ args }) {
1568
- const port = (args.port ? null : resolveWorktreePort(process.cwd())) ?? resolvePort(args.port);
1981
+ const cwd = process.cwd();
1982
+ const port = pickKillPort({
1983
+ explicit: args.port ? resolvePort(args.port) : null,
1984
+ worktree: resolveWorktreePort(cwd),
1985
+ // resolvePort() reads + validates $DEEPSPACE_PORT; only consult it when set.
1986
+ env: process.env.DEEPSPACE_PORT ? resolvePort() : null,
1987
+ appLaunch: resolveAppLaunchPort(findAppDir(cwd) ?? cwd)
1988
+ });
1569
1989
  const targets = /* @__PURE__ */ new Set();
1570
- for (const pid of listenerPids(port)) targets.add(pid);
1990
+ const listeners = enumerateListeners(port);
1991
+ for (const pid of listeners.pids) targets.add(pid);
1571
1992
  for (const pid of [...targets]) {
1572
1993
  for (const child of childPids(pid)) targets.add(child);
1573
1994
  }
1995
+ let swept = false;
1574
1996
  if (args.all) {
1575
- for (const pid of processPidsByName(["workerd", "wrangler", "vite"])) {
1576
- targets.add(pid);
1577
- }
1997
+ const sweep = sweepByName(["workerd", "wrangler", "vite"]);
1998
+ swept = sweep.swept;
1999
+ for (const pid of sweep.pids) targets.add(pid);
1578
2000
  }
1579
2001
  if (targets.size === 0) {
1580
- console.log(
1581
- args.all ? `Nothing listening on :${port} and no stray workerd/wrangler/vite processes found.` : `Nothing listening on :${port}. (Pass --all to sweep stray workerd/wrangler.)`
1582
- );
2002
+ const outcome = noTargetsMessage({
2003
+ enumerated: listeners.enumerated,
2004
+ swept,
2005
+ all: args.all,
2006
+ port
2007
+ });
2008
+ if (!outcome.ok) {
2009
+ console.error(outcome.message);
2010
+ process.exit(1);
2011
+ }
2012
+ console.log(outcome.message);
1583
2013
  return;
1584
2014
  }
1585
2015
  for (const pid of targets) {
@@ -1605,16 +2035,101 @@ var kill_default = defineCommand4({
1605
2035
  );
1606
2036
  process.exit(1);
1607
2037
  }
1608
- console.log(`Port ${port} is free.`);
2038
+ if (args.all) {
2039
+ console.log(`Killed ${targets.size} process(es) (workerd/wrangler/vite sweep). :${port} is free.`);
2040
+ } else {
2041
+ console.log(`Port ${port} is free.`);
2042
+ }
1609
2043
  }
1610
2044
  });
1611
- function listenerPids(port) {
2045
+ function noTargetsMessage(opts) {
2046
+ const { enumerated, swept, all, port } = opts;
2047
+ const couldObserve = enumerated || all && swept;
2048
+ if (!couldObserve) {
2049
+ return {
2050
+ ok: false,
2051
+ message: `Couldn't determine what's running on :${port}: no lsof/pgrep and /proc is unavailable, so nothing was inspected or killed. Install lsof (or procps for pgrep), or find and kill the process manually.`
2052
+ };
2053
+ }
2054
+ return {
2055
+ ok: true,
2056
+ message: all ? `Nothing listening on :${port} and no stray workerd/wrangler/vite processes found.` : `Nothing listening on :${port}. (Pass --all to sweep stray workerd/wrangler.)`
2057
+ };
2058
+ }
2059
+ function enumerateListeners(port) {
1612
2060
  if (IS_WIN) {
1613
- return runPowershell(
2061
+ const r = runPowershellChecked(
1614
2062
  `Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess`
1615
2063
  );
2064
+ return { pids: r.pids, enumerated: r.ran };
2065
+ }
2066
+ const lsof = runLsof(port);
2067
+ if (lsof.ran) return { pids: lsof.pids, enumerated: true };
2068
+ const proc = listenerPidsViaProc(port);
2069
+ return { pids: proc.pids, enumerated: proc.available };
2070
+ }
2071
+ function listenerPids(port) {
2072
+ return enumerateListeners(port).pids;
2073
+ }
2074
+ function runLsof(port) {
2075
+ try {
2076
+ const r = spawnSync("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], {
2077
+ encoding: "utf-8"
2078
+ });
2079
+ if (r.error) return { pids: [], ran: false };
2080
+ return { pids: r.status === 0 ? parsePidLines(r.stdout) : [], ran: true };
2081
+ } catch {
2082
+ return { pids: [], ran: false };
2083
+ }
2084
+ }
2085
+ function listenerPidsViaProc(port) {
2086
+ let available = false;
2087
+ try {
2088
+ const hexPort = port.toString(16).toUpperCase().padStart(4, "0");
2089
+ const inodes = /* @__PURE__ */ new Set();
2090
+ for (const f of ["/proc/net/tcp", "/proc/net/tcp6"]) {
2091
+ let data;
2092
+ try {
2093
+ data = readFileSync9(f, "utf-8");
2094
+ } catch {
2095
+ continue;
2096
+ }
2097
+ available = true;
2098
+ for (const line of data.split("\n").slice(1)) {
2099
+ const cols = line.trim().split(/\s+/);
2100
+ if (cols[3] !== "0A") continue;
2101
+ if (cols[1]?.split(":")[1] === hexPort && cols[9]) inodes.add(cols[9]);
2102
+ }
2103
+ }
2104
+ if (!available) return { pids: [], available: false };
2105
+ if (!inodes.size) return { pids: [], available: true };
2106
+ const pids = /* @__PURE__ */ new Set();
2107
+ for (const pidDir of readdirSync3("/proc")) {
2108
+ if (!/^\d+$/.test(pidDir)) continue;
2109
+ let fds;
2110
+ try {
2111
+ fds = readdirSync3(`/proc/${pidDir}/fd`);
2112
+ } catch {
2113
+ continue;
2114
+ }
2115
+ for (const fd of fds) {
2116
+ let link;
2117
+ try {
2118
+ link = readlinkSync(`/proc/${pidDir}/fd/${fd}`);
2119
+ } catch {
2120
+ continue;
2121
+ }
2122
+ const m = link.match(/^socket:\[(\d+)\]$/);
2123
+ if (m && inodes.has(m[1])) {
2124
+ pids.add(Number(pidDir));
2125
+ break;
2126
+ }
2127
+ }
2128
+ }
2129
+ return { pids: [...pids], available: true };
2130
+ } catch {
2131
+ return { pids: [], available };
1616
2132
  }
1617
- return run(["lsof", "-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"]);
1618
2133
  }
1619
2134
  function childPids(parentPid) {
1620
2135
  if (IS_WIN) {
@@ -1624,22 +2139,31 @@ function childPids(parentPid) {
1624
2139
  }
1625
2140
  return run(["pgrep", "-P", String(parentPid)]);
1626
2141
  }
1627
- function processPidsByName(names) {
2142
+ function sweepByName(names) {
1628
2143
  const all = /* @__PURE__ */ new Set();
1629
2144
  if (IS_WIN) {
1630
2145
  const orClauses = names.map((n) => `($_.Name -like '*${n}*') -or ($_.CommandLine -like '*${n}*')`).join(" -or ");
1631
2146
  const script = `Get-CimInstance Win32_Process | Where-Object { ${orClauses} } | Select-Object -ExpandProperty ProcessId`;
1632
- for (const pid of runPowershell(script)) {
1633
- if (pid !== process.pid) all.add(pid);
1634
- }
1635
- return [...all];
2147
+ const r = runPowershellChecked(script);
2148
+ for (const pid of r.pids) if (pid !== process.pid) all.add(pid);
2149
+ return { pids: [...all], swept: r.ran };
1636
2150
  }
2151
+ let swept = false;
1637
2152
  for (const name of names) {
1638
- for (const pid of run(["pgrep", "-f", name])) {
1639
- if (pid !== process.pid) all.add(pid);
1640
- }
2153
+ const r = runPgrep(["-f", name]);
2154
+ if (r.ran) swept = true;
2155
+ for (const pid of r.pids) if (pid !== process.pid) all.add(pid);
2156
+ }
2157
+ return { pids: [...all], swept };
2158
+ }
2159
+ function runPgrep(pgrepArgs) {
2160
+ try {
2161
+ const r = spawnSync("pgrep", pgrepArgs, { encoding: "utf-8" });
2162
+ if (r.error) return { pids: [], ran: false };
2163
+ return { pids: r.status === 0 ? parsePidLines(r.stdout) : [], ran: r.status === 0 || r.status === 1 };
2164
+ } catch {
2165
+ return { pids: [], ran: false };
1641
2166
  }
1642
- return [...all];
1643
2167
  }
1644
2168
  function run(cmd) {
1645
2169
  try {
@@ -1651,15 +2175,18 @@ function run(cmd) {
1651
2175
  }
1652
2176
  }
1653
2177
  function runPowershell(script) {
2178
+ return runPowershellChecked(script).pids;
2179
+ }
2180
+ function runPowershellChecked(script) {
1654
2181
  const exe = pwshOrPowershell();
1655
2182
  try {
1656
2183
  const r = spawnSync(exe, ["-NoProfile", "-NonInteractive", "-Command", script], {
1657
2184
  encoding: "utf-8"
1658
2185
  });
1659
- if (r.status !== 0) return [];
1660
- return parsePidLines(r.stdout);
2186
+ if (r.error) return { pids: [], ran: false };
2187
+ return { pids: r.status === 0 ? parsePidLines(r.stdout) : [], ran: r.status === 0 };
1661
2188
  } catch {
1662
- return [];
2189
+ return { pids: [], ran: false };
1663
2190
  }
1664
2191
  }
1665
2192
  function pwshOrPowershell() {
@@ -1738,6 +2265,7 @@ var test_default = defineCommand5({
1738
2265
  },
1739
2266
  env: {
1740
2267
  type: "string",
2268
+ alias: "e",
1741
2269
  description: "wrangler.toml [env.<name>] block to test (uses secrets config <name> by default).",
1742
2270
  required: false
1743
2271
  }
@@ -1773,8 +2301,8 @@ var test_default = defineCommand5({
1773
2301
  token = await ensureToken();
1774
2302
  const payload = decodeJwtPayload(token);
1775
2303
  ownerId = payload.sub;
1776
- } catch {
1777
- console.error("`deepspace test` requires you to be logged in. Run `deepspace login` first.");
2304
+ } catch (err) {
2305
+ console.error(err instanceof Error ? err.message : "Not logged in. Run `deepspace login` first.");
1778
2306
  process.exit(1);
1779
2307
  }
1780
2308
  let generatedSecretsCache;
@@ -1935,7 +2463,7 @@ var screenshot_default = defineCommand6({
1935
2463
  // src/cli/commands/test-accounts.ts
1936
2464
  import { defineCommand as defineCommand7 } from "citty";
1937
2465
  import * as p3 from "@clack/prompts";
1938
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync8, existsSync as existsSync9, mkdirSync as mkdirSync4 } from "fs";
2466
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync8, existsSync as existsSync9, mkdirSync as mkdirSync4 } from "fs";
1939
2467
  import { join as join10 } from "path";
1940
2468
  import { homedir as homedir4 } from "os";
1941
2469
  var SESSION_COOKIE2 = "__Secure-better-auth.session_token";
@@ -1945,7 +2473,7 @@ var ACCOUNTS_PATH = join10(DIR3, "test-accounts.json");
1945
2473
  function loadAccounts() {
1946
2474
  if (!existsSync9(ACCOUNTS_PATH)) return [];
1947
2475
  try {
1948
- return JSON.parse(readFileSync8(ACCOUNTS_PATH, "utf-8"));
2476
+ return JSON.parse(readFileSync10(ACCOUNTS_PATH, "utf-8"));
1949
2477
  } catch {
1950
2478
  return [];
1951
2479
  }
@@ -1955,7 +2483,7 @@ function saveAccounts(accounts) {
1955
2483
  writeFileSync8(ACCOUNTS_PATH, JSON.stringify(accounts, null, 2), { mode: 384 });
1956
2484
  }
1957
2485
  function sessionCookie() {
1958
- const token = readFileSync8(SESSION_PATH2, "utf-8").trim();
2486
+ const token = readFileSync10(SESSION_PATH2, "utf-8").trim();
1959
2487
  return `${SESSION_COOKIE2}=${encodeURIComponent(token)}`;
1960
2488
  }
1961
2489
  async function fetchRemoteAccounts() {
@@ -2225,214 +2753,10 @@ var test_accounts_default = defineCommand7({
2225
2753
  import { defineCommand as defineCommand8 } from "citty";
2226
2754
  import { execSync as execSync2 } from "child_process";
2227
2755
  import { randomUUID } from "crypto";
2228
- import { readFileSync as readFileSync9, readdirSync as readdirSync3, existsSync as existsSync10, writeFileSync as writeFileSync9 } from "fs";
2756
+ import { readFileSync as readFileSync11, readdirSync as readdirSync4, existsSync as existsSync10 } from "fs";
2229
2757
  import { join as join11, resolve as resolve7, dirname as dirname2 } from "path";
2230
2758
  import { pathToFileURL } from "url";
2231
2759
  import * as p4 from "@clack/prompts";
2232
-
2233
- // src/server/rooms/binding-manifest.ts
2234
- var AUTO_PROVISION_SENTINEL = "auto";
2235
- var VECTORIZE_METRICS = /* @__PURE__ */ new Set(["cosine", "euclidean", "dot-product"]);
2236
- var ALLOWED_BINDING_TYPES = /* @__PURE__ */ new Set([
2237
- "vectorize",
2238
- "ai",
2239
- "r2_bucket",
2240
- "kv_namespace",
2241
- "d1",
2242
- "queue",
2243
- "browser_rendering",
2244
- "analytics_engine",
2245
- "hyperdrive"
2246
- ]);
2247
- var RESERVED_BINDING_NAMES = /* @__PURE__ */ new Set([
2248
- "ASSETS",
2249
- "PLATFORM_WORKER",
2250
- "API_WORKER",
2251
- "APP_NAME",
2252
- "OWNER_USER_ID",
2253
- "AUTH_JWT_PUBLIC_KEY",
2254
- "AUTH_JWT_ISSUER",
2255
- "AUTH_WORKER_URL",
2256
- "APP_IDENTITY_TOKEN",
2257
- "APP_OWNER_JWT",
2258
- "USAGE_EVENTS"
2259
- ]);
2260
- function validateBindingManifest(manifest) {
2261
- if (!Array.isArray(manifest)) {
2262
- return { valid: false, errors: [{ reason: "Manifest must be an array" }] };
2263
- }
2264
- const errors = [];
2265
- const seenNames = /* @__PURE__ */ new Set();
2266
- for (const entry of manifest) {
2267
- if (!entry || typeof entry !== "object") {
2268
- errors.push({ reason: `Entry must be an object (got ${entry === null ? "null" : typeof entry})` });
2269
- continue;
2270
- }
2271
- const e = entry;
2272
- const type = e.type;
2273
- const name = e.name;
2274
- if (typeof type !== "string" || !ALLOWED_BINDING_TYPES.has(type)) {
2275
- errors.push({
2276
- reason: `Disallowed binding type: ${typeof type === "string" ? type : `(${typeof type})`}. Allowed: ${[...ALLOWED_BINDING_TYPES].sort().join(", ")}`
2277
- });
2278
- continue;
2279
- }
2280
- if (typeof name !== "string" || !name) {
2281
- errors.push({ reason: `Binding of type "${type}" missing 'name' (string)` });
2282
- continue;
2283
- }
2284
- if (RESERVED_BINDING_NAMES.has(name)) {
2285
- errors.push({ reason: `Binding name "${name}" is reserved by the SDK` });
2286
- continue;
2287
- }
2288
- if (seenNames.has(name)) {
2289
- errors.push({ reason: `Duplicate binding name "${name}"` });
2290
- continue;
2291
- }
2292
- seenNames.add(name);
2293
- const binding = entry;
2294
- const fieldErr = requiredFieldError(binding);
2295
- if (fieldErr) errors.push({ binding, reason: fieldErr });
2296
- }
2297
- if (errors.length) return { valid: false, errors };
2298
- return { valid: true, bindings: manifest };
2299
- }
2300
- function requiredFieldError(b) {
2301
- switch (b.type) {
2302
- case "vectorize": {
2303
- if (!b.index_name) return `vectorize binding "${b.name}" missing index_name (or "auto")`;
2304
- if (b.index_name === AUTO_PROVISION_SENTINEL) {
2305
- if (typeof b.dimensions !== "number" || b.dimensions <= 0) {
2306
- return `vectorize binding "${b.name}" with index_name="auto" requires "dimensions" (positive number)`;
2307
- }
2308
- if (!b.metric || !VECTORIZE_METRICS.has(b.metric)) {
2309
- return `vectorize binding "${b.name}" with index_name="auto" requires "metric" (one of: ${[...VECTORIZE_METRICS].join(", ")})`;
2310
- }
2311
- }
2312
- return null;
2313
- }
2314
- case "r2_bucket":
2315
- return b.bucket_name ? null : `r2_bucket binding "${b.name}" missing bucket_name (or "auto")`;
2316
- case "kv_namespace": {
2317
- if (!b.namespace_id) return `kv_namespace binding "${b.name}" missing namespace_id (or "auto")`;
2318
- if (b.namespace_id === AUTO_PROVISION_SENTINEL && !b.title) {
2319
- return `kv_namespace binding "${b.name}" with namespace_id="auto" requires "title"`;
2320
- }
2321
- return null;
2322
- }
2323
- case "d1": {
2324
- if (!b.id) return `d1 binding "${b.name}" missing id (or "auto")`;
2325
- if (b.id === AUTO_PROVISION_SENTINEL && !b.database_name) {
2326
- return `d1 binding "${b.name}" with id="auto" requires "database_name"`;
2327
- }
2328
- return null;
2329
- }
2330
- case "queue":
2331
- return b.queue_name ? null : `queue binding "${b.name}" missing queue_name (or "auto")`;
2332
- case "hyperdrive":
2333
- if (!b.id) return `hyperdrive binding "${b.name}" missing id`;
2334
- if (b.id === AUTO_PROVISION_SENTINEL) {
2335
- return `hyperdrive binding "${b.name}": auto-provisioning not yet supported (provide a real id)`;
2336
- }
2337
- return null;
2338
- case "ai":
2339
- case "browser_rendering":
2340
- case "analytics_engine":
2341
- return null;
2342
- // no required fields beyond `name`
2343
- default: {
2344
- const _exhaustive = b;
2345
- void _exhaustive;
2346
- return null;
2347
- }
2348
- }
2349
- }
2350
- function bindingManifestFromOutputConfig(outputConfig) {
2351
- const out = [];
2352
- const aiBinding = pluckString(outputConfig.ai, "binding");
2353
- if (aiBinding) out.push({ type: "ai", name: aiBinding });
2354
- for (const v of asObjectArray(outputConfig.vectorize)) {
2355
- const name = pluckString(v, "binding");
2356
- const index_name = pluckString(v, "index_name");
2357
- if (!name || !index_name) continue;
2358
- const dimensions = pluckNumber(v, "dimensions");
2359
- const metric = pluckString(v, "metric");
2360
- out.push({
2361
- type: "vectorize",
2362
- name,
2363
- index_name,
2364
- ...dimensions != null && { dimensions },
2365
- ...metric && (metric === "cosine" || metric === "euclidean" || metric === "dot-product") && { metric }
2366
- });
2367
- }
2368
- for (const r of asObjectArray(outputConfig.r2_buckets)) {
2369
- const name = pluckString(r, "binding");
2370
- const bucket_name = pluckString(r, "bucket_name");
2371
- if (name && bucket_name) out.push({ type: "r2_bucket", name, bucket_name });
2372
- }
2373
- for (const k of asObjectArray(outputConfig.kv_namespaces)) {
2374
- const name = pluckString(k, "binding");
2375
- const namespace_id = pluckString(k, "id");
2376
- if (!name || !namespace_id) continue;
2377
- const title = pluckString(k, "title");
2378
- out.push({
2379
- type: "kv_namespace",
2380
- name,
2381
- namespace_id,
2382
- ...title && { title }
2383
- });
2384
- }
2385
- for (const d of asObjectArray(outputConfig.d1_databases)) {
2386
- const name = pluckString(d, "binding");
2387
- const id = pluckString(d, "database_id");
2388
- if (!name || !id) continue;
2389
- const database_name = pluckString(d, "database_name");
2390
- out.push({
2391
- type: "d1",
2392
- name,
2393
- id,
2394
- ...database_name && { database_name }
2395
- });
2396
- }
2397
- const queues = isObject(outputConfig.queues) ? outputConfig.queues : null;
2398
- for (const q of asObjectArray(queues?.producers)) {
2399
- const name = pluckString(q, "binding");
2400
- const queue_name = pluckString(q, "queue");
2401
- if (name && queue_name) out.push({ type: "queue", name, queue_name });
2402
- }
2403
- const browserBinding = pluckString(outputConfig.browser, "binding");
2404
- if (browserBinding) out.push({ type: "browser_rendering", name: browserBinding });
2405
- for (const a of asObjectArray(outputConfig.analytics_engine_datasets)) {
2406
- const name = pluckString(a, "binding");
2407
- const dataset = pluckString(a, "dataset");
2408
- if (name) out.push({ type: "analytics_engine", name, dataset: dataset ?? void 0 });
2409
- }
2410
- for (const h of asObjectArray(outputConfig.hyperdrive)) {
2411
- const name = pluckString(h, "binding");
2412
- const id = pluckString(h, "id");
2413
- if (name && id) out.push({ type: "hyperdrive", name, id });
2414
- }
2415
- return out;
2416
- }
2417
- function isObject(v) {
2418
- return v !== null && typeof v === "object" && !Array.isArray(v);
2419
- }
2420
- function pluckString(v, key) {
2421
- if (!isObject(v)) return null;
2422
- const val = v[key];
2423
- return typeof val === "string" && val.length > 0 ? val : null;
2424
- }
2425
- function pluckNumber(v, key) {
2426
- if (!isObject(v)) return null;
2427
- const val = v[key];
2428
- return typeof val === "number" && Number.isFinite(val) ? val : null;
2429
- }
2430
- function asObjectArray(v) {
2431
- if (!Array.isArray(v)) return [];
2432
- return v.filter(isObject);
2433
- }
2434
-
2435
- // src/cli/commands/deploy.ts
2436
2760
  var DEPLOY_URL3 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
2437
2761
  var API_URL2 = process.env.DEEPSPACE_API_URL ?? PLATFORM_URLS.api;
2438
2762
  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.";
@@ -2486,6 +2810,13 @@ var GROUP_BYTES = (() => {
2486
2810
  const v = Number.isInteger(raw) && raw > 0 ? raw : 3 * 1024 * 1024;
2487
2811
  return Math.min(v, MAX_GROUP_BYTES);
2488
2812
  })();
2813
+ function classifyDevVarsSecrets(opts) {
2814
+ const store = new Set(opts.storeSecretNames);
2815
+ const strayKeys = opts.handEditedDevVarKeys.filter((k) => !store.has(k));
2816
+ if (strayKeys.length === 0) return { kind: "ok" };
2817
+ if (opts.storeSecretNames.length === 0 && !opts.allowMissing) return { kind: "block", strayKeys };
2818
+ return { kind: "warn", strayKeys };
2819
+ }
2489
2820
  function packAssetGroups(assets, maxBytes) {
2490
2821
  const groups = [];
2491
2822
  let current = [];
@@ -2549,6 +2880,7 @@ var deploy_default = defineCommand8({
2549
2880
  },
2550
2881
  env: {
2551
2882
  type: "string",
2883
+ alias: "e",
2552
2884
  description: "wrangler.toml [env.<name>] block to deploy (e.g. --env staging). Omit to deploy the top-level config.",
2553
2885
  required: false
2554
2886
  },
@@ -2556,6 +2888,11 @@ var deploy_default = defineCommand8({
2556
2888
  type: "boolean",
2557
2889
  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.",
2558
2890
  default: false
2891
+ },
2892
+ "allow-missing-secrets": {
2893
+ type: "boolean",
2894
+ description: "Deploy even though hand-edited .dev.vars secrets are absent from the store (they will NOT be deployed, and any a previous deploy set are dropped).",
2895
+ default: false
2559
2896
  }
2560
2897
  },
2561
2898
  async run({ args }) {
@@ -2603,11 +2940,26 @@ var deploy_default = defineCommand8({
2603
2940
  );
2604
2941
  }
2605
2942
  p4.log.info(`Id: ${appId}`);
2943
+ const readCustomDevVarsAt = (path) => {
2944
+ if (!existsSync10(path)) return {};
2945
+ const section = extractCustomDevVars(stripGeneratedSecretsCache(readFileSync11(path, "utf-8")));
2946
+ if (!section) return {};
2947
+ try {
2948
+ return parseDevVars(section);
2949
+ } catch (err) {
2950
+ p4.cancel(`.dev.vars: ${errMessage(err)}`);
2951
+ process.exit(1);
2952
+ }
2953
+ };
2606
2954
  let linkedSecrets = null;
2955
+ let localOnlyDevVars = {};
2607
2956
  try {
2608
2957
  const refreshed = await refreshSecretsCache(DEPLOY_URL3, token, appId, envName);
2609
2958
  if (refreshed) {
2610
2959
  linkedSecrets = refreshed.pulled;
2960
+ localOnlyDevVars = readCustomDevVarsAt(
2961
+ devVarsPathFor(appDir, envName, { sharedDevVarsCache: true })
2962
+ );
2611
2963
  await writeDevVars(appDir, ownerId, token, envName, {
2612
2964
  appId,
2613
2965
  generatedSecretsCache: refreshed.rendered,
@@ -2616,7 +2968,14 @@ var deploy_default = defineCommand8({
2616
2968
  p4.log.info(refreshed.summary);
2617
2969
  }
2618
2970
  } catch (err) {
2619
- p4.cancel(`Failed to refresh app secrets before deploy: ${errMessage(err)}`);
2971
+ const msg = errMessage(err);
2972
+ if (err?.status === 403) {
2973
+ p4.cancel(
2974
+ "You're not authorized to deploy this app \u2014 you must be its owner or a current collaborator. If you were a collaborator, your access may have been revoked."
2975
+ );
2976
+ } else {
2977
+ p4.cancel(`Failed to refresh app secrets before deploy: ${msg}`);
2978
+ }
2620
2979
  process.exit(1);
2621
2980
  }
2622
2981
  const junk = removeMacosJunk(appDir);
@@ -2648,13 +3007,13 @@ var deploy_default = defineCommand8({
2648
3007
  p4.cancel("Build output config not found at .wrangler/deploy/config.json");
2649
3008
  process.exit(1);
2650
3009
  }
2651
- const deployConfig = JSON.parse(readFileSync9(deployConfigPath, "utf-8"));
3010
+ const deployConfig = JSON.parse(readFileSync11(deployConfigPath, "utf-8"));
2652
3011
  const outputWranglerPath = resolve7(dirname2(deployConfigPath), deployConfig.configPath);
2653
3012
  if (!existsSync10(outputWranglerPath)) {
2654
3013
  p4.cancel(`Output wrangler.json not found at ${outputWranglerPath}`);
2655
3014
  process.exit(1);
2656
3015
  }
2657
- const outputConfig = JSON.parse(readFileSync9(outputWranglerPath, "utf-8"));
3016
+ const outputConfig = JSON.parse(readFileSync11(outputWranglerPath, "utf-8"));
2658
3017
  if (outputConfig.name && outputConfig.name !== appName) {
2659
3018
  const envHint = envName ? ` (--env ${envName})` : "";
2660
3019
  p4.cancel(
@@ -2676,7 +3035,7 @@ var deploy_default = defineCommand8({
2676
3035
  s.start("Collecting assets...");
2677
3036
  const assets = collectAssets(clientDir);
2678
3037
  s.stop(`Collected ${assets.length} assets`);
2679
- const workerJs = readFileSync9(workerBundlePath, "utf-8");
3038
+ const workerJs = readFileSync11(workerBundlePath, "utf-8");
2680
3039
  const doBindings = outputConfig.durable_objects?.bindings;
2681
3040
  const sqliteClasses = new Set(
2682
3041
  outputConfig.migrations?.flatMap(
@@ -2708,18 +3067,30 @@ var deploy_default = defineCommand8({
2708
3067
  const devVarsPath = devVarsPathFor(appDir, envName, {
2709
3068
  sharedDevVarsCache: linkedSecrets !== null
2710
3069
  });
2711
- let userSecrets = {};
2712
- if (existsSync10(devVarsPath)) {
2713
- const rawDevVars = readFileSync9(devVarsPath, "utf-8");
2714
- const customSection = extractCustomDevVars(rawDevVars);
2715
- if (customSection) {
2716
- try {
2717
- userSecrets = parseDevVars(customSection);
2718
- } catch (err) {
2719
- p4.cancel(`.dev.vars: ${errMessage(err)}`);
2720
- process.exit(1);
2721
- }
2722
- }
3070
+ const userSecrets = linkedSecrets ? { ...linkedSecrets.values } : {};
3071
+ const handEditedDevVars = linkedSecrets ? localOnlyDevVars : readCustomDevVarsAt(devVarsPath);
3072
+ const secretsDecision = classifyDevVarsSecrets({
3073
+ storeSecretNames: Object.keys(userSecrets),
3074
+ handEditedDevVarKeys: Object.keys(handEditedDevVars),
3075
+ allowMissing: Boolean(args["allow-missing-secrets"])
3076
+ });
3077
+ if (secretsDecision.kind === "block") {
3078
+ const uploadCmd = envName ? `deepspace secrets upload .dev.vars -e ${envName}` : "deepspace secrets upload .dev.vars";
3079
+ const keys = secretsDecision.strayKeys.join(", ");
3080
+ p4.cancel(
3081
+ `Refusing to deploy: the app store has no secrets, but .dev.vars has ${keys}.
3082
+ Deploying now would ship the app with no secrets \u2014 replacing any a previous deploy set (they would be dropped from production).
3083
+ Upload them to the store first:
3084
+ ${uploadCmd}
3085
+ then redeploy. (Pass --allow-missing-secrets to deploy without them.)`
3086
+ );
3087
+ process.exit(1);
3088
+ }
3089
+ if (secretsDecision.kind === "warn") {
3090
+ const stray = secretsDecision.strayKeys;
3091
+ p4.log.warn(
3092
+ `.dev.vars has ${stray.join(", ")} not in the app store \u2014 ${stray.length === 1 ? "it is" : "they are"} NOT deployed. \`.dev.vars\` is a local dev cache, not deploy config. Move ${stray.length === 1 ? "it" : "them"} into the store with \`deepspace secrets set KEY=value\` (or \`deepspace secrets upload .dev.vars\`), then redeploy.`
3093
+ );
2723
3094
  }
2724
3095
  const userSecretNames = Object.keys(userSecrets);
2725
3096
  const declaredBindingNames = /* @__PURE__ */ new Set([
@@ -2729,22 +3100,19 @@ var deploy_default = defineCommand8({
2729
3100
  for (const name of userSecretNames) {
2730
3101
  if (RESERVED_BINDING_NAMES.has(name)) {
2731
3102
  p4.cancel(
2732
- `.dev.vars: "${name}" is a reserved binding name and cannot be used as an app secret.`
3103
+ `App secret "${name}" is a reserved binding name \u2014 remove it with \`deepspace secrets delete ${name}\`.`
2733
3104
  );
2734
3105
  process.exit(1);
2735
3106
  }
2736
3107
  if (declaredBindingNames.has(name)) {
2737
3108
  p4.cancel(
2738
- `.dev.vars: "${name}" collides with a binding declared in wrangler.toml \u2014 rename one or the other.`
3109
+ `App secret "${name}" collides with a binding declared in wrangler.toml \u2014 rename one or the other.`
2739
3110
  );
2740
3111
  process.exit(1);
2741
3112
  }
2742
3113
  }
2743
3114
  if (userSecretNames.length) {
2744
- p4.log.info(`App secrets (from .dev.vars): ${userSecretNames.join(", ")}`);
2745
- if (!linkedSecrets) {
2746
- p4.log.info("Not in the app store yet \u2014 migrating after this deploy.");
2747
- }
3115
+ p4.log.info(`App secrets: ${userSecretNames.join(", ")}`);
2748
3116
  }
2749
3117
  s.start(`Deploying to ${appName}.app.space...`);
2750
3118
  const extraRoutes = extractRunWorkerFirst(wranglerConfig);
@@ -2819,6 +3187,7 @@ var deploy_default = defineCommand8({
2819
3187
  if (userSecretNames.length) {
2820
3188
  form.append("userSecrets", JSON.stringify(userSecrets));
2821
3189
  }
3190
+ form.append("secretsAuthoritative", "true");
2822
3191
  if (extraRoutes.length) {
2823
3192
  form.append("extraRunWorkerFirst", JSON.stringify(extraRoutes));
2824
3193
  }
@@ -2848,6 +3217,12 @@ var deploy_default = defineCommand8({
2848
3217
  let body = await res.json().catch(() => ({}));
2849
3218
  if (res.status === 409 && body.code === "rename_required" && !confirmRename) {
2850
3219
  s.stop("Rename confirmation needed");
3220
+ if (!process.stdin.isTTY) {
3221
+ await bailDeploy(
3222
+ `This deploy renames the app: ${body.fromHost} \u2192 ${body.toHost}. Confirmation needs an interactive terminal \u2014 re-run with --rename to approve the rename, or \`deepspace init --new-id\` if you meant a separate app.`,
3223
+ null
3224
+ );
3225
+ }
2851
3226
  const yes = await p4.confirm({
2852
3227
  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?`
2853
3228
  });
@@ -2874,16 +3249,6 @@ var deploy_default = defineCommand8({
2874
3249
  "First requests may briefly hit the assets transitional page; retry in a minute."
2875
3250
  );
2876
3251
  p4.log.success(`Live at: ${body.url}`);
2877
- await migrateLegacyDevVarsSecrets({
2878
- deployUrl: DEPLOY_URL3,
2879
- token,
2880
- appId,
2881
- envName,
2882
- devVarsPath,
2883
- userSecrets,
2884
- userSecretNames,
2885
- hadStore: linkedSecrets !== null
2886
- });
2887
3252
  await syncSubscriptionPlans(appDir, appId, token);
2888
3253
  await syncOneTimeProducts(appDir, appId, token);
2889
3254
  p4.outro("Done");
@@ -2892,41 +3257,11 @@ var deploy_default = defineCommand8({
2892
3257
  }
2893
3258
  s.stop("Deployed!");
2894
3259
  p4.log.success(`Live at: ${body.url}`);
2895
- await migrateLegacyDevVarsSecrets({
2896
- deployUrl: DEPLOY_URL3,
2897
- token,
2898
- appId,
2899
- envName,
2900
- devVarsPath,
2901
- userSecrets,
2902
- userSecretNames,
2903
- hadStore: linkedSecrets !== null
2904
- });
2905
3260
  await syncSubscriptionPlans(appDir, appId, token);
2906
3261
  await syncOneTimeProducts(appDir, appId, token);
2907
3262
  p4.outro("Done");
2908
3263
  }
2909
3264
  });
2910
- async function migrateLegacyDevVarsSecrets(opts) {
2911
- const { deployUrl, token, appId, envName, devVarsPath, userSecrets, userSecretNames, hadStore } = opts;
2912
- if (hadStore || userSecretNames.length === 0) return;
2913
- const configName = defaultConfigNameForEnv(envName);
2914
- try {
2915
- await uploadSecrets(deployUrl, token, appId, configName, userSecrets, false);
2916
- if (existsSync10(devVarsPath)) {
2917
- const cleaned = removeDevVarsKeys(readFileSync9(devVarsPath, "utf-8"), new Set(userSecretNames));
2918
- writeFileSync9(devVarsPath, cleaned.endsWith("\n") ? cleaned : `${cleaned}
2919
- `);
2920
- }
2921
- p4.log.success(
2922
- `Migrated ${userSecretNames.length} secret${userSecretNames.length === 1 ? "" : "s"} from .dev.vars into the app store (config ${configName}) \u2014 now managed by \`deepspace secrets\`.`
2923
- );
2924
- } catch (err) {
2925
- p4.log.warn(
2926
- `Couldn't migrate .dev.vars secrets into the app store (${errMessage(err)}); they deployed as bindings for now and the next deploy will retry.`
2927
- );
2928
- }
2929
- }
2930
3265
  async function syncSubscriptionPlans(appDir, appName, token) {
2931
3266
  const subsPath = join11(appDir, "src", "subscriptions.ts");
2932
3267
  if (!existsSync10(subsPath)) return;
@@ -3155,7 +3490,7 @@ async function waitForAssetsReady(url, timeoutMs) {
3155
3490
  function collectAssets(dir) {
3156
3491
  const assets = [];
3157
3492
  function walk(d, prefix) {
3158
- for (const entry of readdirSync3(d, { withFileTypes: true })) {
3493
+ for (const entry of readdirSync4(d, { withFileTypes: true })) {
3159
3494
  if (entry.name === ".assetsignore") continue;
3160
3495
  const full = join11(d, entry.name);
3161
3496
  const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
@@ -3164,7 +3499,7 @@ function collectAssets(dir) {
3164
3499
  } else {
3165
3500
  assets.push({
3166
3501
  path: "/" + rel,
3167
- contentBase64: readFileSync9(full).toString("base64")
3502
+ contentBase64: readFileSync11(full).toString("base64")
3168
3503
  });
3169
3504
  }
3170
3505
  }
@@ -3185,7 +3520,7 @@ var undeploy_default = defineCommand9({
3185
3520
  args: {
3186
3521
  name: {
3187
3522
  type: "positional",
3188
- description: "App id to undeploy (reads DEEPSPACE_APP_ID from wrangler.toml if omitted)",
3523
+ description: "App id or subdomain name to undeploy (reads DEEPSPACE_APP_ID from wrangler.toml if omitted)",
3189
3524
  required: false
3190
3525
  },
3191
3526
  env: {
@@ -3196,18 +3531,6 @@ var undeploy_default = defineCommand9({
3196
3531
  },
3197
3532
  async run({ args }) {
3198
3533
  const envName = typeof args.env === "string" && args.env.trim() ? args.env.trim() : void 0;
3199
- let appId = args.name;
3200
- if (appId && !APP_ID_RE.test(appId)) {
3201
- 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.`);
3202
- process.exit(1);
3203
- }
3204
- if (!appId && hasWranglerConfig(process.cwd())) {
3205
- appId = readAppId(process.cwd(), envName) ?? void 0;
3206
- }
3207
- if (!appId) {
3208
- p5.cancel("Provide an app id or run from a DeepSpace app directory with a DEEPSPACE_APP_ID.");
3209
- process.exit(1);
3210
- }
3211
3534
  let token;
3212
3535
  try {
3213
3536
  token = await ensureToken();
@@ -3215,6 +3538,22 @@ var undeploy_default = defineCommand9({
3215
3538
  p5.cancel(err instanceof Error ? err.message : String(err));
3216
3539
  process.exit(1);
3217
3540
  }
3541
+ let appId;
3542
+ const positional = typeof args.name === "string" && args.name.trim() ? args.name.trim() : void 0;
3543
+ if (positional) {
3544
+ try {
3545
+ appId = await resolveAppSelector(DEPLOY_URL4, token, positional);
3546
+ } catch (err) {
3547
+ p5.cancel(err instanceof Error ? err.message : String(err));
3548
+ process.exit(1);
3549
+ }
3550
+ } else if (hasWranglerConfig(process.cwd())) {
3551
+ appId = readAppId(process.cwd(), envName) ?? void 0;
3552
+ }
3553
+ if (!appId) {
3554
+ p5.cancel("Provide an app id or subdomain name, or run from a DeepSpace app directory with a DEEPSPACE_APP_ID.");
3555
+ process.exit(1);
3556
+ }
3218
3557
  p5.intro(`Undeploying ${appId}`);
3219
3558
  const s = createSpinner();
3220
3559
  s.start("Removing...");
@@ -3251,24 +3590,28 @@ var whoami_default = defineCommand10({
3251
3590
  }
3252
3591
  },
3253
3592
  async run({ args }) {
3593
+ const failLoggedOut = (msg) => {
3594
+ if (args.json) console.log(JSON.stringify({ loggedIn: false, error: msg }));
3595
+ else console.error(msg);
3596
+ process.exit(1);
3597
+ };
3254
3598
  let jwt;
3255
3599
  try {
3256
3600
  jwt = await ensureToken();
3257
3601
  } catch (err) {
3258
- console.error(err instanceof Error ? err.message : "Not signed in. Run `deepspace login`.");
3259
- process.exit(1);
3602
+ failLoggedOut(err instanceof Error ? err.message : "Not logged in. Run `deepspace login` first.");
3260
3603
  }
3261
3604
  let payload;
3262
3605
  try {
3263
3606
  payload = decodeJwtPayload(jwt);
3264
3607
  } catch {
3265
- console.error("Malformed session token. Run `deepspace login` again.");
3266
- process.exit(1);
3608
+ failLoggedOut("Malformed session token. Run `deepspace login` again.");
3267
3609
  }
3268
3610
  const isTestAccount = !!payload.email?.endsWith("@deepspace.test");
3269
3611
  const accountType = isTestAccount ? "test-account" : payload.email ? "developer" : "unknown";
3270
3612
  if (args.json) {
3271
3613
  console.log(JSON.stringify({
3614
+ loggedIn: true,
3272
3615
  name: payload.name ?? null,
3273
3616
  email: payload.email ?? null,
3274
3617
  userId: payload.sub ?? null,
@@ -3292,11 +3635,48 @@ var whoami_default = defineCommand10({
3292
3635
  }
3293
3636
  });
3294
3637
 
3295
- // src/cli/commands/add.ts
3638
+ // src/cli/commands/apps.ts
3296
3639
  import { defineCommand as defineCommand11 } from "citty";
3297
- import { existsSync as existsSync11 } from "fs";
3298
- import { resolve as resolve8, join as join12 } from "path";
3299
- import { execSync as execSync3 } from "child_process";
3640
+ var DEPLOY_URL5 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
3641
+ var apps_default = defineCommand11({
3642
+ meta: {
3643
+ name: "apps",
3644
+ description: "List your apps (deployed and registered)"
3645
+ },
3646
+ args: {
3647
+ json: {
3648
+ type: "boolean",
3649
+ description: "Emit JSON instead of human output",
3650
+ default: false
3651
+ }
3652
+ },
3653
+ async run({ args }) {
3654
+ const token = await ensureToken();
3655
+ const { apps } = await apiFetch(DEPLOY_URL5, token, "/api/apps");
3656
+ if (args.json) {
3657
+ process.stdout.write(JSON.stringify(apps, null, 2) + "\n");
3658
+ return;
3659
+ }
3660
+ if (!apps.length) {
3661
+ console.log("No apps yet. Create one with `npx create-deepspace <name>` and `deepspace deploy`.");
3662
+ return;
3663
+ }
3664
+ const nameWidth = Math.max(4, ...apps.map((a) => (a.name ?? "\u2014").length));
3665
+ const idWidth = Math.max(6, ...apps.map((a) => a.appId.length));
3666
+ console.log(`${"NAME".padEnd(nameWidth)} ${"APP ID".padEnd(idWidth)} URL`);
3667
+ for (const a of apps) {
3668
+ const url = a.url ?? "(not deployed)";
3669
+ console.log(`${(a.name ?? "\u2014").padEnd(nameWidth)} ${a.appId.padEnd(idWidth)} ${url}`);
3670
+ }
3671
+ }
3672
+ });
3673
+
3674
+ // src/cli/commands/add.ts
3675
+ import { defineCommand as defineCommand12 } from "citty";
3676
+ import { existsSync as existsSync11, readFileSync as readFileSync12 } from "fs";
3677
+ import { resolve as resolve8, join as join12, dirname as dirname3 } from "path";
3678
+ import { fileURLToPath } from "url";
3679
+ import { spawnSync as spawnSync5 } from "child_process";
3300
3680
  function findScript(from) {
3301
3681
  const inNodeModules = join12(from, "node_modules", "deepspace", "scripts", "add-feature.cjs");
3302
3682
  if (existsSync11(inNodeModules)) return inNodeModules;
@@ -3304,7 +3684,31 @@ function findScript(from) {
3304
3684
  if (existsSync11(inDotDeepspace)) return inDotDeepspace;
3305
3685
  return null;
3306
3686
  }
3307
- var add_default = defineCommand11({
3687
+ function runningScript() {
3688
+ const here = dirname3(fileURLToPath(import.meta.url));
3689
+ const script = join12(here, "..", "scripts", "add-feature.cjs");
3690
+ return existsSync11(script) ? script : null;
3691
+ }
3692
+ function isDeepSpaceApp(dir) {
3693
+ if (existsSync11(join12(dir, "wrangler.toml"))) return true;
3694
+ const pkgPath = join12(dir, "package.json");
3695
+ if (!existsSync11(pkgPath)) return false;
3696
+ try {
3697
+ const pkg2 = JSON.parse(readFileSync12(pkgPath, "utf-8"));
3698
+ return Boolean(pkg2.dependencies?.deepspace || pkg2.devDependencies?.deepspace);
3699
+ } catch {
3700
+ return false;
3701
+ }
3702
+ }
3703
+ function runInstaller(script, passArgs, cwd) {
3704
+ const res = spawnSync5(process.execPath, [script, ...passArgs], { stdio: "inherit", cwd });
3705
+ if (res.error) {
3706
+ console.error(`Could not run the feature installer: ${res.error.message}`);
3707
+ process.exit(1);
3708
+ }
3709
+ process.exit(res.status ?? 1);
3710
+ }
3711
+ var add_default = defineCommand12({
3308
3712
  meta: {
3309
3713
  name: "add",
3310
3714
  description: "Add a feature to your DeepSpace app"
@@ -3321,6 +3725,11 @@ var add_default = defineCommand11({
3321
3725
  description: "Show details about a feature",
3322
3726
  required: false
3323
3727
  },
3728
+ install: {
3729
+ type: "boolean",
3730
+ description: "Run your package manager after adding dependencies",
3731
+ required: false
3732
+ },
3324
3733
  feature: {
3325
3734
  type: "positional",
3326
3735
  description: "Feature to install",
@@ -3334,6 +3743,27 @@ var add_default = defineCommand11({
3334
3743
  },
3335
3744
  run({ args }) {
3336
3745
  const appDir = resolve8(args.dir ?? ".");
3746
+ if (args.list || args.info || !args.feature) {
3747
+ const script2 = runningScript();
3748
+ if (!script2) {
3749
+ console.error("Could not find the deepspace feature catalog.");
3750
+ console.error("Reinstall deepspace (`npm i -g deepspace`) or run inside a DeepSpace app.");
3751
+ process.exit(1);
3752
+ }
3753
+ const passArgs2 = args.list ? ["--list"] : args.info ? ["--info", String(args.info)] : ["--help"];
3754
+ runInstaller(script2, passArgs2, process.cwd());
3755
+ }
3756
+ if (!existsSync11(appDir)) {
3757
+ console.error(`Target directory not found: ${appDir}`);
3758
+ console.error("Create a new app first: deepspace create <name>");
3759
+ process.exit(1);
3760
+ }
3761
+ if (!isDeepSpaceApp(appDir)) {
3762
+ console.error(`Not a DeepSpace app: ${appDir}`);
3763
+ console.error('(no wrangler.toml and no "deepspace" dependency found)');
3764
+ console.error("Create one with: deepspace create <name>");
3765
+ process.exit(1);
3766
+ }
3337
3767
  ensureInstallReady(appDir);
3338
3768
  const script = findScript(appDir);
3339
3769
  if (!script) {
@@ -3344,136 +3774,14 @@ var add_default = defineCommand11({
3344
3774
  console.error("Are you in a DeepSpace app directory? Try `npm install` first.");
3345
3775
  process.exit(1);
3346
3776
  }
3347
- const passArgs = [];
3348
- if (args.list) {
3349
- passArgs.push("--list");
3350
- } else if (args.info) {
3351
- passArgs.push("--info", args.info);
3352
- } else if (args.feature) {
3353
- passArgs.push(args.feature, appDir);
3354
- } else {
3355
- passArgs.push("--help");
3356
- }
3357
- execSync3(`node ${JSON.stringify(script)} ${passArgs.map((a) => JSON.stringify(a)).join(" ")}`, {
3358
- stdio: "inherit",
3359
- cwd: appDir
3360
- });
3361
- }
3362
- });
3363
-
3364
- // src/cli/commands/skills.ts
3365
- import { defineCommand as defineCommand12 } from "citty";
3366
- import spawn2 from "cross-spawn";
3367
- import { mkdirSync as mkdirSync5 } from "fs";
3368
- import { join as join13 } from "path";
3369
- import { homedir as homedir5 } from "os";
3370
- var SKILL_REPO = "deepdotspace/deepspace-skill";
3371
- function runSkillsAdd(extraArgs) {
3372
- return new Promise((resolve10) => {
3373
- const child = spawn2(
3374
- "npx",
3375
- ["-y", "skills@latest", "add", SKILL_REPO, ...extraArgs],
3376
- { stdio: "inherit" }
3377
- );
3378
- child.on("close", (code) => resolve10(code ?? 1));
3379
- child.on("error", (err) => {
3380
- console.error(`Failed to invoke npx: ${err.message}`);
3381
- console.error("Is Node.js installed and on PATH?");
3382
- resolve10(1);
3383
- });
3384
- });
3385
- }
3386
- function precreateClaudeDir(baseDir) {
3387
- mkdirSync5(join13(baseDir, ".claude"), { recursive: true });
3388
- }
3389
- var add = defineCommand12({
3390
- meta: {
3391
- name: "add",
3392
- description: "Install the DeepSpace skill for your coding agent"
3393
- },
3394
- args: {
3395
- global: {
3396
- type: "boolean",
3397
- alias: "g",
3398
- description: "Install globally (~/.agents/skills/) instead of into the current directory",
3399
- default: false
3400
- },
3401
- yes: {
3402
- type: "boolean",
3403
- alias: "y",
3404
- description: "Skip prompts (assume yes for any confirmation)",
3405
- default: false
3406
- },
3407
- agent: {
3408
- type: "string",
3409
- description: "Target a single agent (e.g. claude-code, codex, opencode). Default: install for all detected agents.",
3410
- required: false
3411
- }
3412
- },
3413
- async run({ args }) {
3414
- const extra = [];
3415
- if (args.global) extra.push("-g");
3416
- if (args.yes) extra.push("-y");
3417
- if (args.agent) extra.push("--agent", args.agent);
3418
- if (!args.agent || args.agent === "claude-code") {
3419
- const baseDir = args.global ? homedir5() : process.cwd();
3420
- try {
3421
- precreateClaudeDir(baseDir);
3422
- } catch (err) {
3423
- console.warn(`
3424
- Note: could not pre-create .claude/ for Claude Code: ${err instanceof Error ? err.message : String(err)}`);
3425
- }
3426
- }
3427
- const code = await runSkillsAdd(extra);
3428
- if (code !== 0) process.exit(code);
3429
- }
3430
- });
3431
- var skills_default = defineCommand12({
3432
- meta: {
3433
- name: "skills",
3434
- description: "Manage the DeepSpace agent skill (Claude Code, Codex, OpenCode, Cursor, ...)"
3435
- },
3436
- subCommands: {
3437
- add
3777
+ const passArgs = [args.feature, appDir];
3778
+ if (args.install) passArgs.push("--install");
3779
+ runInstaller(script, passArgs, appDir);
3438
3780
  }
3439
3781
  });
3440
3782
 
3441
3783
  // src/cli/commands/domain.ts
3442
3784
  import { defineCommand as defineCommand13 } from "citty";
3443
-
3444
- // src/cli/lib/api.ts
3445
- var ApiError = class extends Error {
3446
- constructor(message, status3, code) {
3447
- super(message);
3448
- this.status = status3;
3449
- this.code = code;
3450
- }
3451
- };
3452
- async function apiFetch(baseUrl, token, path, init) {
3453
- const res = await fetch(`${baseUrl}${path}`, {
3454
- ...init,
3455
- headers: {
3456
- Authorization: `Bearer ${token}`,
3457
- "Content-Type": "application/json",
3458
- ...init?.headers ?? {}
3459
- }
3460
- });
3461
- const text2 = await res.text();
3462
- if (!res.ok) {
3463
- let msg = text2;
3464
- let code;
3465
- try {
3466
- const body = JSON.parse(text2);
3467
- msg = body.error ?? text2;
3468
- code = body.code;
3469
- } catch {
3470
- }
3471
- throw new ApiError(`API ${path} (${res.status}): ${msg}`, res.status, code);
3472
- }
3473
- return text2 ? JSON.parse(text2) : {};
3474
- }
3475
-
3476
- // src/cli/commands/domain.ts
3477
3785
  var API_URL3 = process.env.DEEPSPACE_API_URL ?? PLATFORM_URLS.api;
3478
3786
  var api = (token, path, init) => apiFetch(API_URL3, token, path, init);
3479
3787
  function scrubInternal(value) {
@@ -3810,18 +4118,19 @@ function isTestAccountEmail(email) {
3810
4118
 
3811
4119
  // src/cli/commands/collaborators.ts
3812
4120
  var API_URL4 = process.env.DEEPSPACE_API_URL ?? PLATFORM_URLS.api;
4121
+ var DEPLOY_URL6 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
3813
4122
  function api2(token, path, init) {
3814
4123
  return apiFetch(API_URL4, token, path, init);
3815
4124
  }
3816
4125
  var list3 = defineCommand14({
3817
4126
  meta: { name: "list", description: "List collaborators on your app" },
3818
4127
  args: {
3819
- app: { type: "string", alias: "a", description: "App id (defaults to ./wrangler.toml)" },
4128
+ app: { type: "string", alias: "a", description: "App id or name (defaults to ./wrangler.toml)" },
3820
4129
  json: { type: "boolean", description: "Emit JSON" }
3821
4130
  },
3822
4131
  async run({ args }) {
3823
4132
  const token = await ensureToken();
3824
- const app = requireAppIdArg(args.app);
4133
+ const app = await resolveAppTarget(DEPLOY_URL6, token, args.app);
3825
4134
  const { collaborators } = await api2(
3826
4135
  token,
3827
4136
  `/api/app-collaborators/${encodeURIComponent(app)}`
@@ -3834,12 +4143,13 @@ var list3 = defineCommand14({
3834
4143
  console.log(`No collaborators on ${app}. Add one with \`deepspace collaborators add <email>\`.`);
3835
4144
  return;
3836
4145
  }
4146
+ console.log(`COLLABORATORS ON ${app}`);
3837
4147
  for (const c of collaborators) {
3838
- console.log(`${c.emailDisplay.padEnd(32)} ${c.userId}`);
4148
+ console.log(` ${c.emailDisplay}`);
3839
4149
  }
3840
4150
  }
3841
4151
  });
3842
- var add2 = defineCommand14({
4152
+ var add = defineCommand14({
3843
4153
  meta: { name: "add", description: "Authorize a DeepSpace user to deploy your app" },
3844
4154
  args: {
3845
4155
  email: {
@@ -3847,12 +4157,12 @@ var add2 = defineCommand14({
3847
4157
  description: "Collaborator email (must already be a DeepSpace user)",
3848
4158
  required: true
3849
4159
  },
3850
- app: { type: "string", alias: "a", description: "App id (defaults to ./wrangler.toml)" },
4160
+ app: { type: "string", alias: "a", description: "App id or name (defaults to ./wrangler.toml)" },
3851
4161
  json: { type: "boolean", description: "Emit JSON" }
3852
4162
  },
3853
4163
  async run({ args }) {
3854
4164
  const token = await ensureToken();
3855
- const app = requireAppIdArg(args.app);
4165
+ const app = await resolveAppTarget(DEPLOY_URL6, token, args.app);
3856
4166
  if (isTestAccountEmail(args.email)) {
3857
4167
  console.error(
3858
4168
  "Test accounts (@deepspace.test) cannot be added as collaborators. Use a real DeepSpace account."
@@ -3875,12 +4185,12 @@ var remove = defineCommand14({
3875
4185
  meta: { name: "remove", description: "Remove a collaborator from your app" },
3876
4186
  args: {
3877
4187
  email: { type: "positional", description: "Collaborator email", required: true },
3878
- app: { type: "string", alias: "a", description: "App id (defaults to ./wrangler.toml)" },
4188
+ app: { type: "string", alias: "a", description: "App id or name (defaults to ./wrangler.toml)" },
3879
4189
  json: { type: "boolean", description: "Emit JSON" }
3880
4190
  },
3881
4191
  async run({ args }) {
3882
4192
  const token = await ensureToken();
3883
- const app = requireAppIdArg(args.app);
4193
+ const app = await resolveAppTarget(DEPLOY_URL6, token, args.app);
3884
4194
  const { collaborators } = await api2(
3885
4195
  token,
3886
4196
  `/api/app-collaborators/${encodeURIComponent(app)}`
@@ -3905,28 +4215,57 @@ var remove = defineCommand14({
3905
4215
  });
3906
4216
  var collaborators_default = defineCommand14({
3907
4217
  meta: { name: "collaborators", description: "Manage who can deploy your app" },
3908
- subCommands: { list: list3, add: add2, remove }
4218
+ subCommands: { list: list3, add, remove }
3909
4219
  });
3910
4220
 
3911
4221
  // src/cli/commands/transfer.ts
3912
4222
  import { defineCommand as defineCommand15 } from "citty";
4223
+ import * as p6 from "@clack/prompts";
3913
4224
  var API_URL5 = process.env.DEEPSPACE_API_URL ?? PLATFORM_URLS.api;
3914
- var DEPLOY_URL5 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
4225
+ var DEPLOY_URL7 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
3915
4226
  var offer = defineCommand15({
3916
4227
  meta: { name: "offer", description: "Offer this app to another DeepSpace user" },
3917
4228
  args: {
3918
4229
  email: { type: "positional", description: "Recipient email", required: true },
3919
- app: { type: "string", alias: "a", description: "App id (defaults to ./wrangler.toml)" }
4230
+ app: { type: "string", alias: "a", description: "App id or name (defaults to ./wrangler.toml)" },
4231
+ replace: {
4232
+ type: "boolean",
4233
+ description: "Replace a pending offer to someone else without asking"
4234
+ }
3920
4235
  },
3921
4236
  async run({ args }) {
3922
4237
  const token = await ensureToken();
3923
- const app = requireAppIdArg(args.app);
4238
+ const app = await resolveAppTarget(DEPLOY_URL7, token, args.app);
4239
+ const pending = await apiFetch(
4240
+ API_URL5,
4241
+ token,
4242
+ `/api/app-transfers/${encodeURIComponent(app)}`
4243
+ );
4244
+ const pendingTo = pending.transfer?.toEmailDisplay;
4245
+ if (pendingTo && pendingTo.toLowerCase() !== args.email.trim().toLowerCase() && !args.replace) {
4246
+ if (!process.stdin.isTTY) {
4247
+ console.error(
4248
+ `A pending offer to ${pendingTo} already exists for ${app}. Re-run with --replace to replace it, or \`deepspace transfer cancel\` first.`
4249
+ );
4250
+ process.exit(1);
4251
+ }
4252
+ const yes = await p6.confirm({
4253
+ message: `A pending offer to ${pendingTo} exists \u2014 replace it with ${args.email}?`
4254
+ });
4255
+ if (p6.isCancel(yes) || !yes) {
4256
+ console.log(`Kept the pending offer to ${pendingTo}.`);
4257
+ process.exit(1);
4258
+ }
4259
+ }
3924
4260
  const { expiresAt } = await apiFetch(
3925
4261
  API_URL5,
3926
4262
  token,
3927
4263
  `/api/app-transfers/${encodeURIComponent(app)}`,
3928
4264
  { method: "POST", body: JSON.stringify({ email: args.email }) }
3929
4265
  );
4266
+ if (pendingTo && pendingTo.toLowerCase() !== args.email.trim().toLowerCase()) {
4267
+ console.log(`\u25B2 Replaced the pending offer to ${pendingTo}.`);
4268
+ }
3930
4269
  console.log(
3931
4270
  `\u2713 Offered ${app} to ${args.email} (expires ${expiresAt}).
3932
4271
  They accept with: deepspace transfer accept --app ${app}`
@@ -3936,11 +4275,11 @@ var offer = defineCommand15({
3936
4275
  var status2 = defineCommand15({
3937
4276
  meta: { name: "status", description: "Show the pending transfer offer, if any" },
3938
4277
  args: {
3939
- app: { type: "string", alias: "a", description: "App id (defaults to ./wrangler.toml)" }
4278
+ app: { type: "string", alias: "a", description: "App id or name (defaults to ./wrangler.toml)" }
3940
4279
  },
3941
4280
  async run({ args }) {
3942
4281
  const token = await ensureToken();
3943
- const app = requireAppIdArg(args.app);
4282
+ const app = await resolveAppTarget(DEPLOY_URL7, token, args.app);
3944
4283
  const { transfer } = await apiFetch(API_URL5, token, `/api/app-transfers/${encodeURIComponent(app)}`);
3945
4284
  if (!transfer) {
3946
4285
  console.log(`No pending transfer for ${app}.`);
@@ -3957,7 +4296,7 @@ var accept = defineCommand15({
3957
4296
  async run({ args }) {
3958
4297
  const token = await ensureToken();
3959
4298
  const app = requireAppIdArg(args.app);
3960
- await apiFetch(DEPLOY_URL5, token, `/api/apps/${encodeURIComponent(app)}/transfer/accept`, {
4299
+ await apiFetch(DEPLOY_URL7, token, `/api/apps/${encodeURIComponent(app)}/transfer/accept`, {
3961
4300
  method: "POST"
3962
4301
  });
3963
4302
  console.log(
@@ -3968,11 +4307,11 @@ var accept = defineCommand15({
3968
4307
  var cancel4 = defineCommand15({
3969
4308
  meta: { name: "cancel", description: "Cancel/decline the pending offer (either party)" },
3970
4309
  args: {
3971
- app: { type: "string", alias: "a", description: "App id (defaults to ./wrangler.toml)" }
4310
+ app: { type: "string", alias: "a", description: "App id or name (defaults to ./wrangler.toml)" }
3972
4311
  },
3973
4312
  async run({ args }) {
3974
4313
  const token = await ensureToken();
3975
- const app = requireAppIdArg(args.app);
4314
+ const app = await resolveAppTarget(DEPLOY_URL7, token, args.app);
3976
4315
  await apiFetch(API_URL5, token, `/api/app-transfers/${encodeURIComponent(app)}`, { method: "DELETE" });
3977
4316
  console.log(`\u2713 Transfer offer for ${app} cancelled.`);
3978
4317
  }
@@ -3986,7 +4325,8 @@ var transfer_default = defineCommand15({
3986
4325
  import { defineCommand as defineCommand16 } from "citty";
3987
4326
 
3988
4327
  // src/cli/commands/_invoke-impl.ts
3989
- import { readFileSync as readFileSync10 } from "fs";
4328
+ import { readFileSync as readFileSync13 } from "fs";
4329
+ import * as p7 from "@clack/prompts";
3990
4330
  var API_URL6 = process.env.DEEPSPACE_API_URL ?? PLATFORM_URLS.api;
3991
4331
  var DEFAULT_TIMEOUT_MS = 12e4;
3992
4332
  function parseTarget(target) {
@@ -4011,7 +4351,7 @@ function resolveBody(opts) {
4011
4351
  if (opts.body != null) {
4012
4352
  raw = opts.body;
4013
4353
  } else if (opts.bodyFile != null) {
4014
- raw = opts.bodyFile === "-" ? readFileSync10(0, "utf-8") : readFileSync10(opts.bodyFile, "utf-8");
4354
+ raw = opts.bodyFile === "-" ? readFileSync13(0, "utf-8") : readFileSync13(opts.bodyFile, "utf-8");
4015
4355
  } else {
4016
4356
  return "{}";
4017
4357
  }
@@ -4045,6 +4385,17 @@ function formatCurrency(value, currency) {
4045
4385
  if (currency === "USD") return `$${value}`;
4046
4386
  return `${value} ${currency}`;
4047
4387
  }
4388
+ function billingUnit(model) {
4389
+ if (model === "per_token") return "per token";
4390
+ if (model === "per_call") return "per call";
4391
+ return model.startsWith("per_") ? model.replace(/^per_/, "per ").replace(/_/g, " ") : model;
4392
+ }
4393
+ function isInteractive(stdin, stdout) {
4394
+ return Boolean(stdin.isTTY && stdout.isTTY);
4395
+ }
4396
+ function shouldConfirmCost(opts) {
4397
+ return !opts.json && !opts.yes && opts.isTTY && opts.baseCost > 0;
4398
+ }
4048
4399
  async function runList(args) {
4049
4400
  const catalog = await fetchCatalog({ summary: true });
4050
4401
  if (args.json) {
@@ -4090,7 +4441,7 @@ async function runInfo(args) {
4090
4441
  }
4091
4442
  console.log(`${integration}/${endpoint}`);
4092
4443
  console.log(
4093
- ` billing: ${info2.billing.model}, ${formatCurrency(info2.billing.baseCost, info2.billing.currency)} per call`
4444
+ ` billing: ${formatCurrency(info2.billing.baseCost, info2.billing.currency)} ${billingUnit(info2.billing.model)}`
4094
4445
  );
4095
4446
  console.log();
4096
4447
  console.log("Input schema:");
@@ -4105,6 +4456,31 @@ async function runInvoke(args) {
4105
4456
  const { integration, endpoint } = parseTarget(args.target);
4106
4457
  const body = resolveBody({ body: args.body, bodyFile: args.bodyFile });
4107
4458
  const timeoutMs = args.timeout ?? DEFAULT_TIMEOUT_MS;
4459
+ const interactive = isInteractive(process.stdin, process.stdout);
4460
+ if (!args.json && !args.yes && interactive) {
4461
+ let info2 = null;
4462
+ try {
4463
+ info2 = findEndpoint(await fetchCatalog({ summary: true }), integration, endpoint);
4464
+ } catch {
4465
+ console.error("Note: could not verify the call cost; proceeding. Pass --yes to skip this check.");
4466
+ info2 = null;
4467
+ }
4468
+ if (info2 && shouldConfirmCost({
4469
+ json: !!args.json,
4470
+ yes: !!args.yes,
4471
+ isTTY: interactive,
4472
+ baseCost: info2.billing.baseCost
4473
+ })) {
4474
+ const cost = formatCurrency(info2.billing.baseCost, info2.billing.currency);
4475
+ const ok2 = await p7.confirm({
4476
+ message: `${integration}/${endpoint} costs ${cost} ${billingUnit(info2.billing.model)}, billed to your account. Continue?`
4477
+ });
4478
+ if (p7.isCancel(ok2) || !ok2) {
4479
+ p7.cancel("Cancelled \u2014 no call made.");
4480
+ process.exit(0);
4481
+ }
4482
+ }
4483
+ }
4108
4484
  let jwt;
4109
4485
  try {
4110
4486
  jwt = await ensureToken();
@@ -4247,6 +4623,12 @@ var invoke = defineCommand16({
4247
4623
  type: "string",
4248
4624
  description: "Request timeout in milliseconds (default: 120000)",
4249
4625
  required: false
4626
+ },
4627
+ yes: {
4628
+ type: "boolean",
4629
+ alias: "y",
4630
+ description: "Skip the paid-call cost confirmation",
4631
+ default: false
4250
4632
  }
4251
4633
  },
4252
4634
  async run({ args }) {
@@ -4260,7 +4642,8 @@ var invoke = defineCommand16({
4260
4642
  body: args.body,
4261
4643
  bodyFile: args["body-file"],
4262
4644
  json: args.json,
4263
- timeout
4645
+ timeout,
4646
+ yes: args.yes
4264
4647
  });
4265
4648
  } catch (err) {
4266
4649
  console.error(err instanceof Error ? err.message : String(err));
@@ -4324,6 +4707,12 @@ var invoke_default = defineCommand17({
4324
4707
  type: "string",
4325
4708
  description: "Request timeout in milliseconds (default: 120000)",
4326
4709
  required: false
4710
+ },
4711
+ yes: {
4712
+ type: "boolean",
4713
+ alias: "y",
4714
+ description: "Skip the paid-call cost confirmation",
4715
+ default: false
4327
4716
  }
4328
4717
  },
4329
4718
  async run({ args }) {
@@ -4345,7 +4734,8 @@ var invoke_default = defineCommand17({
4345
4734
  body: args.body,
4346
4735
  bodyFile: args["body-file"],
4347
4736
  json: args.json,
4348
- timeout
4737
+ timeout,
4738
+ yes: args.yes
4349
4739
  });
4350
4740
  } catch (err) {
4351
4741
  console.error(err instanceof Error ? err.message : String(err));
@@ -4356,10 +4746,10 @@ var invoke_default = defineCommand17({
4356
4746
 
4357
4747
  // src/cli/commands/library.ts
4358
4748
  import { defineCommand as defineCommand18 } from "citty";
4359
- import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
4360
- import { join as join14, resolve as resolve9 } from "path";
4749
+ import { existsSync as existsSync12, readFileSync as readFileSync14 } from "fs";
4750
+ import { join as join13, resolve as resolve9 } from "path";
4361
4751
  import { parse as parseToml4 } from "smol-toml";
4362
- import * as p6 from "@clack/prompts";
4752
+ import * as p8 from "@clack/prompts";
4363
4753
  var DEFAULT_LIBRARY_APP = "deepdotspace-site";
4364
4754
  var publish = defineCommand18({
4365
4755
  meta: {
@@ -4421,25 +4811,25 @@ var publish = defineCommand18({
4421
4811
  async run({ args }) {
4422
4812
  const json = !!args.json;
4423
4813
  const intro6 = (s) => {
4424
- if (!json) p6.intro(s);
4814
+ if (!json) p8.intro(s);
4425
4815
  };
4426
4816
  const info2 = (s) => {
4427
- if (!json) p6.log.info(s);
4817
+ if (!json) p8.log.info(s);
4428
4818
  };
4429
4819
  const die = (msg) => {
4430
4820
  if (json) console.error(JSON.stringify({ ok: false, error: msg }));
4431
- else p6.cancel(msg);
4821
+ else p8.cancel(msg);
4432
4822
  process.exit(1);
4433
4823
  };
4434
4824
  intro6("Publishing to the DeepSpace library");
4435
4825
  let appName = typeof args.app === "string" && args.app.trim() ? args.app.trim() : void 0;
4436
4826
  if (!appName) {
4437
4827
  const appDir = resolve9(typeof args.dir === "string" && args.dir ? args.dir : ".");
4438
- const wranglerPath = join14(appDir, "wrangler.toml");
4828
+ const wranglerPath = join13(appDir, "wrangler.toml");
4439
4829
  if (!existsSync12(wranglerPath)) {
4440
4830
  return die("No --app provided and no wrangler.toml found. Either pass --app <name> or run from your app's directory.");
4441
4831
  }
4442
- const cfg = parseToml4(readFileSync11(wranglerPath, "utf-8"));
4832
+ const cfg = parseToml4(readFileSync14(wranglerPath, "utf-8"));
4443
4833
  const nameRes = resolveAppName(cfg.name);
4444
4834
  if (!nameRes.ok) return die(`wrangler.toml: ${nameRes.reason}`);
4445
4835
  appName = nameRes.name;
@@ -4495,9 +4885,9 @@ var publish = defineCommand18({
4495
4885
  if (!parsed.success) {
4496
4886
  return die(parsed.error ?? `Publish failed (HTTP ${res.status}).`);
4497
4887
  }
4498
- p6.log.success(`Published ${appName} as ${parsed.data?.templateHandle} (v${parsed.data?.version})`);
4499
- if (parsed.data?.url) p6.log.message(parsed.data.url);
4500
- p6.outro("Done");
4888
+ p8.log.success(`Published ${appName} as ${parsed.data?.templateHandle} (v${parsed.data?.version})`);
4889
+ if (parsed.data?.url) p8.log.message(parsed.data.url);
4890
+ p8.outro("Done");
4501
4891
  }
4502
4892
  });
4503
4893
  var unpublish = defineCommand18({
@@ -4526,10 +4916,10 @@ var unpublish = defineCommand18({
4526
4916
  const json = !!args.json;
4527
4917
  const die = (msg) => {
4528
4918
  if (json) console.error(JSON.stringify({ ok: false, error: msg }));
4529
- else p6.cancel(msg);
4919
+ else p8.cancel(msg);
4530
4920
  process.exit(1);
4531
4921
  };
4532
- if (!json) p6.intro("Unpublishing library entry");
4922
+ if (!json) p8.intro("Unpublishing library entry");
4533
4923
  let token;
4534
4924
  try {
4535
4925
  token = await ensureToken();
@@ -4555,8 +4945,8 @@ var unpublish = defineCommand18({
4555
4945
  process.exit(parsed.success ? 0 : 1);
4556
4946
  }
4557
4947
  if (!parsed.success) return die(parsed.error ?? `HTTP ${res.status}`);
4558
- p6.log.success(`Unpublished ${parsed.data?.templateHandle}`);
4559
- p6.outro("Done");
4948
+ p8.log.success(`Unpublished ${parsed.data?.templateHandle}`);
4949
+ p8.outro("Done");
4560
4950
  }
4561
4951
  });
4562
4952
  var library_default = defineCommand18({
@@ -4572,10 +4962,10 @@ var library_default = defineCommand18({
4572
4962
 
4573
4963
  // src/cli/commands/feedback.ts
4574
4964
  import { defineCommand as defineCommand19 } from "citty";
4575
- import * as p7 from "@clack/prompts";
4576
- import { existsSync as existsSync13, readFileSync as readFileSync12 } from "fs";
4577
- import { dirname as dirname3, join as join15 } from "path";
4578
- import { fileURLToPath } from "url";
4965
+ import * as p9 from "@clack/prompts";
4966
+ import { existsSync as existsSync13, readFileSync as readFileSync15 } from "fs";
4967
+ import { dirname as dirname4, join as join14 } from "path";
4968
+ import { fileURLToPath as fileURLToPath2 } from "url";
4579
4969
  import { release } from "os";
4580
4970
  var API_URL7 = process.env.DEEPSPACE_API_URL ?? PLATFORM_URLS.api;
4581
4971
  var TYPES = ["bug", "feature", "other"];
@@ -4595,14 +4985,14 @@ function normalizeType(raw) {
4595
4985
  }
4596
4986
  function readCliVersion() {
4597
4987
  try {
4598
- const here = dirname3(fileURLToPath(import.meta.url));
4988
+ const here = dirname4(fileURLToPath2(import.meta.url));
4599
4989
  const candidates = [
4600
- join15(here, "..", "package.json"),
4601
- join15(here, "..", "..", "..", "package.json")
4990
+ join14(here, "..", "package.json"),
4991
+ join14(here, "..", "..", "..", "package.json")
4602
4992
  ];
4603
4993
  for (const path of candidates) {
4604
4994
  if (!existsSync13(path)) continue;
4605
- const pkg2 = JSON.parse(readFileSync12(path, "utf-8"));
4995
+ const pkg2 = JSON.parse(readFileSync15(path, "utf-8"));
4606
4996
  if (pkg2.name === "deepspace" && pkg2.version) return pkg2.version;
4607
4997
  }
4608
4998
  } catch {
@@ -4707,21 +5097,21 @@ var feedback_default = defineCommand19({
4707
5097
  );
4708
5098
  }
4709
5099
  if (interactive && (!args.type || !title || !body)) {
4710
- p7.intro("DeepSpace Feedback");
5100
+ p9.intro("DeepSpace Feedback");
4711
5101
  if (!args.type) {
4712
- const picked = await p7.select({
5102
+ const picked = await p9.select({
4713
5103
  message: "What kind of feedback is this?",
4714
5104
  options: TYPES.map((t) => ({ value: t, label: TYPE_LABELS[t] })),
4715
5105
  initialValue: type
4716
5106
  });
4717
- if (p7.isCancel(picked)) {
4718
- p7.cancel("Cancelled.");
5107
+ if (p9.isCancel(picked)) {
5108
+ p9.cancel("Cancelled.");
4719
5109
  process.exit(0);
4720
5110
  }
4721
5111
  type = picked;
4722
5112
  }
4723
5113
  if (!title) {
4724
- const t = await p7.text({
5114
+ const t = await p9.text({
4725
5115
  message: "Title",
4726
5116
  placeholder: "Short summary of the issue",
4727
5117
  validate: (v) => {
@@ -4731,14 +5121,14 @@ var feedback_default = defineCommand19({
4731
5121
  return void 0;
4732
5122
  }
4733
5123
  });
4734
- if (p7.isCancel(t)) {
4735
- p7.cancel("Cancelled.");
5124
+ if (p9.isCancel(t)) {
5125
+ p9.cancel("Cancelled.");
4736
5126
  process.exit(0);
4737
5127
  }
4738
5128
  title = String(t);
4739
5129
  }
4740
5130
  if (!body) {
4741
- const m = await p7.text({
5131
+ const m = await p9.text({
4742
5132
  message: "Details",
4743
5133
  placeholder: "What happened? Steps to reproduce, what you expected\u2026",
4744
5134
  validate: (v) => {
@@ -4748,8 +5138,8 @@ var feedback_default = defineCommand19({
4748
5138
  return void 0;
4749
5139
  }
4750
5140
  });
4751
- if (p7.isCancel(m)) {
4752
- p7.cancel("Cancelled.");
5141
+ if (p9.isCancel(m)) {
5142
+ p9.cancel("Cancelled.");
4753
5143
  process.exit(0);
4754
5144
  }
4755
5145
  body = String(m);
@@ -4767,7 +5157,7 @@ var feedback_default = defineCommand19({
4767
5157
  return fail2(err.message);
4768
5158
  }
4769
5159
  if (interactive && !args.yes) {
4770
- p7.note(
5160
+ p9.note(
4771
5161
  `${TYPE_LABELS[payload.type]}
4772
5162
  ${payload.title}
4773
5163
 
@@ -4776,9 +5166,9 @@ ${payload.body}
4776
5166
  Context: deepspace ${payload.cliVersion} \xB7 ${payload.nodeVersion} \xB7 ${payload.platform}` + (payload.appName ? ` \xB7 app ${payload.appName}` : ""),
4777
5167
  "Review"
4778
5168
  );
4779
- const ok = await p7.confirm({ message: "Send this to the DeepSpace team?" });
4780
- if (p7.isCancel(ok) || !ok) {
4781
- p7.cancel("Not sent.");
5169
+ const ok = await p9.confirm({ message: "Send this to the DeepSpace team?" });
5170
+ if (p9.isCancel(ok) || !ok) {
5171
+ p9.cancel("Not sent.");
4782
5172
  process.exit(0);
4783
5173
  }
4784
5174
  }
@@ -4802,7 +5192,7 @@ Context: deepspace ${payload.cliVersion} \xB7 ${payload.nodeVersion} \xB7 ${payl
4802
5192
  return;
4803
5193
  }
4804
5194
  if (interactive) {
4805
- p7.outro(`Thanks! Submitted as ${result.report.id}.`);
5195
+ p9.outro(`Thanks! Submitted as ${result.report.id}.`);
4806
5196
  } else {
4807
5197
  console.log(`Submitted feedback ${result.report.id} (${result.report.type}).`);
4808
5198
  }
@@ -4812,8 +5202,18 @@ Context: deepspace ${payload.cliVersion} \xB7 ${payload.nodeVersion} \xB7 ${payl
4812
5202
 
4813
5203
  // src/cli/commands/secrets.ts
4814
5204
  import { defineCommand as defineCommand20 } from "citty";
4815
- import { readFileSync as readFileSync13 } from "fs";
4816
- var DEPLOY_URL6 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
5205
+ import { readFileSync as readFileSync16 } from "fs";
5206
+
5207
+ // src/cli/lib/citty-args.ts
5208
+ function dedupePositionals(bound, rest) {
5209
+ const restArr = Array.isArray(rest) ? rest : [];
5210
+ return [.../* @__PURE__ */ new Set([bound, ...restArr])].filter(
5211
+ (v) => typeof v === "string" && v.length > 0
5212
+ );
5213
+ }
5214
+
5215
+ // src/cli/commands/secrets.ts
5216
+ var DEPLOY_URL8 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
4817
5217
  var COMMON_ARGS = {
4818
5218
  app: {
4819
5219
  type: "string",
@@ -4844,6 +5244,11 @@ async function resolveTarget(args) {
4844
5244
  const appDir = findAppDir();
4845
5245
  appId = appDir && readAppId(appDir, wranglerEnv) || void 0;
4846
5246
  if (!appId) {
5247
+ if (wranglerEnv && appDir && readAppId(appDir, void 0)) {
5248
+ throw new Error(
5249
+ `No app id for env "${wranglerEnv}" \u2014 wrangler.toml has no [env.${wranglerEnv}] block with its own DEEPSPACE_APP_ID. To target the "${wranglerEnv}" config on this app, use \`-c ${wranglerEnv}\` instead of \`--env ${wranglerEnv}\`.`
5250
+ );
5251
+ }
4847
5252
  throw new Error(
4848
5253
  "No app id. Run from an app directory whose wrangler.toml carries DEEPSPACE_APP_ID (`deepspace init` mints one), or pass --app <appId>."
4849
5254
  );
@@ -4857,6 +5262,15 @@ function fail(err) {
4857
5262
  console.error(err instanceof Error ? err.message : String(err));
4858
5263
  process.exit(1);
4859
5264
  }
5265
+ function warnIfDebugRoutesEnabled(secrets) {
5266
+ if (secrets.ALLOW_DEBUG_ROUTES !== "true") return false;
5267
+ console.warn(
5268
+ "\n\u26A0 ALLOW_DEBUG_ROUTES=true exposes an UNAUTHENTICATED debug API on the deployed app \u2014\n anyone who can reach /api/debug/* can read and mutate any record. Redeploy to apply;\n `deepspace secrets delete ALLOW_DEBUG_ROUTES` (then redeploy) turns it back off."
5269
+ );
5270
+ return true;
5271
+ }
5272
+ var APPLY_HINT = "Run `deepspace deploy` to apply \u2014 secrets take effect at deploy time.";
5273
+ var LOCAL_DEV_HINT = "Running `deepspace dev`? Restart it to load the change locally.";
4860
5274
  var list5 = defineCommand20({
4861
5275
  meta: { name: "list", description: "List masked secrets in a config" },
4862
5276
  args: {
@@ -4871,7 +5285,7 @@ var list5 = defineCommand20({
4871
5285
  async run({ args }) {
4872
5286
  try {
4873
5287
  const t = await resolveTarget(args);
4874
- const { secrets } = await listSecrets(DEPLOY_URL6, t.token, t.appId, t.configName);
5288
+ const { secrets } = await listSecrets(DEPLOY_URL8, t.token, t.appId, t.configName);
4875
5289
  if (args.json) {
4876
5290
  console.log(JSON.stringify({ appId: t.appId, config: t.configName, secrets }, null, 2));
4877
5291
  return;
@@ -4881,7 +5295,7 @@ var list5 = defineCommand20({
4881
5295
  return;
4882
5296
  }
4883
5297
  for (const s of secrets) {
4884
- console.log(args["only-names"] ? s.key : `${s.key} (v${s.version}, ${s.valueBytes}B, ${s.updatedAt})`);
5298
+ console.log(args["only-names"] ? s.key : `${s.key} (v${s.version}, ${s.updatedAt})`);
4885
5299
  }
4886
5300
  } catch (err) {
4887
5301
  fail(err);
@@ -4897,25 +5311,34 @@ var set = defineCommand20({
4897
5311
  async run({ args }) {
4898
5312
  try {
4899
5313
  const t = await resolveTarget(args);
4900
- const pairs = [args.secret, ...args._ ?? []].filter(
4901
- (v) => typeof v === "string" && v.length > 0
4902
- );
5314
+ const pairs = dedupePositionals(args.secret, args._);
4903
5315
  const secrets = {};
5316
+ const dupes = [];
4904
5317
  for (const pair of pairs) {
4905
5318
  const eq = pair.indexOf("=");
4906
5319
  if (eq <= 0) throw new Error(`Expected KEY=value, got "${pair}"`);
4907
5320
  const key = validateSecretName(pair.slice(0, eq));
4908
5321
  const value = pair.slice(eq + 1);
4909
5322
  validateSecretValue(key, value);
5323
+ if (key in secrets && !dupes.includes(key)) dupes.push(key);
4910
5324
  secrets[key] = value;
4911
5325
  }
5326
+ if (dupes.length) {
5327
+ console.warn(
5328
+ `Warning: ${dupes.join(", ")} given more than once \u2014 kept the last value for each.`
5329
+ );
5330
+ }
4912
5331
  if (Object.keys(secrets).length === 1) {
4913
5332
  const [[k, v]] = Object.entries(secrets);
4914
- await setSecret(DEPLOY_URL6, t.token, t.appId, t.configName, k, v);
5333
+ await setSecret(DEPLOY_URL8, t.token, t.appId, t.configName, k, v);
4915
5334
  } else {
4916
- await uploadSecrets(DEPLOY_URL6, t.token, t.appId, t.configName, secrets, false);
5335
+ await uploadSecrets(DEPLOY_URL8, t.token, t.appId, t.configName, secrets, false);
4917
5336
  }
4918
5337
  console.log(`Set ${Object.keys(secrets).length} secret${Object.keys(secrets).length === 1 ? "" : "s"} in ${t.configName}.`);
5338
+ if (!warnIfDebugRoutesEnabled(secrets)) {
5339
+ console.log(APPLY_HINT);
5340
+ console.log(LOCAL_DEV_HINT);
5341
+ }
4919
5342
  } catch (err) {
4920
5343
  fail(err);
4921
5344
  }
@@ -4933,14 +5356,14 @@ var get = defineCommand20({
4933
5356
  const t = await resolveTarget(args);
4934
5357
  const key = validateSecretName(args.key);
4935
5358
  if (args.plain) {
4936
- const { value } = await getSecretPlain(DEPLOY_URL6, t.token, t.appId, t.configName, key);
4937
- process.stdout.write(value + "\n");
5359
+ const { value } = await getSecretPlain(DEPLOY_URL8, t.token, t.appId, t.configName, key);
5360
+ process.stdout.write(process.stdout.isTTY ? value + "\n" : value);
4938
5361
  return;
4939
5362
  }
4940
- const { secrets } = await listSecrets(DEPLOY_URL6, t.token, t.appId, t.configName);
5363
+ const { secrets } = await listSecrets(DEPLOY_URL8, t.token, t.appId, t.configName);
4941
5364
  const item = secrets.find((s) => s.key === key);
4942
5365
  if (!item) fail(new Error(`Secret "${key}" not found in ${t.configName}`));
4943
- console.log(`${item.key} (v${item.version}, ${item.valueBytes}B, ${item.updatedAt})`);
5366
+ console.log(`${item.key} (v${item.version}, ${item.updatedAt})`);
4944
5367
  } catch (err) {
4945
5368
  fail(err);
4946
5369
  }
@@ -4955,13 +5378,22 @@ var del2 = defineCommand20({
4955
5378
  async run({ args }) {
4956
5379
  try {
4957
5380
  const t = await resolveTarget(args);
4958
- const keys = [args.key, ...args._ ?? []].filter(
4959
- (v) => typeof v === "string" && v.length > 0
4960
- );
5381
+ const keys = dedupePositionals(args.key, args._);
5382
+ let deleted = 0;
5383
+ let absent = 0;
4961
5384
  for (const key of keys) {
4962
- await deleteSecret(DEPLOY_URL6, t.token, t.appId, t.configName, validateSecretName(key));
5385
+ const name = validateSecretName(key);
5386
+ try {
5387
+ await deleteSecret(DEPLOY_URL8, t.token, t.appId, t.configName, name);
5388
+ deleted++;
5389
+ } catch (err) {
5390
+ if (err?.status === 404) absent++;
5391
+ else throw err;
5392
+ }
4963
5393
  }
4964
- console.log(`Deleted ${keys.length} secret${keys.length === 1 ? "" : "s"} from ${t.configName}.`);
5394
+ const note3 = absent > 0 ? ` (${absent} already absent)` : "";
5395
+ console.log(`Deleted ${deleted} secret${deleted === 1 ? "" : "s"} from ${t.configName}.${note3}`);
5396
+ if (deleted > 0) console.log(APPLY_HINT);
4965
5397
  } catch (err) {
4966
5398
  fail(err);
4967
5399
  }
@@ -4981,11 +5413,12 @@ var upload = defineCommand20({
4981
5413
  async run({ args }) {
4982
5414
  try {
4983
5415
  const t = await resolveTarget(args);
4984
- const content = args.file === "-" ? readFileSync13(0, "utf-8") : readFileSync13(args.file, "utf-8");
5416
+ const content = args.file === "-" ? readFileSync16(0, "utf-8") : readFileSync16(args.file, "utf-8");
4985
5417
  const secrets = parseSecretsUpload(content);
4986
5418
  if (Object.keys(secrets).length === 0) fail(new Error("No secrets found in the input."));
4987
- await uploadSecrets(DEPLOY_URL6, t.token, t.appId, t.configName, secrets, args.replace);
5419
+ await uploadSecrets(DEPLOY_URL8, t.token, t.appId, t.configName, secrets, args.replace);
4988
5420
  console.log(`Uploaded ${Object.keys(secrets).length} secrets to ${t.configName}.`);
5421
+ if (!warnIfDebugRoutesEnabled(secrets)) console.log(APPLY_HINT);
4989
5422
  } catch (err) {
4990
5423
  fail(err);
4991
5424
  }
@@ -5008,7 +5441,7 @@ var download = defineCommand20({
5008
5441
  if (!["dotenv", "json", "shell"].includes(format)) {
5009
5442
  fail(new Error(`Unknown format "${args.format}" \u2014 use dotenv, json, or shell.`));
5010
5443
  }
5011
- const { secrets } = await fetchSecretsValues(DEPLOY_URL6, t.token, t.appId, t.configName);
5444
+ const { secrets } = await fetchSecretsValues(DEPLOY_URL8, t.token, t.appId, t.configName);
5012
5445
  process.stdout.write(formatSecretsDownload(secrets, format));
5013
5446
  } catch (err) {
5014
5447
  fail(err);
@@ -5025,7 +5458,7 @@ var pull = defineCommand20({
5025
5458
  if (!appDir) fail(new Error("Run from a DeepSpace app directory (one containing wrangler.toml)."));
5026
5459
  const t = await resolveTarget(args);
5027
5460
  const ownerId = decodeJwtPayload(t.token).sub;
5028
- const pulled = await pullAppSecretsCache(DEPLOY_URL6, t.token, t.appId, t.configName);
5461
+ const pulled = await pullAppSecretsCache(DEPLOY_URL8, t.token, t.appId, t.configName);
5029
5462
  if (!pulled) {
5030
5463
  console.log(`No secrets in ${t.configName} yet \u2014 nothing to pull.`);
5031
5464
  return;
@@ -5049,7 +5482,7 @@ var configsList = defineCommand20({
5049
5482
  async run({ args }) {
5050
5483
  try {
5051
5484
  const t = await resolveTarget(args);
5052
- const { configs: configs2 } = await listConfigs(DEPLOY_URL6, t.token, t.appId);
5485
+ const { configs: configs2 } = await listConfigs(DEPLOY_URL8, t.token, t.appId);
5053
5486
  if (configs2.length === 0) {
5054
5487
  console.log("No configs yet \u2014 the first `secrets set` creates one.");
5055
5488
  return;
@@ -5073,7 +5506,20 @@ var configsCreate = defineCommand20({
5073
5506
  try {
5074
5507
  const t = await resolveTarget(args);
5075
5508
  const name = validateConfigName(args.name);
5076
- await createConfig(DEPLOY_URL6, t.token, t.appId, name, args["copy-from"] || void 0);
5509
+ const copyFrom = args["copy-from"] || void 0;
5510
+ const { configs: configs2 } = await listConfigs(DEPLOY_URL8, t.token, t.appId);
5511
+ if (configs2.some((c) => c.name === name)) {
5512
+ if (copyFrom) {
5513
+ fail(
5514
+ new Error(
5515
+ `Config "${name}" already exists \u2014 refusing to copy "${copyFrom}" over it. Delete it first, or pick a new name.`
5516
+ )
5517
+ );
5518
+ }
5519
+ console.log(`Config ${name} already exists.`);
5520
+ return;
5521
+ }
5522
+ await createConfig(DEPLOY_URL8, t.token, t.appId, name, copyFrom);
5077
5523
  console.log(`Created ${name}.`);
5078
5524
  } catch (err) {
5079
5525
  fail(err);
@@ -5090,7 +5536,7 @@ var configsDelete = defineCommand20({
5090
5536
  try {
5091
5537
  const t = await resolveTarget(args);
5092
5538
  const name = validateConfigName(args.name);
5093
- await deleteConfig(DEPLOY_URL6, t.token, t.appId, name);
5539
+ await deleteConfig(DEPLOY_URL8, t.token, t.appId, name);
5094
5540
  console.log(`Deleted ${name}.`);
5095
5541
  } catch (err) {
5096
5542
  fail(err);
@@ -5138,7 +5584,9 @@ var init_default = defineCommand21({
5138
5584
  async run({ args }) {
5139
5585
  const appDir = findAppDir();
5140
5586
  if (!appDir) {
5141
- console.error("Run from a DeepSpace app directory (one containing wrangler.toml).");
5587
+ console.error(
5588
+ "No wrangler.toml here \u2014 `deepspace init` stamps an app id into an existing DeepSpace app. To create a new app, run `deepspace create <name>` (or `npm create deepspace@latest <name>`), then run this from inside it."
5589
+ );
5142
5590
  process.exit(1);
5143
5591
  }
5144
5592
  const envName = args.env || void 0;
@@ -5160,28 +5608,36 @@ var init_default = defineCommand21({
5160
5608
  });
5161
5609
 
5162
5610
  // src/cli/cli.ts
5163
- var __dirname = dirname4(fileURLToPath2(import.meta.url));
5164
- var pkg = JSON.parse(readFileSync14(join16(__dirname, "..", "package.json"), "utf-8"));
5611
+ var __dirname = dirname5(fileURLToPath3(import.meta.url));
5612
+ var pkg = JSON.parse(readFileSync17(join15(__dirname, "..", "package.json"), "utf-8"));
5165
5613
  if (process.platform === "linux" && !process.env.SSL_CERT_FILE && existsSync14("/etc/ssl/certs/ca-certificates.crt")) {
5166
5614
  process.env.SSL_CERT_FILE = "/etc/ssl/certs/ca-certificates.crt";
5167
5615
  }
5168
5616
  var create2 = defineCommand22({
5169
5617
  meta: {
5170
5618
  name: "create",
5171
- description: "Create a new DeepSpace app"
5619
+ description: "Create a new DeepSpace app (runs create-deepspace; all flags are forwarded)"
5172
5620
  },
5173
5621
  args: {
5174
5622
  name: {
5175
5623
  type: "positional",
5176
5624
  description: "App name",
5177
5625
  required: false
5626
+ },
5627
+ interactive: {
5628
+ type: "boolean",
5629
+ description: "Prompt for options instead of using defaults"
5630
+ },
5631
+ local: {
5632
+ type: "string",
5633
+ description: "Use a local SDK monorepo checkout instead of the published package"
5178
5634
  }
5179
5635
  },
5180
- run({ args }) {
5181
- const name = args.name ? ` ${args.name}` : "";
5182
- console.log(`Running: npm create deepspace${name}
5183
- `);
5184
- execSync4(`npm create deepspace@latest${name}`, { stdio: "inherit" });
5636
+ run({ rawArgs }) {
5637
+ const scaffolder = `create-deepspace@${pkg.version}`;
5638
+ console.log(`Running: npx ${scaffolder} ${rawArgs.join(" ")}`.trimEnd() + "\n");
5639
+ const res = spawnSync6("npx", ["-y", scaffolder, ...rawArgs], { stdio: "inherit" });
5640
+ process.exit(res.status ?? 1);
5185
5641
  }
5186
5642
  });
5187
5643
  var main = defineCommand22({
@@ -5196,13 +5652,13 @@ var main = defineCommand22({
5196
5652
  login: login_default,
5197
5653
  logout: logout_default,
5198
5654
  whoami: whoami_default,
5655
+ apps: apps_default,
5199
5656
  dev: dev_default,
5200
5657
  kill: kill_default,
5201
5658
  test: test_default,
5202
5659
  screenshot: screenshot_default,
5203
5660
  "test-accounts": test_accounts_default,
5204
5661
  add: add_default,
5205
- skills: skills_default,
5206
5662
  integrations: integrations_default,
5207
5663
  invoke: invoke_default,
5208
5664
  deploy: deploy_default,