@kryd/cli 0.2.1 → 0.3.0

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.
Files changed (2) hide show
  1. package/dist/index.js +328 -82
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -88,6 +88,69 @@ var PRODUCTION_TLD = "kryd.cloud";
88
88
  var PREVIEW_TLD = "kryd.dev";
89
89
  var TENANT_SLUG_RE = new RegExp(`^[a-z0-9](?:[a-z0-9-]{0,${MAX_TENANT_SLUG - 2}}[a-z0-9])?$`);
90
90
 
91
+ // ../../packages/shared-types/dist/reserved-slugs.js
92
+ var FORGEJO_RESERVED_USERNAMES = [
93
+ ".",
94
+ "..",
95
+ "-",
96
+ ".well-known",
97
+ "api",
98
+ "metrics",
99
+ "v2",
100
+ "assets",
101
+ "attachments",
102
+ "avatar",
103
+ "avatars",
104
+ "repo-avatars",
105
+ "captcha",
106
+ "login",
107
+ "org",
108
+ "repo",
109
+ "user",
110
+ "admin",
111
+ "explore",
112
+ "issues",
113
+ "pulls",
114
+ "milestones",
115
+ "notifications",
116
+ "report_abuse",
117
+ "favicon.ico",
118
+ "manifest.json",
119
+ "robots.txt",
120
+ "sitemap.xml",
121
+ "ssh_info",
122
+ "swagger.v1.json",
123
+ "ghost",
124
+ "gitea-actions",
125
+ "forgejo-actions"
126
+ ];
127
+ var PLATFORM_RESERVED_SLUGS = [
128
+ "kryd",
129
+ "krydhq",
130
+ "www",
131
+ "api",
132
+ "app",
133
+ "ai",
134
+ "docs",
135
+ "forge",
136
+ "admin",
137
+ "console",
138
+ "status",
139
+ "mail",
140
+ "support",
141
+ "billing",
142
+ "account",
143
+ "accounts",
144
+ "static",
145
+ "assets",
146
+ "cdn",
147
+ "dashboard"
148
+ ];
149
+ var RESERVED = /* @__PURE__ */ new Set([
150
+ ...FORGEJO_RESERVED_USERNAMES,
151
+ ...PLATFORM_RESERVED_SLUGS
152
+ ]);
153
+
91
154
  // ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
92
155
  var external_exports = {};
