deepspace 0.5.7 → 0.6.1

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
@@ -3,8 +3,8 @@
3
3
  // src/cli/cli.ts
4
4
  import { defineCommand as defineCommand22, runMain } from "citty";
5
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";
6
+ import { readFileSync as readFileSync17, existsSync as existsSync15 } from "fs";
7
+ import { dirname as dirname5, join as join16 } from "path";
8
8
  import { fileURLToPath as fileURLToPath3 } from "url";
9
9
 
10
10
  // src/cli/lib/cli-errors.ts
@@ -13,7 +13,9 @@ var API_ERROR_HINTS = {
13
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
- 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."
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.",
17
+ insufficient_credits: "Out of credits. Inviting a new collaborator by email sends them a transactional email billed to your account \u2014 top up your credits and try again.",
18
+ invite_email_failed: "The invite email could not be sent. You were not charged \u2014 please try again in a moment."
17
19
  };
18
20
  function formatCliError(err) {
19
21
  const message = err instanceof Error ? err.message : String(err);
@@ -555,18 +557,27 @@ function mintAppId(now = Date.now()) {
555
557
  }
556
558
  return `app_${ts}${rs}`.slice(0, 30);
557
559
  }
558
- async function resolveOwnedAppId(deployUrl, token, appName) {
560
+ async function resolveExistingAppId(deployUrl, token, appName) {
561
+ const headers = { Authorization: `Bearer ${token}` };
559
562
  try {
560
- const res = await fetch(`${deployUrl}/api/apps`, {
561
- headers: { Authorization: `Bearer ${token}` }
562
- });
563
- if (!res.ok) return null;
564
- const body = await res.json();
565
- const match = body.apps?.find((a) => a.name === appName);
566
- return match?.appId ?? null;
563
+ const res = await fetch(`${deployUrl}/api/apps`, { headers });
564
+ if (res.ok) {
565
+ const body = await res.json();
566
+ const match = body.apps?.find((a) => a.name === appName);
567
+ if (match?.appId) return { kind: "adopted", appId: match.appId, owned: true };
568
+ }
569
+ } catch {
570
+ }
571
+ try {
572
+ const res = await fetch(
573
+ `${deployUrl}/api/apps/${encodeURIComponent(appName)}/analytics?period=1h`,
574
+ { headers }
575
+ );
576
+ if (res.ok) return { kind: "adopted", appId: appName, owned: false };
577
+ if (res.status === 403) return { kind: "taken" };
567
578
  } catch {
568
- return null;
569
579
  }
580
+ return { kind: "none" };
570
581
  }
571
582
  function readAppId(cwd = process.cwd(), wranglerEnv) {
572
583
  const wranglerPath = join(resolve(cwd), "wrangler.toml");
@@ -1331,7 +1342,7 @@ var logout_default = defineCommand2({
1331
1342
 
1332
1343
  // src/cli/commands/dev.ts
1333
1344
  import { defineCommand as defineCommand3 } from "citty";
1334
- import { resolve as resolve4, basename as basename2, join as join9 } from "path";
1345
+ import { resolve as resolve4, basename as basename2, join as join10 } from "path";
1335
1346
  import spawn from "cross-spawn";
1336
1347
 
1337
1348
  // src/cli/auth.ts
@@ -1777,6 +1788,112 @@ function removeMacosJunk(dir) {
1777
1788
  return removed;
1778
1789
  }
1779
1790
 
1791
+ // src/cli/lib/schema-lint.ts
1792
+ import { existsSync as existsSync9 } from "fs";
1793
+ import { join as join9 } from "path";
1794
+ import { pathToFileURL } from "url";
1795
+
1796
+ // src/server/schemas/registry.ts
1797
+ var USERS_COLUMNS = [
1798
+ { name: "email", storage: "text", interpretation: "plain" },
1799
+ { name: "name", storage: "text", interpretation: "plain" },
1800
+ { name: "imageUrl", storage: "text", interpretation: "plain" },
1801
+ { name: "role", storage: "text", interpretation: "plain" },
1802
+ { name: "createdAt", storage: "text", interpretation: { kind: "datetime" } },
1803
+ { name: "lastSeenAt", storage: "text", interpretation: { kind: "datetime" } }
1804
+ ];
1805
+ var BASE_USERS_SCHEMA = {
1806
+ name: "users",
1807
+ columns: [...USERS_COLUMNS],
1808
+ permissions: {
1809
+ "*": { read: "own", create: false, update: false, delete: false },
1810
+ admin: { read: true, create: false, update: true, delete: true }
1811
+ }
1812
+ };
1813
+ function lintSchema(schema) {
1814
+ const warnings = [];
1815
+ const VISIBILITY_AWARE = ["published", "shared"];
1816
+ if (schema.visibilityField) {
1817
+ const offendingRoles = Object.entries(schema.permissions).filter(([, perms]) => perms.read === true).map(([role]) => role);
1818
+ if (offendingRoles.length > 0) {
1819
+ const usesAware = Object.values(schema.permissions).some(
1820
+ (p10) => typeof p10.read === "string" && VISIBILITY_AWARE.includes(p10.read)
1821
+ );
1822
+ if (!usesAware) {
1823
+ warnings.push(
1824
+ `[${schema.name}] visibilityField is declared but no role uses read: 'published' or 'shared'. Roles with read: true (${offendingRoles.join(", ")}) will see every row regardless of visibility. Change those to read: 'published' (owner OR public) or 'shared' (owner OR collaborator OR public) to actually enforce the filter, or remove visibilityField if you don't intend to gate reads by it.`
1825
+ );
1826
+ }
1827
+ }
1828
+ }
1829
+ if (schema.ownerField) {
1830
+ const col = schema.columns.find((c) => c.name === schema.ownerField);
1831
+ if (col && !col.userBound) {
1832
+ warnings.push(
1833
+ `[${schema.name}] ownerField is '${schema.ownerField}' but that column is not marked userBound: true. A client can create a row with someone else's id in this field, bypassing 'own' permission checks. Add userBound: true (and ideally immutable: true) to the column.`
1834
+ );
1835
+ }
1836
+ }
1837
+ for (const col of schema.columns) {
1838
+ if (col.userBound && col.storage !== "text") {
1839
+ warnings.push(
1840
+ `[${schema.name}] column '${col.name}' is userBound but storage is '${col.storage}'. userBound stamps the user id (a string); use storage: 'text'.`
1841
+ );
1842
+ }
1843
+ }
1844
+ return warnings;
1845
+ }
1846
+
1847
+ // src/cli/lib/schema-lint.ts
1848
+ async function lintProjectSchemas(appDir) {
1849
+ const schemasPath = join9(appDir, "src", "schemas.ts");
1850
+ if (!existsSync9(schemasPath)) return null;
1851
+ let schemas;
1852
+ try {
1853
+ const esbuild = await import("esbuild");
1854
+ const outFile = join9(appDir, ".wrangler", "deploy", "schemas.lint.bundle.mjs");
1855
+ await esbuild.build({
1856
+ entryPoints: [schemasPath],
1857
+ bundle: true,
1858
+ format: "esm",
1859
+ platform: "node",
1860
+ target: "node18",
1861
+ outfile: outFile,
1862
+ logLevel: "silent",
1863
+ write: true
1864
+ });
1865
+ const mod = await import(`${pathToFileURL(outFile).href}?t=${Date.now()}`);
1866
+ schemas = mod.schemas;
1867
+ } catch {
1868
+ console.warn(
1869
+ "Schema lint skipped: could not bundle src/schemas.ts (the worker still lints at runtime)."
1870
+ );
1871
+ return null;
1872
+ }
1873
+ if (!Array.isArray(schemas)) return null;
1874
+ const findings = [];
1875
+ for (const schema of schemas) {
1876
+ if (!schema || typeof schema !== "object") continue;
1877
+ try {
1878
+ findings.push(...lintSchema(schema));
1879
+ } catch {
1880
+ }
1881
+ }
1882
+ return findings;
1883
+ }
1884
+ var MAX_PRINTED_FINDINGS = 5;
1885
+ function formatSchemaLintFindings(findings) {
1886
+ if (findings.length === 0) return [];
1887
+ const n = findings.length;
1888
+ const shown = findings.slice(0, MAX_PRINTED_FINDINGS);
1889
+ const hidden = n - shown.length;
1890
+ return [
1891
+ `Schema lint: ${n} warning${n === 1 ? "" : "s"} in src/schemas.ts \u2014 ${n === 1 ? "this ships" : "these ship"} as permission/privacy bugs:`,
1892
+ ...shown.map((f) => ` \u2022 ${f}`),
1893
+ ...hidden > 0 ? [` \u2026and ${hidden} more \u2014 fix the above and re-run to see the rest.`] : []
1894
+ ];
1895
+ }
1896
+
1780
1897
  // src/cli/lib/port.ts
1781
1898
  import { createServer } from "net";
1782
1899
  var DEFAULT_PORT = 5173;
@@ -1863,7 +1980,7 @@ var dev_default = defineCommand3({
1863
1980
  );
1864
1981
  } else {
1865
1982
  console.warn(
1866
- `Warning: could not update ${join9(worktree.mainRepoRoot, ".claude", "launch.json")} (malformed or unwritable) \u2014 the Claude preview tool may serve the main repo's code instead of this worktree.`
1983
+ `Warning: could not update ${join10(worktree.mainRepoRoot, ".claude", "launch.json")} (malformed or unwritable) \u2014 the Claude preview tool may serve the main repo's code instead of this worktree.`
1867
1984
  );
1868
1985
  }
1869
1986
  if (!explicitPort && process.env.DEEPSPACE_PORT) {
@@ -1876,6 +1993,10 @@ var dev_default = defineCommand3({
1876
1993
  updatePort: explicitPort
1877
1994
  });
1878
1995
  ensureInstallReady(appDir);
1996
+ const lintFindings = await lintProjectSchemas(appDir);
1997
+ if (lintFindings && lintFindings.length > 0) {
1998
+ for (const line of formatSchemaLintFindings(lintFindings)) console.warn(line);
1999
+ }
1879
2000
  let token;
1880
2001
  try {
1881
2002
  token = await ensureToken();
@@ -2463,15 +2584,15 @@ var screenshot_default = defineCommand6({
2463
2584
  // src/cli/commands/test-accounts.ts
2464
2585
  import { defineCommand as defineCommand7 } from "citty";
2465
2586
  import * as p3 from "@clack/prompts";
2466
- import { readFileSync as readFileSync10, writeFileSync as writeFileSync8, existsSync as existsSync9, mkdirSync as mkdirSync4 } from "fs";
2467
- import { join as join10 } from "path";
2587
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync8, existsSync as existsSync10, mkdirSync as mkdirSync4 } from "fs";
2588
+ import { join as join11 } from "path";
2468
2589
  import { homedir as homedir4 } from "os";
2469
2590
  var SESSION_COOKIE2 = "__Secure-better-auth.session_token";
2470
2591
  var AUTH_URL4 = process.env.DEEPSPACE_AUTH_URL ?? PLATFORM_URLS.auth;
2471
- var DIR3 = join10(homedir4(), ".deepspace");
2472
- var ACCOUNTS_PATH = join10(DIR3, "test-accounts.json");
2592
+ var DIR3 = join11(homedir4(), ".deepspace");
2593
+ var ACCOUNTS_PATH = join11(DIR3, "test-accounts.json");
2473
2594
  function loadAccounts() {
2474
- if (!existsSync9(ACCOUNTS_PATH)) return [];
2595
+ if (!existsSync10(ACCOUNTS_PATH)) return [];
2475
2596
  try {
2476
2597
  return JSON.parse(readFileSync10(ACCOUNTS_PATH, "utf-8"));
2477
2598
  } catch {
@@ -2753,9 +2874,9 @@ var test_accounts_default = defineCommand7({
2753
2874
  import { defineCommand as defineCommand8 } from "citty";
2754
2875
  import { execSync as execSync2 } from "child_process";
2755
2876
  import { randomUUID } from "crypto";
2756
- import { readFileSync as readFileSync11, readdirSync as readdirSync4, existsSync as existsSync10 } from "fs";
2757
- import { join as join11, resolve as resolve7, dirname as dirname2 } from "path";
2758
- import { pathToFileURL } from "url";
2877
+ import { readFileSync as readFileSync11, readdirSync as readdirSync4, existsSync as existsSync11 } from "fs";
2878
+ import { join as join12, resolve as resolve7, dirname as dirname2 } from "path";
2879
+ import { pathToFileURL as pathToFileURL2 } from "url";
2759
2880
  import * as p4 from "@clack/prompts";
2760
2881
  var DEPLOY_URL3 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
2761
2882
  var API_URL2 = process.env.DEEPSPACE_API_URL ?? PLATFORM_URLS.api;
@@ -2893,6 +3014,11 @@ var deploy_default = defineCommand8({
2893
3014
  type: "boolean",
2894
3015
  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
3016
  default: false
3017
+ },
3018
+ adopt: {
3019
+ type: "boolean",
3020
+ description: "Confirm adopting an existing app you can deploy but do not own (collaborator/admin on-behalf) when wrangler.toml has no DEEPSPACE_APP_ID. Without this flag an interactive prompt asks.",
3021
+ default: false
2896
3022
  }
2897
3023
  },
2898
3024
  async run({ args }) {
@@ -2932,16 +3058,37 @@ var deploy_default = defineCommand8({
2932
3058
  process.exit(1);
2933
3059
  }
2934
3060
  if (!appId) {
2935
- const adopted = await resolveOwnedAppId(DEPLOY_URL3, token, appName);
2936
- appId = adopted ?? mintAppId();
3061
+ const existing = await resolveExistingAppId(DEPLOY_URL3, token, appName);
3062
+ if (existing.kind === "taken") {
3063
+ p4.cancel(
3064
+ `"${appName}" is taken by an app you don't have deploy access to. Ask the owner to add you as a collaborator (\`deepspace collaborators add\`), or pick a different \`name\` in wrangler.toml.`
3065
+ );
3066
+ process.exit(1);
3067
+ }
3068
+ if (existing.kind === "adopted" && !existing.owned && !args.adopt) {
3069
+ if (!process.stdin.isTTY) {
3070
+ p4.cancel(
3071
+ `"${appName}" is an existing app owned by another account that you can deploy on-behalf (collaborator/admin). Confirmation needs an interactive terminal \u2014 re-run with --adopt to deploy it, or pick a different \`name\` in wrangler.toml if you meant a new app.`
3072
+ );
3073
+ process.exit(1);
3074
+ }
3075
+ const yes = await p4.confirm({
3076
+ message: `"${appName}" is an existing app owned by another account that you can deploy on-behalf (you are a collaborator or admin). Deploy it? (Meant a new app? Pick a different \`name\` in wrangler.toml.)`
3077
+ });
3078
+ if (p4.isCancel(yes) || !yes) {
3079
+ p4.cancel("Deploy cancelled.");
3080
+ process.exit(1);
3081
+ }
3082
+ }
3083
+ appId = existing.kind === "adopted" ? existing.appId : mintAppId();
2937
3084
  writeAppId(appDir, appId, { wranglerEnv: envName });
2938
3085
  p4.log.info(
2939
- adopted ? `Adopted existing app id ${appId} for ${appName} \u2014 commit wrangler.toml.` : `Minted app id ${appId} \u2014 commit wrangler.toml.`
3086
+ existing.kind === "adopted" ? `Adopted existing app id ${appId} for ${appName} \u2014 commit wrangler.toml.` : `Minted app id ${appId} \u2014 commit wrangler.toml.`
2940
3087
  );
2941
3088
  }
2942
3089
  p4.log.info(`Id: ${appId}`);
2943
3090
  const readCustomDevVarsAt = (path) => {
2944
- if (!existsSync10(path)) return {};
3091
+ if (!existsSync11(path)) return {};
2945
3092
  const section = extractCustomDevVars(stripGeneratedSecretsCache(readFileSync11(path, "utf-8")));
2946
3093
  if (!section) return {};
2947
3094
  try {
@@ -2980,6 +3127,10 @@ var deploy_default = defineCommand8({
2980
3127
  }
2981
3128
  const junk = removeMacosJunk(appDir);
2982
3129
  if (junk > 0) p4.log.info(`Removed ${junk} macOS metadata file(s) (._*, .DS_Store)`);
3130
+ const lintFindings = await lintProjectSchemas(appDir);
3131
+ if (lintFindings && lintFindings.length > 0) {
3132
+ p4.log.warn(formatSchemaLintFindings(lintFindings).join("\n"));
3133
+ }
2983
3134
  const s = createSpinner();
2984
3135
  s.start("Building...");
2985
3136
  let preparedWranglerConfig;
@@ -3002,14 +3153,14 @@ var deploy_default = defineCommand8({
3002
3153
  preparedWranglerConfig?.cleanup();
3003
3154
  }
3004
3155
  s.stop("Built");
3005
- const deployConfigPath = join11(appDir, ".wrangler", "deploy", "config.json");
3006
- if (!existsSync10(deployConfigPath)) {
3156
+ const deployConfigPath = join12(appDir, ".wrangler", "deploy", "config.json");
3157
+ if (!existsSync11(deployConfigPath)) {
3007
3158
  p4.cancel("Build output config not found at .wrangler/deploy/config.json");
3008
3159
  process.exit(1);
3009
3160
  }
3010
3161
  const deployConfig = JSON.parse(readFileSync11(deployConfigPath, "utf-8"));
3011
3162
  const outputWranglerPath = resolve7(dirname2(deployConfigPath), deployConfig.configPath);
3012
- if (!existsSync10(outputWranglerPath)) {
3163
+ if (!existsSync11(outputWranglerPath)) {
3013
3164
  p4.cancel(`Output wrangler.json not found at ${outputWranglerPath}`);
3014
3165
  process.exit(1);
3015
3166
  }
@@ -3022,13 +3173,13 @@ var deploy_default = defineCommand8({
3022
3173
  process.exit(1);
3023
3174
  }
3024
3175
  const workerDir = dirname2(outputWranglerPath);
3025
- const workerBundlePath = join11(workerDir, outputConfig.main);
3176
+ const workerBundlePath = join12(workerDir, outputConfig.main);
3026
3177
  const clientDir = outputConfig.assets?.directory ? resolve7(workerDir, outputConfig.assets.directory) : null;
3027
- if (!existsSync10(workerBundlePath)) {
3178
+ if (!existsSync11(workerBundlePath)) {
3028
3179
  p4.cancel(`Worker bundle not found at ${workerBundlePath}`);
3029
3180
  process.exit(1);
3030
3181
  }
3031
- if (!clientDir || !existsSync10(clientDir)) {
3182
+ if (!clientDir || !existsSync11(clientDir)) {
3032
3183
  p4.cancel(`Client assets not found at ${clientDir}`);
3033
3184
  process.exit(1);
3034
3185
  }
@@ -3263,13 +3414,13 @@ then redeploy. (Pass --allow-missing-secrets to deploy without them.)`
3263
3414
  }
3264
3415
  });
3265
3416
  async function syncSubscriptionPlans(appDir, appName, token) {
3266
- const subsPath = join11(appDir, "src", "subscriptions.ts");
3267
- if (!existsSync10(subsPath)) return;
3417
+ const subsPath = join12(appDir, "src", "subscriptions.ts");
3418
+ if (!existsSync11(subsPath)) return;
3268
3419
  let plans;
3269
3420
  try {
3270
3421
  const esbuild = await import("esbuild");
3271
- const outDir = join11(appDir, ".wrangler", "deploy");
3272
- const outFile = join11(outDir, "subscriptions.bundle.mjs");
3422
+ const outDir = join12(appDir, ".wrangler", "deploy");
3423
+ const outFile = join12(outDir, "subscriptions.bundle.mjs");
3273
3424
  await esbuild.build({
3274
3425
  entryPoints: [subsPath],
3275
3426
  bundle: true,
@@ -3280,7 +3431,7 @@ async function syncSubscriptionPlans(appDir, appName, token) {
3280
3431
  logLevel: "silent",
3281
3432
  write: true
3282
3433
  });
3283
- const mod = await import(`${pathToFileURL(outFile).href}?t=${Date.now()}`);
3434
+ const mod = await import(`${pathToFileURL2(outFile).href}?t=${Date.now()}`);
3284
3435
  plans = mod.subscriptionPlans;
3285
3436
  } catch (err) {
3286
3437
  p4.log.warn(`Could not load src/subscriptions.ts: ${errMessage(err)}`);
@@ -3411,13 +3562,13 @@ current billing period? (Skip to leave them grandfathered.)`
3411
3562
  }
3412
3563
  }
3413
3564
  async function syncOneTimeProducts(appDir, appName, token) {
3414
- const productsPath = join11(appDir, "src", "products.ts");
3415
- if (!existsSync10(productsPath)) return;
3565
+ const productsPath = join12(appDir, "src", "products.ts");
3566
+ if (!existsSync11(productsPath)) return;
3416
3567
  let products;
3417
3568
  try {
3418
3569
  const esbuild = await import("esbuild");
3419
- const outDir = join11(appDir, ".wrangler", "deploy");
3420
- const outFile = join11(outDir, "products.bundle.mjs");
3570
+ const outDir = join12(appDir, ".wrangler", "deploy");
3571
+ const outFile = join12(outDir, "products.bundle.mjs");
3421
3572
  await esbuild.build({
3422
3573
  entryPoints: [productsPath],
3423
3574
  bundle: true,
@@ -3428,7 +3579,7 @@ async function syncOneTimeProducts(appDir, appName, token) {
3428
3579
  logLevel: "silent",
3429
3580
  write: true
3430
3581
  });
3431
- const mod = await import(`${pathToFileURL(outFile).href}?t=${Date.now()}`);
3582
+ const mod = await import(`${pathToFileURL2(outFile).href}?t=${Date.now()}`);
3432
3583
  products = mod.oneTimeProducts;
3433
3584
  } catch (err) {
3434
3585
  p4.log.warn(`Could not load src/products.ts: ${errMessage(err)}`);
@@ -3492,7 +3643,7 @@ function collectAssets(dir) {
3492
3643
  function walk(d, prefix) {
3493
3644
  for (const entry of readdirSync4(d, { withFileTypes: true })) {
3494
3645
  if (entry.name === ".assetsignore") continue;
3495
- const full = join11(d, entry.name);
3646
+ const full = join12(d, entry.name);
3496
3647
  const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
3497
3648
  if (entry.isDirectory()) {
3498
3649
  walk(full, rel);
@@ -3673,26 +3824,26 @@ var apps_default = defineCommand11({
3673
3824
 
3674
3825
  // src/cli/commands/add.ts
3675
3826
  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";
3827
+ import { existsSync as existsSync12, readFileSync as readFileSync12 } from "fs";
3828
+ import { resolve as resolve8, join as join13, dirname as dirname3 } from "path";
3678
3829
  import { fileURLToPath } from "url";
3679
3830
  import { spawnSync as spawnSync5 } from "child_process";
3680
3831
  function findScript(from) {
3681
- const inNodeModules = join12(from, "node_modules", "deepspace", "scripts", "add-feature.cjs");
3682
- if (existsSync11(inNodeModules)) return inNodeModules;
3683
- const inDotDeepspace = join12(from, ".deepspace", "scripts", "add-feature.cjs");
3684
- if (existsSync11(inDotDeepspace)) return inDotDeepspace;
3832
+ const inNodeModules = join13(from, "node_modules", "deepspace", "scripts", "add-feature.cjs");
3833
+ if (existsSync12(inNodeModules)) return inNodeModules;
3834
+ const inDotDeepspace = join13(from, ".deepspace", "scripts", "add-feature.cjs");
3835
+ if (existsSync12(inDotDeepspace)) return inDotDeepspace;
3685
3836
  return null;
3686
3837
  }
3687
3838
  function runningScript() {
3688
3839
  const here = dirname3(fileURLToPath(import.meta.url));
3689
- const script = join12(here, "..", "scripts", "add-feature.cjs");
3690
- return existsSync11(script) ? script : null;
3840
+ const script = join13(here, "..", "scripts", "add-feature.cjs");
3841
+ return existsSync12(script) ? script : null;
3691
3842
  }
3692
3843
  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;
3844
+ if (existsSync12(join13(dir, "wrangler.toml"))) return true;
3845
+ const pkgPath = join13(dir, "package.json");
3846
+ if (!existsSync12(pkgPath)) return false;
3696
3847
  try {
3697
3848
  const pkg2 = JSON.parse(readFileSync12(pkgPath, "utf-8"));
3698
3849
  return Boolean(pkg2.dependencies?.deepspace || pkg2.devDependencies?.deepspace);
@@ -3753,7 +3904,7 @@ var add_default = defineCommand12({
3753
3904
  const passArgs2 = args.list ? ["--list"] : args.info ? ["--info", String(args.info)] : ["--help"];
3754
3905
  runInstaller(script2, passArgs2, process.cwd());
3755
3906
  }
3756
- if (!existsSync11(appDir)) {
3907
+ if (!existsSync12(appDir)) {
3757
3908
  console.error(`Target directory not found: ${appDir}`);
3758
3909
  console.error("Create a new app first: deepspace create <name>");
3759
3910
  process.exit(1);
@@ -3769,8 +3920,8 @@ var add_default = defineCommand12({
3769
3920
  if (!script) {
3770
3921
  console.error("Could not find the deepspace feature installer.");
3771
3922
  console.error("Looked in:");
3772
- console.error(` ${join12(appDir, "node_modules", "deepspace", "scripts", "add-feature.cjs")}`);
3773
- console.error(` ${join12(appDir, ".deepspace", "scripts", "add-feature.cjs")}`);
3923
+ console.error(` ${join13(appDir, "node_modules", "deepspace", "scripts", "add-feature.cjs")}`);
3924
+ console.error(` ${join13(appDir, ".deepspace", "scripts", "add-feature.cjs")}`);
3774
3925
  console.error("Are you in a DeepSpace app directory? Try `npm install` first.");
3775
3926
  process.exit(1);
3776
3927
  }
@@ -4131,30 +4282,35 @@ var list3 = defineCommand14({
4131
4282
  async run({ args }) {
4132
4283
  const token = await ensureToken();
4133
4284
  const app = await resolveAppTarget(DEPLOY_URL6, token, args.app);
4134
- const { collaborators } = await api2(
4135
- token,
4136
- `/api/app-collaborators/${encodeURIComponent(app)}`
4137
- );
4285
+ const { collaborators, pending = [] } = await api2(token, `/api/app-collaborators/${encodeURIComponent(app)}`);
4138
4286
  if (args.json) {
4139
- process.stdout.write(JSON.stringify(collaborators, null, 2) + "\n");
4287
+ process.stdout.write(JSON.stringify({ collaborators, pending }, null, 2) + "\n");
4140
4288
  return;
4141
4289
  }
4142
- if (!collaborators.length) {
4290
+ if (!collaborators.length && !pending.length) {
4143
4291
  console.log(`No collaborators on ${app}. Add one with \`deepspace collaborators add <email>\`.`);
4144
4292
  return;
4145
4293
  }
4146
- console.log(`COLLABORATORS ON ${app}`);
4147
- for (const c of collaborators) {
4148
- console.log(` ${c.emailDisplay}`);
4294
+ if (collaborators.length) {
4295
+ console.log(`COLLABORATORS ON ${app}`);
4296
+ for (const c of collaborators) {
4297
+ console.log(` ${c.emailDisplay}`);
4298
+ }
4299
+ }
4300
+ if (pending.length) {
4301
+ console.log(`PENDING INVITES ON ${app}`);
4302
+ for (const p10 of pending) {
4303
+ console.log(` ${p10.email} (expires ${new Date(p10.expiresAt).toLocaleDateString()})`);
4304
+ }
4149
4305
  }
4150
4306
  }
4151
4307
  });
4152
4308
  var add = defineCommand14({
4153
- meta: { name: "add", description: "Authorize a DeepSpace user to deploy your app" },
4309
+ meta: { name: "add", description: "Authorize someone to deploy your app" },
4154
4310
  args: {
4155
4311
  email: {
4156
4312
  type: "positional",
4157
- description: "Collaborator email (must already be a DeepSpace user)",
4313
+ description: "Collaborator email",
4158
4314
  required: true
4159
4315
  },
4160
4316
  app: { type: "string", alias: "a", description: "App id or name (defaults to ./wrangler.toml)" },
@@ -4169,16 +4325,29 @@ var add = defineCommand14({
4169
4325
  );
4170
4326
  process.exit(1);
4171
4327
  }
4172
- const { collaborator } = await api2(
4328
+ const res = await api2(
4173
4329
  token,
4174
4330
  `/api/app-collaborators/${encodeURIComponent(app)}`,
4175
4331
  { method: "POST", body: JSON.stringify({ email: args.email }) }
4176
4332
  );
4177
4333
  if (args.json) {
4178
- process.stdout.write(JSON.stringify(collaborator, null, 2) + "\n");
4334
+ process.stdout.write(JSON.stringify(res, null, 2) + "\n");
4179
4335
  return;
4180
4336
  }
4181
- console.log(`\u2713 ${collaborator.emailDisplay} can now deploy ${app}`);
4337
+ if (res.status === "invited" || res.status === "already_invited") {
4338
+ const expires = new Date(res.expiresAt).toLocaleDateString();
4339
+ if (res.status === "already_invited") {
4340
+ console.log(
4341
+ `\u2022 ${res.email} already has a pending invite (expires ${expires}). No new email was sent. Cancel it with \`deepspace collaborators cancel ${res.email}\` to reset.`
4342
+ );
4343
+ return;
4344
+ }
4345
+ console.log(
4346
+ `\u2713 Invite sent to ${res.email} (expires ${expires}). They become a collaborator when they sign in.`
4347
+ );
4348
+ return;
4349
+ }
4350
+ console.log(`\u2713 ${res.collaborator.emailDisplay} can now deploy ${app}`);
4182
4351
  }
4183
4352
  });
4184
4353
  var remove = defineCommand14({
@@ -4213,9 +4382,41 @@ var remove = defineCommand14({
4213
4382
  console.log(`\u2713 ${found.emailDisplay} can no longer deploy ${app}`);
4214
4383
  }
4215
4384
  });
4385
+ var cancel4 = defineCommand14({
4386
+ meta: { name: "cancel", description: "Cancel a pending (un-accepted) email invite" },
4387
+ args: {
4388
+ email: { type: "positional", description: "Invited email", required: true },
4389
+ app: { type: "string", alias: "a", description: "App id or name (defaults to ./wrangler.toml)" },
4390
+ json: { type: "boolean", description: "Emit JSON" }
4391
+ },
4392
+ async run({ args }) {
4393
+ const token = await ensureToken();
4394
+ const app = await resolveAppTarget(DEPLOY_URL6, token, args.app);
4395
+ const { pending = [] } = await api2(
4396
+ token,
4397
+ `/api/app-collaborators/${encodeURIComponent(app)}`
4398
+ );
4399
+ const target = args.email.trim().toLowerCase();
4400
+ const found = pending.find((p10) => p10.email.toLowerCase() === target);
4401
+ if (!found) {
4402
+ console.error(`${args.email} has no pending invite on ${app}`);
4403
+ process.exit(1);
4404
+ }
4405
+ await api2(
4406
+ token,
4407
+ `/api/app-collaborators/${encodeURIComponent(app)}/pending/${encodeURIComponent(found.email)}`,
4408
+ { method: "DELETE" }
4409
+ );
4410
+ if (args.json) {
4411
+ process.stdout.write(JSON.stringify({ cancelled: found }, null, 2) + "\n");
4412
+ return;
4413
+ }
4414
+ console.log(`\u2713 Cancelled the pending invite to ${found.email} on ${app}`);
4415
+ }
4416
+ });
4216
4417
  var collaborators_default = defineCommand14({
4217
4418
  meta: { name: "collaborators", description: "Manage who can deploy your app" },
4218
- subCommands: { list: list3, add, remove }
4419
+ subCommands: { list: list3, add, remove, cancel: cancel4 }
4219
4420
  });
4220
4421
 
4221
4422
  // src/cli/commands/transfer.ts
@@ -4304,7 +4505,7 @@ var accept = defineCommand15({
4304
4505
  );
4305
4506
  }
4306
4507
  });
4307
- var cancel4 = defineCommand15({
4508
+ var cancel5 = defineCommand15({
4308
4509
  meta: { name: "cancel", description: "Cancel/decline the pending offer (either party)" },
4309
4510
  args: {
4310
4511
  app: { type: "string", alias: "a", description: "App id or name (defaults to ./wrangler.toml)" }
@@ -4318,7 +4519,7 @@ var cancel4 = defineCommand15({
4318
4519
  });
4319
4520
  var transfer_default = defineCommand15({
4320
4521
  meta: { name: "transfer", description: "Transfer app ownership to another user" },
4321
- subCommands: { offer, status: status2, accept, cancel: cancel4 }
4522
+ subCommands: { offer, status: status2, accept, cancel: cancel5 }
4322
4523
  });
4323
4524
 
4324
4525
  // src/cli/commands/integrations.ts
@@ -4746,8 +4947,8 @@ var invoke_default = defineCommand17({
4746
4947
 
4747
4948
  // src/cli/commands/library.ts
4748
4949
  import { defineCommand as defineCommand18 } from "citty";
4749
- import { existsSync as existsSync12, readFileSync as readFileSync14 } from "fs";
4750
- import { join as join13, resolve as resolve9 } from "path";
4950
+ import { existsSync as existsSync13, readFileSync as readFileSync14 } from "fs";
4951
+ import { join as join14, resolve as resolve9 } from "path";
4751
4952
  import { parse as parseToml4 } from "smol-toml";
4752
4953
  import * as p8 from "@clack/prompts";
4753
4954
  var DEFAULT_LIBRARY_APP = "deepdotspace-site";
@@ -4825,8 +5026,8 @@ var publish = defineCommand18({
4825
5026
  let appName = typeof args.app === "string" && args.app.trim() ? args.app.trim() : void 0;
4826
5027
  if (!appName) {
4827
5028
  const appDir = resolve9(typeof args.dir === "string" && args.dir ? args.dir : ".");
4828
- const wranglerPath = join13(appDir, "wrangler.toml");
4829
- if (!existsSync12(wranglerPath)) {
5029
+ const wranglerPath = join14(appDir, "wrangler.toml");
5030
+ if (!existsSync13(wranglerPath)) {
4830
5031
  return die("No --app provided and no wrangler.toml found. Either pass --app <name> or run from your app's directory.");
4831
5032
  }
4832
5033
  const cfg = parseToml4(readFileSync14(wranglerPath, "utf-8"));
@@ -4963,8 +5164,8 @@ var library_default = defineCommand18({
4963
5164
  // src/cli/commands/feedback.ts
4964
5165
  import { defineCommand as defineCommand19 } from "citty";
4965
5166
  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";
5167
+ import { existsSync as existsSync14, readFileSync as readFileSync15 } from "fs";
5168
+ import { dirname as dirname4, join as join15 } from "path";
4968
5169
  import { fileURLToPath as fileURLToPath2 } from "url";
4969
5170
  import { release } from "os";
4970
5171
  var API_URL7 = process.env.DEEPSPACE_API_URL ?? PLATFORM_URLS.api;
@@ -4987,11 +5188,11 @@ function readCliVersion() {
4987
5188
  try {
4988
5189
  const here = dirname4(fileURLToPath2(import.meta.url));
4989
5190
  const candidates = [
4990
- join14(here, "..", "package.json"),
4991
- join14(here, "..", "..", "..", "package.json")
5191
+ join15(here, "..", "package.json"),
5192
+ join15(here, "..", "..", "..", "package.json")
4992
5193
  ];
4993
5194
  for (const path of candidates) {
4994
- if (!existsSync13(path)) continue;
5195
+ if (!existsSync14(path)) continue;
4995
5196
  const pkg2 = JSON.parse(readFileSync15(path, "utf-8"));
4996
5197
  if (pkg2.name === "deepspace" && pkg2.version) return pkg2.version;
4997
5198
  }
@@ -5609,8 +5810,8 @@ var init_default = defineCommand21({
5609
5810
 
5610
5811
  // src/cli/cli.ts
5611
5812
  var __dirname = dirname5(fileURLToPath3(import.meta.url));
5612
- var pkg = JSON.parse(readFileSync17(join15(__dirname, "..", "package.json"), "utf-8"));
5613
- if (process.platform === "linux" && !process.env.SSL_CERT_FILE && existsSync14("/etc/ssl/certs/ca-certificates.crt")) {
5813
+ var pkg = JSON.parse(readFileSync17(join16(__dirname, "..", "package.json"), "utf-8"));
5814
+ if (process.platform === "linux" && !process.env.SSL_CERT_FILE && existsSync15("/etc/ssl/certs/ca-certificates.crt")) {
5614
5815
  process.env.SSL_CERT_FILE = "/etc/ssl/certs/ca-certificates.crt";
5615
5816
  }
5616
5817
  var create2 = defineCommand22({