93
156
  __export(external_exports, {
@@ -14806,6 +14869,7 @@ import {
14806
14869
  existsSync,
14807
14870
  mkdirSync,
14808
14871
  readFileSync,
14872
+ rmSync,
14809
14873
  writeFileSync
14810
14874
  } from "node:fs";
14811
14875
  import { homedir } from "node:os";
@@ -14922,6 +14986,18 @@ function writeSecretFile(path, contents) {
14922
14986
  writeFileSync(path, contents, { mode: 384 });
14923
14987
  chmodSync(path, 384);
14924
14988
  }
14989
+ function clearProjectLinkIfMatches(projectId, cwd = process.cwd()) {
14990
+ const root = findProjectLinkDir(cwd);
14991
+ if (!root) return false;
14992
+ const link = loadProjectLink(cwd);
14993
+ if (link?.projectId !== projectId) return false;
14994
+ try {
14995
+ rmSync(join(root, PROJECT_LINK_DIR, PROJECT_LINK_FILE));
14996
+ return true;
14997
+ } catch {
14998
+ return false;
14999
+ }
15000
+ }
14925
15001
  function resolveProjectId(explicit, cwd = process.cwd()) {
14926
15002
  if (explicit) return explicit;
14927
15003
  return loadProjectLink(cwd)?.projectId ?? null;
@@ -15241,6 +15317,49 @@ async function detachDatabase(apiUrl, token, projectId) {
15241
15317
  );
15242
15318
  }
15243
15319
  }
15320
+ async function getProject(apiUrl, token, projectId) {
15321
+ const res = await fetch(`${apiUrl}/projects/${encodeURIComponent(projectId)}`, {
15322
+ headers: { authorization: `Bearer ${token}` }
15323
+ });
15324
+ if (res.status === 404) return null;
15325
+ if (!res.ok) {
15326
+ throw new ApiError(`Reading the project failed (${res.status})`, await parseEnvelope(res));
15327
+ }
15328
+ return await res.json();
15329
+ }
15330
+ async function deleteProject(apiUrl, token, projectId) {
15331
+ const res = await fetch(`${apiUrl}/projects/${encodeURIComponent(projectId)}`, {
15332
+ method: "DELETE",
15333
+ headers: { authorization: `Bearer ${token}` }
15334
+ });
15335
+ if (!res.ok) {
15336
+ throw new ApiError(`Project delete failed (${res.status})`, await parseEnvelope(res));
15337
+ }
15338
+ return await res.json();
15339
+ }
15340
+ async function pollProjectDeleted(read, opts) {
15341
+ const intervalMs = opts?.intervalMs ?? 2e3;
15342
+ const timeoutMs = opts?.timeoutMs ?? 5 * 6e4;
15343
+ const maxConsecutiveErrors = opts?.maxConsecutiveErrors ?? 5;
15344
+ const sleep = opts?.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
15345
+ const started = Date.now();
15346
+ let consecutiveErrors = 0;
15347
+ let last = null;
15348
+ for (; ; ) {
15349
+ try {
15350
+ const project2 = await read();
15351
+ consecutiveErrors = 0;
15352
+ if (project2 === null) return { gone: true, status: null };
15353
+ last = project2.status;
15354
+ opts?.onTick?.(project2.status);
15355
+ if (project2.status === "delete_failed") return { gone: false, status: last };
15356
+ } catch (err) {
15357
+ if (++consecutiveErrors >= maxConsecutiveErrors) throw err;
15358
+ }
15359
+ if (Date.now() - started >= timeoutMs) return { gone: false, status: last };
15360
+ await sleep(intervalMs);
15361
+ }
15362
+ }
15244
15363
  async function detachStorage(apiUrl, token, projectId) {
15245
15364
  const res = await fetch(`${apiUrl}/storage/${encodeURIComponent(projectId)}`, {
15246
15365
  method: "DELETE",
@@ -15711,6 +15830,18 @@ async function promptHidden(question, io) {
15711
15830
  io.output.write("\n");
15712
15831
  }
15713
15832
  }
15833
+ async function promptLine(question, io) {
15834
+ const rl = createInterface({ input: io.input, output: io.output });
15835
+ try {
15836
+ return await new Promise((resolve2) => {
15837
+ rl.once("SIGINT", () => resolve2(null));
15838
+ rl.once("close", () => resolve2(null));
15839
+ rl.question(question, (answer) => resolve2(answer));
15840
+ });
15841
+ } finally {
15842
+ rl.close();
15843
+ }
15844
+ }
15714
15845
  async function promptConfirm(question, io) {
15715
15846
  const rl = createInterface({ input: io.input, output: io.output });
15716
15847
  try {
@@ -15885,7 +16016,7 @@ async function runInit(opts) {
15885
16016
  }
15886
16017
  const name = opts.name ?? detected.name;
15887
16018
  try {
15888
- const { project, repo, pushToken } = await linkProject(apiUrl, token, {
16019
+ const { project: project2, repo, pushToken } = await linkProject(apiUrl, token, {
15889
16020
  name,
15890
16021
  framework,
15891
16022
  // First `kryd init` for the account claims the tenant slug (the vanity routing key in
@@ -15895,7 +16026,7 @@ async function runInit(opts) {
15895
16026
  });
15896
16027
  let linkNote = "";
15897
16028
  try {
15898
- saveProjectLink(cwd, { projectId: project.id });
16029
+ saveProjectLink(cwd, { projectId: project2.id });
15899
16030
  linkNote = "Linked this folder \u2192 .kryd/project.json \u2014 `kryd deploy` / `kryd logs` now work here with no id.\n";
15900
16031
  try {
15901
16032
  ensureKrydIgnored(cwd);
@@ -15907,7 +16038,7 @@ async function runInit(opts) {
15907
16038
  }
15908
16039
  } catch (linkErr) {
15909
16040
  process.stderr.write(
15910
- `warning: could not write .kryd/project.json (${linkErr instanceof Error ? linkErr.message : String(linkErr)}) \u2014 pass the project id explicitly, e.g. \`kryd deploy ${project.id}\`.
16041
+ `warning: could not write .kryd/project.json (${linkErr instanceof Error ? linkErr.message : String(linkErr)}) \u2014 pass the project id explicitly, e.g. \`kryd deploy ${project2.id}\`.
15911
16042
  `
15912
16043
  );
15913
16044
  }
@@ -15940,9 +16071,9 @@ Install git (https://git-scm.com/downloads), then re-run \`kryd init\` here.
15940
16071
  break;
15941
16072
  }
15942
16073
  process.stdout.write(
15943
- `Linked "${name}" (${project.framework}) \u2192 ${repo.htmlUrl}
15944
- Production URL: https://${project.subdomain}.${PRODUCTION_TLD}
15945
- Preview branches deploy to https://<branch>-<hash>-${project.subdomain}.${PREVIEW_TLD}
16074
+ `Linked "${name}" (${project2.framework}) \u2192 ${repo.htmlUrl}
16075
+ Production URL: https://${project2.subdomain}.${PRODUCTION_TLD}
16076
+ Preview branches deploy to https://<branch>-<hash>-${project2.subdomain}.${PREVIEW_TLD}
15946
16077
  ${linkNote}
15947
16078
  ${nextStep}`
15948
16079
  );
@@ -16060,13 +16191,13 @@ async function runDeploy(opts) {
16060
16191
  process.exitCode = 1;
16061
16192
  return;
16062
16193
  }
16063
- const project = resolveProjectId(opts.project, opts.cwd);
16064
- if (!project) {
16194
+ const project2 = resolveProjectId(opts.project, opts.cwd);
16195
+ if (!project2) {
16065
16196
  reportNotLinked("kryd deploy <projectId>");
16066
16197
  return;
16067
16198
  }
16068
16199
  try {
16069
- const deploymentId = await triggerDeploy(apiUrl, token, project);
16200
+ const deploymentId = await triggerDeploy(apiUrl, token, project2);
16070
16201
  process.stdout.write(`Triggered deploy ${deploymentId}
16071
16202
  `);
16072
16203
  await followDeploy(apiUrl, token, deploymentId);
@@ -16211,8 +16342,8 @@ async function runRollback(opts) {
16211
16342
  process.exitCode = 1;
16212
16343
  return;
16213
16344
  }
16214
- const project = resolveProjectId(opts.project, opts.cwd);
16215
- if (!project) {
16345
+ const project2 = resolveProjectId(opts.project, opts.cwd);
16346
+ if (!project2) {
16216
16347
  reportNotLinked("kryd rollback <projectId> [deployment]");
16217
16348
  return;
16218
16349
  }
@@ -16220,7 +16351,7 @@ async function runRollback(opts) {
16220
16351
  const { deploymentId, rolledBackTo, note } = await rollbackDeploy(
16221
16352
  apiUrl,
16222
16353
  token,
16223
- project,
16354
+ project2,
16224
16355
  opts.deployment
16225
16356
  );
16226
16357
  process.stdout.write(
@@ -16241,8 +16372,8 @@ async function runRedeploy(opts) {
16241
16372
  process.exitCode = 1;
16242
16373
  return;
16243
16374
  }
16244
- const project = resolveProjectId(opts.project, opts.cwd);
16245
- if (!project) {
16375
+ const project2 = resolveProjectId(opts.project, opts.cwd);
16376
+ if (!project2) {
16246
16377
  reportNotLinked("kryd redeploy <projectId>");
16247
16378
  return;
16248
16379
  }
@@ -16250,7 +16381,7 @@ async function runRedeploy(opts) {
16250
16381
  const { deploymentId, redeployedCommit, note } = await redeployProject(
16251
16382
  apiUrl,
16252
16383
  token,
16253
- project
16384
+ project2
16254
16385
  );
16255
16386
  process.stdout.write(
16256
16387
  `Redeploying ${redeployedCommit.slice(0, 7)} \u2192 ${deploymentId}
@@ -16270,8 +16401,8 @@ async function runDbCreate(opts) {
16270
16401
  process.exitCode = 1;
16271
16402
  return;
16272
16403
  }
16273
- const project = resolveProjectId(opts.project, opts.cwd);
16274
- if (!project) {
16404
+ const project2 = resolveProjectId(opts.project, opts.cwd);
16405
+ if (!project2) {
16275
16406
  reportNotLinked("kryd db create <projectId>");
16276
16407
  return;
16277
16408
  }
@@ -16299,29 +16430,29 @@ async function runDbCreate(opts) {
16299
16430
  }
16300
16431
  try {
16301
16432
  const db2 = await attachDatabase(apiUrl, token, {
16302
- projectId: project,
16433
+ projectId: project2,
16303
16434
  mode,
16304
16435
  ...connectionString ? { connectionString } : {}
16305
16436
  });
16306
16437
  if (db2.status === "ready") {
16307
16438
  process.stdout.write(
16308
- `Attached ${db2.mode} database ${db2.id} to ${project}.
16439
+ `Attached ${db2.mode} database ${db2.id} to ${project2}.
16309
16440
  The connection string will be injected as DATABASE_URL on your next deploy.
16310
16441
  `
16311
16442
  );
16312
16443
  return;
16313
16444
  }
16314
- process.stdout.write(`Provisioning a shared database for ${project}\u2026
16445
+ process.stdout.write(`Provisioning a shared database for ${project2}\u2026
16315
16446
  `);
16316
16447
  const result = await pollResourceUntilTerminal(
16317
- () => getDatabaseStatus(apiUrl, token, project)
16448
+ () => getDatabaseStatus(apiUrl, token, project2)
16318
16449
  );
16319
16450
  reportSettleResult(result, {
16320
- readyMsg: `Attached shared database ${db2.id} to ${project}.
16451
+ readyMsg: `Attached shared database ${db2.id} to ${project2}.
16321
16452
  The connection string will be injected as DATABASE_URL on your next deploy.
16322
16453
  `,
16323
16454
  failedLabel: "Database provisioning",
16324
- retryCmd: `kryd db create ${project}`
16455
+ retryCmd: `kryd db create ${project2}`
16325
16456
  });
16326
16457
  } catch (err) {
16327
16458
  reportError(err);
@@ -16351,24 +16482,24 @@ async function runStorageCreate(opts) {
16351
16482
  process.exitCode = 1;
16352
16483
  return;
16353
16484
  }
16354
- const project = resolveProjectId(opts.project, opts.cwd);
16355
- if (!project) {
16485
+ const project2 = resolveProjectId(opts.project, opts.cwd);
16486
+ if (!project2) {
16356
16487
  reportNotLinked("kryd storage create <projectId>");
16357
16488
  return;
16358
16489
  }
16359
16490
  try {
16360
- const bucket = await createStorage(apiUrl, token, { projectId: project });
16361
- process.stdout.write(`Provisioning object storage for ${project}\u2026
16491
+ const bucket = await createStorage(apiUrl, token, { projectId: project2 });
16492
+ process.stdout.write(`Provisioning object storage for ${project2}\u2026
16362
16493
  `);
16363
16494
  const result = await pollResourceUntilTerminal(
16364
- () => getStorageStatus(apiUrl, token, project)
16495
+ () => getStorageStatus(apiUrl, token, project2)
16365
16496
  );
16366
16497
  reportSettleResult(result, {
16367
- readyMsg: `Created object storage ${bucket.id} for ${project}.
16498
+ readyMsg: `Created object storage ${bucket.id} for ${project2}.
16368
16499
  Its S3 credentials will be injected on your next deploy.
16369
16500
  `,
16370
16501
  failedLabel: "Storage provisioning",
16371
- retryCmd: `kryd storage create ${project}`
16502
+ retryCmd: `kryd storage create ${project2}`
16372
16503
  });
16373
16504
  } catch (err) {
16374
16505
  reportError(err);
@@ -16391,6 +16522,113 @@ function reportDetachResult(result, opts) {
16391
16522
  process.exitCode = 1;
16392
16523
  }
16393
16524
  }
16525
+ async function runProjectRemove(opts) {
16526
+ const apiUrl = resolveApiUrl(opts.apiUrl);
16527
+ const token = loadConfig().token;
16528
+ if (!token) {
16529
+ process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
16530
+ process.exitCode = 1;
16531
+ return;
16532
+ }
16533
+ const projectId = resolveProjectId(opts.project, opts.cwd);
16534
+ if (!projectId) {
16535
+ reportNotLinked("kryd project rm <projectId>");
16536
+ return;
16537
+ }
16538
+ let project2;
16539
+ let resources;
16540
+ try {
16541
+ project2 = await getProject(apiUrl, token, projectId);
16542
+ if (!project2) {
16543
+ process.stderr.write(`No project ${projectId} on this account.
16544
+ `);
16545
+ process.exitCode = 1;
16546
+ return;
16547
+ }
16548
+ if (project2.status === "deleting") {
16549
+ process.stdout.write(`${project2.name} is already being deleted \u2014 following it.
16550
+ `);
16551
+ return followProjectDeletion(apiUrl, token, projectId, project2.name, opts);
16552
+ }
16553
+ resources = project2.status === "delete_failed" ? ["whatever the previous attempt did not manage to remove"] : await describeProjectResources(apiUrl, token, projectId);
16554
+ } catch (err) {
16555
+ reportError(err);
16556
+ return;
16557
+ }
16558
+ if (!opts.yes) {
16559
+ const io = opts.io ?? defaultPromptIO();
16560
+ if (!isInteractive(io)) {
16561
+ process.stderr.write(
16562
+ `Refusing to delete ${project2.name} without confirmation. Re-run with --yes to confirm non-interactively.
16563
+ `
16564
+ );
16565
+ process.exitCode = 1;
16566
+ return;
16567
+ }
16568
+ io.output.write(
16569
+ `This permanently deletes ${project2.name} and:
16570
+ ` + resources.map((r) => ` \u2022 ${r}
16571
+ `).join("") + "This cannot be undone.\n"
16572
+ );
16573
+ const typed = await promptLine(
16574
+ `Type ${project2.name} to confirm: `,
16575
+ io
16576
+ );
16577
+ if (typed !== project2.name) {
16578
+ process.stdout.write(`Left ${project2.name} in place.
16579
+ `);
16580
+ return;
16581
+ }
16582
+ }
16583
+ try {
16584
+ const accepted = await deleteProject(apiUrl, token, projectId);
16585
+ process.stdout.write(
16586
+ accepted.alreadyInProgress ? `${project2.name} was already being deleted \u2014 following it.
16587
+ ` : `Deleting ${project2.name}\u2026
16588
+ `
16589
+ );
16590
+ await followProjectDeletion(apiUrl, token, projectId, project2.name, opts);
16591
+ } catch (err) {
16592
+ reportError(err);
16593
+ }
16594
+ }
16595
+ async function followProjectDeletion(apiUrl, token, projectId, name, opts) {
16596
+ const result = await pollProjectDeleted(
16597
+ () => getProject(apiUrl, token, projectId),
16598
+ opts.pollOpts
16599
+ );
16600
+ if (result.gone) {
16601
+ const cleared = clearProjectLinkIfMatches(projectId, opts.cwd);
16602
+ process.stdout.write(
16603
+ `Deleted ${name}.
16604
+ ` + (cleared ? "Removed the local .kryd/project.json link.\n" : "")
16605
+ );
16606
+ return;
16607
+ }
16608
+ if (result.status === "delete_failed") {
16609
+ process.stderr.write(
16610
+ `Deleting ${name} failed part-way. Your project is still listed, but some of its resources may already be gone \u2014 the teardown removes them in order and stops where it failed.
16611
+ Re-run \`kryd project rm ${projectId}\` to resume from where it stopped.
16612
+ `
16613
+ );
16614
+ process.exitCode = 1;
16615
+ return;
16616
+ }
16617
+ process.stderr.write(
16618
+ `Could not confirm that ${name} is gone \u2014 it is still being deleted (or the worker stalled). Re-run \`kryd project rm ${projectId}\` to check.
16619
+ `
16620
+ );
16621
+ process.exitCode = 1;
16622
+ }
16623
+ async function describeProjectResources(apiUrl, token, projectId) {
16624
+ const out = ["its deployed app, its containers and its built images"];
16625
+ const db2 = await getDatabaseStatus(apiUrl, token, projectId).catch(() => null);
16626
+ if (db2) out.push("its Postgres database \u2014 and all the data in it");
16627
+ const storage2 = await getStorageStatus(apiUrl, token, projectId).catch(() => null);
16628
+ if (storage2) out.push("its object-storage bucket \u2014 and every object in it");
16629
+ out.push("its Git repository on the forge, and its push token");
16630
+ return out;
16631
+ }
16394
16632
  async function runDbDetach(opts) {
16395
16633
  const apiUrl = resolveApiUrl(opts.apiUrl);
16396
16634
  const token = loadConfig().token;
@@ -16399,23 +16637,23 @@ async function runDbDetach(opts) {
16399
16637
  process.exitCode = 1;
16400
16638
  return;
16401
16639
  }
16402
- const project = resolveProjectId(opts.project, opts.cwd);
16403
- if (!project) {
16640
+ const project2 = resolveProjectId(opts.project, opts.cwd);
16641
+ if (!project2) {
16404
16642
  reportNotLinked("kryd db detach <projectId>");
16405
16643
  return;
16406
16644
  }
16407
16645
  try {
16408
- await detachDatabase(apiUrl, token, project);
16409
- process.stdout.write(`Detaching the database from ${project}\u2026
16646
+ await detachDatabase(apiUrl, token, project2);
16647
+ process.stdout.write(`Detaching the database from ${project2}\u2026
16410
16648
  `);
16411
16649
  const result = await pollResourceDetached(
16412
- () => getDatabaseStatus(apiUrl, token, project)
16650
+ () => getDatabaseStatus(apiUrl, token, project2)
16413
16651
  );
16414
16652
  reportDetachResult(result, {
16415
- goneMsg: `Detached the database from ${project}.
16653
+ goneMsg: `Detached the database from ${project2}.
16416
16654
  `,
16417
16655
  failedLabel: "Database detach",
16418
- retryCmd: `kryd db detach ${project}`
16656
+ retryCmd: `kryd db detach ${project2}`
16419
16657
  });
16420
16658
  } catch (err) {
16421
16659
  reportError(err);
@@ -16429,15 +16667,15 @@ async function runAiEnable(opts) {
16429
16667
  process.exitCode = 1;
16430
16668
  return;
16431
16669
  }
16432
- const project = resolveProjectId(opts.project, opts.cwd);
16433
- if (!project) {
16670
+ const project2 = resolveProjectId(opts.project, opts.cwd);
16671
+ if (!project2) {
16434
16672
  reportNotLinked("kryd ai enable <projectId>");
16435
16673
  return;
16436
16674
  }
16437
16675
  try {
16438
- const status = await enableAi(apiUrl, token, { projectId: project });
16676
+ const status = await enableAi(apiUrl, token, { projectId: project2 });
16439
16677
  process.stdout.write(
16440
- `AI enabled for ${project}.
16678
+ `AI enabled for ${project2}.
16441
16679
  ` + (status.url ? `Endpoint: ${status.url} (OpenAI-compatible base URL \u2014 use it as-is)
16442
16680
  ` : "") + `AI_GATEWAY_URL + AI_GATEWAY_TOKEN will be injected on your next deploy (kryd deploy).
16443
16681
  `
@@ -16454,23 +16692,23 @@ async function runStorageDetach(opts) {
16454
16692
  process.exitCode = 1;
16455
16693
  return;
16456
16694
  }
16457
- const project = resolveProjectId(opts.project, opts.cwd);
16458
- if (!project) {
16695
+ const project2 = resolveProjectId(opts.project, opts.cwd);
16696
+ if (!project2) {
16459
16697
  reportNotLinked("kryd storage detach <projectId>");
16460
16698
  return;
16461
16699
  }
16462
16700
  try {
16463
- await detachStorage(apiUrl, token, project);
16464
- process.stdout.write(`Detaching object storage from ${project}\u2026
16701
+ await detachStorage(apiUrl, token, project2);
16702
+ process.stdout.write(`Detaching object storage from ${project2}\u2026
16465
16703
  `);
16466
16704
  const result = await pollResourceDetached(
16467
- () => getStorageStatus(apiUrl, token, project)
16705
+ () => getStorageStatus(apiUrl, token, project2)
16468
16706
  );
16469
16707
  reportDetachResult(result, {
16470
- goneMsg: `Detached object storage from ${project}.
16708
+ goneMsg: `Detached object storage from ${project2}.
16471
16709
  `,
16472
16710
  failedLabel: "Storage detach",
16473
- retryCmd: `kryd storage detach ${project}`
16711
+ retryCmd: `kryd storage detach ${project2}`
16474
16712
  });
16475
16713
  } catch (err) {
16476
16714
  reportError(err);
@@ -16523,16 +16761,16 @@ async function runEnvList(opts) {
16523
16761
  process.exitCode = 1;
16524
16762
  return;
16525
16763
  }
16526
- const project = resolveProjectId(opts.project, opts.cwd);
16527
- if (!project) {
16764
+ const project2 = resolveProjectId(opts.project, opts.cwd);
16765
+ if (!project2) {
16528
16766
  reportNotLinked("kryd env list <projectId>");
16529
16767
  return;
16530
16768
  }
16531
16769
  try {
16532
- const vars = await listEnvVars(apiUrl, token, project);
16770
+ const vars = await listEnvVars(apiUrl, token, project2);
16533
16771
  if (vars.length === 0) {
16534
16772
  process.stdout.write(
16535
- `No environment variables for ${project} yet.
16773
+ `No environment variables for ${project2} yet.
16536
16774
  Set one with \`kryd env set KEY --stdin\`, or attach a database, storage or the AI gateway.
16537
16775
  `
16538
16776
  );
@@ -16605,8 +16843,8 @@ async function runEnvPull(opts) {
16605
16843
  process.exitCode = 1;
16606
16844
  return;
16607
16845
  }
16608
- const project = resolveProjectId(opts.project, opts.cwd);
16609
- if (!project) {
16846
+ const project2 = resolveProjectId(opts.project, opts.cwd);
16847
+ if (!project2) {
16610
16848
  reportNotLinked("kryd env pull <projectId>");
16611
16849
  return;
16612
16850
  }
@@ -16624,11 +16862,11 @@ async function runEnvPull(opts) {
16624
16862
  return;
16625
16863
  }
16626
16864
  try {
16627
- const { values } = await pullEnvValues(apiUrl, token, project, environment);
16865
+ const { values } = await pullEnvValues(apiUrl, token, project2, environment);
16628
16866
  const entries = Object.entries(values).sort(
16629
16867
  ([a], [b]) => a.localeCompare(b)
16630
16868
  );
16631
- const header = `# Written by \`kryd env pull\` \u2014 your customer-set variables for ${project} (${environment}).
16869
+ const header = `# Written by \`kryd env pull\` \u2014 your customer-set variables for ${project2} (${environment}).
16632
16870
  # Regenerate with \`kryd env pull\`. Do not commit this file.
16633
16871
  `;
16634
16872
  const body = entries.map(([k, v]) => toDotenvLine(k, v)).join("\n");
@@ -16640,7 +16878,7 @@ ${body}
16640
16878
  ` : header);
16641
16879
  const count = entries.length;
16642
16880
  process.stdout.write(
16643
- count === 0 ? `No customer environment variables for ${project} (${environment}) \u2014 wrote an empty ${outRel}.
16881
+ count === 0 ? `No customer environment variables for ${project2} (${environment}) \u2014 wrote an empty ${outRel}.
16644
16882
  ` : `Wrote ${count} variable${count === 1 ? "" : "s"} to ${outRel} (${environment}), mode 0600.
16645
16883
  `
16646
16884
  );
@@ -16706,8 +16944,8 @@ async function runEnvSet(opts) {
16706
16944
  process.exitCode = 1;
16707
16945
  return;
16708
16946
  }
16709
- const project = resolveProjectId(opts.project, opts.cwd);
16710
- if (!project) {
16947
+ const project2 = resolveProjectId(opts.project, opts.cwd);
16948
+ if (!project2) {
16711
16949
  reportNotLinked("kryd env set KEY=VALUE <projectId>");
16712
16950
  return;
16713
16951
  }
@@ -16773,7 +17011,7 @@ async function runEnvSet(opts) {
16773
17011
  const written = await setEnvVar(
16774
17012
  apiUrl,
16775
17013
  token,
16776
- project,
17014
+ project2,
16777
17015
  key,
16778
17016
  resolved.value,
16779
17017
  scope,
@@ -16781,7 +17019,7 @@ async function runEnvSet(opts) {
16781
17019
  );
16782
17020
  const envTag = environment === "preview" ? " (preview)" : "";
16783
17021
  process.stdout.write(
16784
- `${written.created ? "Set" : "Updated"} ${written.key}${scope === "build" ? " (build-time)" : envTag} for ${project}.
17022
+ `${written.created ? "Set" : "Updated"} ${written.key}${scope === "build" ? " (build-time)" : envTag} for ${project2}.
16785
17023
  ` + // The public-bundle warning comes FIRST, before the applies-when note. Someone who reads one
16786
17024
  // line of output should read the consequential one.
16787
17025
  (scope === "build" ? `${BUILD_ENV_PUBLIC_WARNING}
@@ -16801,8 +17039,8 @@ async function runEnvRemove(opts) {
16801
17039
  process.exitCode = 1;
16802
17040
  return;
16803
17041
  }
16804
- const project = resolveProjectId(opts.project, opts.cwd);
16805
- if (!project) {
17042
+ const project2 = resolveProjectId(opts.project, opts.cwd);
17043
+ if (!project2) {
16806
17044
  reportNotLinked("kryd env rm KEY <projectId>");
16807
17045
  return;
16808
17046
  }
@@ -16837,10 +17075,10 @@ If it comes from a resource you attached, detach that instead (\`kryd db detach\
16837
17075
  }
16838
17076
  if (scope === "build") {
16839
17077
  try {
16840
- const vars = await listEnvVars(apiUrl, token, project, "build");
17078
+ const vars = await listEnvVars(apiUrl, token, project2, "build");
16841
17079
  if (!vars.some((v) => v.key === key && v.scope === "build")) {
16842
17080
  process.stderr.write(
16843
- `${key} is not set as a build-time variable on ${project}.
17081
+ `${key} is not set as a build-time variable on ${project2}.
16844
17082
  Run \`kryd env list\` to see what is set. (If you meant the runtime variable of that name, drop --build.)
16845
17083
  `
16846
17084
  );
@@ -16863,7 +17101,7 @@ Run \`kryd env list\` to see what is set. (If you meant the runtime variable of
16863
17101
  return;
16864
17102
  }
16865
17103
  const confirmed = await promptConfirm(
16866
- scope === "build" ? `Remove the build-time variable ${key} from ${project}? Your next build will not have it (the current image still does).` : environment === "preview" ? `Remove the preview override for ${key} on ${project}? Previews will revert to inheriting the production value.` : `Remove ${key} from ${project}? Its stored value (production and any preview override) is deleted and cannot be recovered.`,
17104
+ scope === "build" ? `Remove the build-time variable ${key} from ${project2}? Your next build will not have it (the current image still does).` : environment === "preview" ? `Remove the preview override for ${key} on ${project2}? Previews will revert to inheriting the production value.` : `Remove ${key} from ${project2}? Its stored value (production and any preview override) is deleted and cannot be recovered.`,
16867
17105
  io
16868
17106
  );
16869
17107
  if (!confirmed) {
@@ -16873,10 +17111,10 @@ Run \`kryd env list\` to see what is set. (If you meant the runtime variable of
16873
17111
  }
16874
17112
  }
16875
17113
  try {
16876
- const removed = await removeEnvVar(apiUrl, token, project, key, scope, environment);
17114
+ const removed = await removeEnvVar(apiUrl, token, project2, key, scope, environment);
16877
17115
  const envTag = environment === "preview" ? " (preview override)" : "";
16878
17116
  process.stdout.write(
16879
- `Removed ${removed.key}${scope === "build" ? " (build-time)" : envTag} from ${project}.
17117
+ `Removed ${removed.key}${scope === "build" ? " (build-time)" : envTag} from ${project2}.
16880
17118
  ` + // The build-scope warning is deliberately bleaker than the runtime one, because the truth is.
16881
17119
  // A runtime value is gone from the container at the next deploy. A build-time value is
16882
17120
  // COMPILED INTO the live image and into every bundle already downloaded from it — removing
@@ -16889,7 +17127,7 @@ Run \`kryd env list\` to see what is set. (If you meant the runtime variable of
16889
17127
  reportError(err);
16890
17128
  }
16891
17129
  }
16892
- var CLI_VERSION = true ? "0.2.1" : "0.0.0-dev";
17130
+ var CLI_VERSION = true ? "0.3.0" : "0.0.0-dev";
16893
17131
  var program = new Command();
16894
17132
  program.name("kryd").description("Kryd CLI").version(CLI_VERSION);
16895
17133
  program.command("login").description("Sign in via the browser (default) and store a token").option("--email <email>", "account email (with --password; non-interactive escape hatch)").option("--password <password>", "account password (with --email; visible in `ps` \u2014 prefer the browser flow)").option(
@@ -16903,7 +17141,10 @@ program.command("init").description("Link this project's repo + register its pus
16903
17141
  "react-router | nextjs | vite-spa | node (auto-detected if omitted)"
16904
17142
  ).option(
16905
17143
  "--tenant <slug>",
16906
- "your tenant slug (claimed once on first init \u2014 appears in every deploy URL)"
17144
+ // KRYD-265 / KRYD-188: this used to say "claimed once on first init", which was false — signup
17145
+ // has always assigned a slug, so the flag could only agree with it or 409. It now changes the
17146
+ // slug, and only while nothing derives from it.
17147
+ "your tenant slug \u2014 appears in every deploy URL; changeable until your first project"
16907
17148
  ).option("--api-url <url>", "control-plane API base URL").action((opts) => runInit(opts));
16908
17149
  program.command("logs [target]").description(
16909
17150
  "Follow logs. Default: a deploy's build/deploy logs ([target]=deployment id, latest if omitted). With --runtime: a container's stdout/stderr \u2014 [target]=a project id tails its live app, a deployment id (dpl_\u2026) tails THAT deploy's container (incl. a failed one, to see why it crashed)."
@@ -16921,24 +17162,28 @@ program.command("push [branch]").description(
16921
17162
  ).option("--api-url <url>", "control-plane API base URL").action(
16922
17163
  (branch, opts, cmd) => runPush({ ...opts, branch, gitArgs: cmd.args.slice(1) })
16923
17164
  );
16924
- program.command("deploy [project]").description("Trigger a deploy of the project's production branch and follow it live").option("--api-url <url>", "control-plane API base URL").action((project, opts) => runDeploy({ ...opts, project }));
17165
+ program.command("deploy [project]").description("Trigger a deploy of the project's production branch and follow it live").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runDeploy({ ...opts, project: project2 }));
16925
17166
  program.command("rollback [project] [deployment]").description("Roll back to a previous successful deploy (no rebuild) and follow it live").option("--api-url <url>", "control-plane API base URL").action(
16926
- (project, deployment, opts) => runRollback({ ...opts, project, deployment })
17167
+ (project2, deployment, opts) => runRollback({ ...opts, project: project2, deployment })
16927
17168
  );
16928
17169
  program.command("redeploy [project]").description(
16929
17170
  "Redeploy the current live commit (no rebuild) to apply config/env changes, and follow it live"
16930
- ).option("--api-url <url>", "control-plane API base URL").action((project, opts) => runRedeploy({ ...opts, project }));
17171
+ ).option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runRedeploy({ ...opts, project: project2 }));
17172
+ var project = program.command("project").description("Manage your projects");
17173
+ project.command("rm [project]").description(
17174
+ "Permanently delete a project and everything Kryd created for it (asks you to type its name)"
17175
+ ).option("--yes", "skip the confirmation (for non-interactive use)").option("--api-url <url>", "control-plane API base URL").action((projectArg, opts) => runProjectRemove({ ...opts, project: projectArg }));
16931
17176
  var db = program.command("db").description("Manage project databases");
16932
17177
  db.command("create [project]").description("Attach a shared or bring-your-own Postgres database to a project").option("--shared", "attach a shared managed database (the default)").option(
16933
17178
  "--connection-string <url>",
16934
17179
  "attach an existing database (BYO) by pasting its connection string"
16935
- ).option("--api-url <url>", "control-plane API base URL").action((project, opts) => runDbCreate({ ...opts, project }));
16936
- db.command("detach [project]").description("Tear down the project's database and reclaim its credential").option("--api-url <url>", "control-plane API base URL").action((project, opts) => runDbDetach({ ...opts, project }));
17180
+ ).option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runDbCreate({ ...opts, project: project2 }));
17181
+ db.command("detach [project]").description("Tear down the project's database and reclaim its credential").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runDbDetach({ ...opts, project: project2 }));
16937
17182
  var storage = program.command("storage").description("Manage project object storage");
16938
- storage.command("create [project]").description("Create an S3-compatible object-storage bucket for a project").option("--api-url <url>", "control-plane API base URL").action((project, opts) => runStorageCreate({ ...opts, project }));
16939
- storage.command("detach [project]").description("Tear down the project's object storage and reclaim its credentials").option("--api-url <url>", "control-plane API base URL").action((project, opts) => runStorageDetach({ ...opts, project }));
17183
+ storage.command("create [project]").description("Create an S3-compatible object-storage bucket for a project").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runStorageCreate({ ...opts, project: project2 }));
17184
+ storage.command("detach [project]").description("Tear down the project's object storage and reclaim its credentials").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runStorageDetach({ ...opts, project: project2 }));
16940
17185
  var env = program.command("env").description("Manage your app's environment variables");
16941
- env.command("list [project]").description("List the project's environment variables (names and origin \u2014 never values)").option("--api-url <url>", "control-plane API base URL").action((project, opts) => runEnvList({ ...opts, project }));
17186
+ env.command("list [project]").description("List the project's environment variables (names and origin \u2014 never values)").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runEnvList({ ...opts, project: project2 }));
16942
17187
  env.command("set <key> [project]").description(
16943
17188
  "Set one of your environment variables \u2014 `KEY=VALUE`, or `KEY` with --stdin / a prompt"
16944
17189
  ).option(
@@ -16950,14 +17195,14 @@ env.command("set <key> [project]").description(
16950
17195
  ).option(
16951
17196
  "--preview",
16952
17197
  "set the value for PREVIEW deploys only \u2014 an override that shadows production for previews (without it, a set targets production, which previews inherit)"
16953
- ).option("--api-url <url>", "control-plane API base URL").action((key, project, opts) => runEnvSet({ ...opts, spec: key, project }));
17198
+ ).option("--api-url <url>", "control-plane API base URL").action((key, project2, opts) => runEnvSet({ ...opts, spec: key, project: project2 }));
16954
17199
  env.command("rm <key> [project]").description("Remove one of your environment variables (asks for confirmation)").option("--yes", "skip the confirmation prompt (required when there is no terminal)").option(
16955
17200
  "--build",
16956
17201
  "remove the build-time variable of this name rather than the runtime one (the same name can exist as both)"
16957
17202
  ).option(
16958
17203
  "--preview",
16959
17204
  "remove only the PREVIEW override, reverting previews to inheriting production (without it, the whole key is removed \u2014 production and any preview override)"
16960
- ).option("--api-url <url>", "control-plane API base URL").action((key, project, opts) => runEnvRemove({ ...opts, key, project }));
17205
+ ).option("--api-url <url>", "control-plane API base URL").action((key, project2, opts) => runEnvRemove({ ...opts, key, project: project2 }));
16961
17206
  env.command("pull [project]").description(
16962
17207
  "Write your customer-set environment variables (values included) to a local .env file for development"
16963
17208
  ).option(
@@ -16966,9 +17211,9 @@ env.command("pull [project]").description(
16966
17211
  ).option(
16967
17212
  "--out <path>",
16968
17213
  "file to write, relative to the project root (default: .env.kryd \u2014 never your hand-maintained .env)"
16969
- ).option("--force", "overwrite the --out file if it already exists").option("--api-url <url>", "control-plane API base URL").action((project, opts) => runEnvPull({ ...opts, project }));
17214
+ ).option("--force", "overwrite the --out file if it already exists").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runEnvPull({ ...opts, project: project2 }));
16970
17215
  var ai = program.command("ai").description("Manage the app's EU AI gateway");
16971
- ai.command("enable [project]").description("Give the project an authenticated EU AI endpoint (injects on next deploy)").option("--api-url <url>", "control-plane API base URL").action((project, opts) => runAiEnable({ ...opts, project }));
17216
+ ai.command("enable [project]").description("Give the project an authenticated EU AI endpoint (injects on next deploy)").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runAiEnable({ ...opts, project: project2 }));
16972
17217
  if (import.meta.url === `file://${process.argv[1]}`) {
16973
17218
  program.parseAsync().catch((err) => {
16974
17219
  process.stderr.write(
@@ -16992,6 +17237,7 @@ export {
16992
17237
  runLogin,
16993
17238
  runLogout,
16994
17239
  runLogs,
17240
+ runProjectRemove,
16995
17241
  runPush,
16996
17242
  runRedeploy,
16997
17243
  runRollback,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kryd/cli",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Kryd CLI — push a React / Vite / Next.js app to the European cloud for your AI: git push → live with SSL, one-click managed Postgres & object storage, and an EU-hosted AI gateway already wired in. Your code and your model calls stay in the EU.",
5
5
  "keywords": [
6
6
  "kryd",