@kryd/cli 0.2.1 → 0.4.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 +420 -83
  2. package/package.json +3 -3
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,58 @@ 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 listProjects(apiUrl, token) {
15331
+ const res = await fetch(`${apiUrl}/projects`, {
15332
+ headers: { authorization: `Bearer ${token}` }
15333
+ });
15334
+ if (!res.ok) {
15335
+ throw new ApiError(`Listing projects failed (${res.status})`, await parseEnvelope(res));
15336
+ }
15337
+ return (await res.json()).items;
15338
+ }
15339
+ async function deleteProject(apiUrl, token, projectId) {
15340
+ const res = await fetch(`${apiUrl}/projects/${encodeURIComponent(projectId)}`, {
15341
+ method: "DELETE",
15342
+ headers: { authorization: `Bearer ${token}` }
15343
+ });
15344
+ if (!res.ok) {
15345
+ throw new ApiError(`Project delete failed (${res.status})`, await parseEnvelope(res));
15346
+ }
15347
+ return await res.json();
15348
+ }
15349
+ async function pollProjectDeleted(read, opts) {
15350
+ const intervalMs = opts?.intervalMs ?? 2e3;
15351
+ const timeoutMs = opts?.timeoutMs ?? 5 * 6e4;
15352
+ const maxConsecutiveErrors = opts?.maxConsecutiveErrors ?? 5;
15353
+ const sleep = opts?.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
15354
+ const started = Date.now();
15355
+ let consecutiveErrors = 0;
15356
+ let last = null;
15357
+ for (; ; ) {
15358
+ try {
15359
+ const project2 = await read();
15360
+ consecutiveErrors = 0;
15361
+ if (project2 === null) return { gone: true, status: null };
15362
+ last = project2.status;
15363
+ opts?.onTick?.(project2.status);
15364
+ if (project2.status === "delete_failed") return { gone: false, status: last };
15365
+ } catch (err) {
15366
+ if (++consecutiveErrors >= maxConsecutiveErrors) throw err;
15367
+ }
15368
+ if (Date.now() - started >= timeoutMs) return { gone: false, status: last };
15369
+ await sleep(intervalMs);
15370
+ }
15371
+ }
15244
15372
  async function detachStorage(apiUrl, token, projectId) {
15245
15373
  const res = await fetch(`${apiUrl}/storage/${encodeURIComponent(projectId)}`, {
15246
15374
  method: "DELETE",
@@ -15711,6 +15839,18 @@ async function promptHidden(question, io) {
15711
15839
  io.output.write("\n");
15712
15840
  }
15713
15841
  }
15842
+ async function promptLine(question, io) {
15843
+ const rl = createInterface({ input: io.input, output: io.output });
15844
+ try {
15845
+ return await new Promise((resolve2) => {
15846
+ rl.once("SIGINT", () => resolve2(null));
15847
+ rl.once("close", () => resolve2(null));
15848
+ rl.question(question, (answer) => resolve2(answer));
15849
+ });
15850
+ } finally {
15851
+ rl.close();
15852
+ }
15853
+ }
15714
15854
  async function promptConfirm(question, io) {
15715
15855
  const rl = createInterface({ input: io.input, output: io.output });
15716
15856
  try {
@@ -15885,17 +16025,20 @@ async function runInit(opts) {
15885
16025
  }
15886
16026
  const name = opts.name ?? detected.name;
15887
16027
  try {
15888
- const { project, repo, pushToken } = await linkProject(apiUrl, token, {
16028
+ const { project: project2, repo, pushToken } = await linkProject(apiUrl, token, {
15889
16029
  name,
15890
16030
  framework,
15891
16031
  // First `kryd init` for the account claims the tenant slug (the vanity routing key in
15892
16032
  // every deploy URL); later inits omit it (the account already has one). Story 3.8.
15893
16033
  // Built conditionally (exactOptionalPropertyTypes — don't pass an explicit undefined).
15894
- ...opts.tenant ? { tenantSlug: opts.tenant } : {}
16034
+ ...opts.tenant ? { tenantSlug: opts.tenant } : {},
16035
+ // KRYD-184a: only sent when the flag is present. The API defaults to private and sends
16036
+ // `private` explicitly to the forge on every path, so omitting this is never "public".
16037
+ ...opts.public ? { public: true } : {}
15895
16038
  });
15896
16039
  let linkNote = "";
15897
16040
  try {
15898
- saveProjectLink(cwd, { projectId: project.id });
16041
+ saveProjectLink(cwd, { projectId: project2.id });
15899
16042
  linkNote = "Linked this folder \u2192 .kryd/project.json \u2014 `kryd deploy` / `kryd logs` now work here with no id.\n";
15900
16043
  try {
15901
16044
  ensureKrydIgnored(cwd);
@@ -15907,7 +16050,7 @@ async function runInit(opts) {
15907
16050
  }
15908
16051
  } catch (linkErr) {
15909
16052
  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}\`.
16053
+ `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
16054
  `
15912
16055
  );
15913
16056
  }
@@ -15940,9 +16083,9 @@ Install git (https://git-scm.com/downloads), then re-run \`kryd init\` here.
15940
16083
  break;
15941
16084
  }
15942
16085
  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}
16086
+ `Linked "${name}" (${project2.framework}) \u2192 ${repo.htmlUrl}
16087
+ Production URL: https://${project2.subdomain}.${PRODUCTION_TLD}
16088
+ Preview branches deploy to https://<branch>-<hash>-${project2.subdomain}.${PREVIEW_TLD}
15946
16089
  ${linkNote}
15947
16090
  ${nextStep}`
15948
16091
  );
@@ -16060,13 +16203,13 @@ async function runDeploy(opts) {
16060
16203
  process.exitCode = 1;
16061
16204
  return;
16062
16205
  }
16063
- const project = resolveProjectId(opts.project, opts.cwd);
16064
- if (!project) {
16206
+ const project2 = resolveProjectId(opts.project, opts.cwd);
16207
+ if (!project2) {
16065
16208
  reportNotLinked("kryd deploy <projectId>");
16066
16209
  return;
16067
16210
  }
16068
16211
  try {
16069
- const deploymentId = await triggerDeploy(apiUrl, token, project);
16212
+ const deploymentId = await triggerDeploy(apiUrl, token, project2);
16070
16213
  process.stdout.write(`Triggered deploy ${deploymentId}
16071
16214
  `);
16072
16215
  await followDeploy(apiUrl, token, deploymentId);
@@ -16211,8 +16354,8 @@ async function runRollback(opts) {
16211
16354
  process.exitCode = 1;
16212
16355
  return;
16213
16356
  }
16214
- const project = resolveProjectId(opts.project, opts.cwd);
16215
- if (!project) {
16357
+ const project2 = resolveProjectId(opts.project, opts.cwd);
16358
+ if (!project2) {
16216
16359
  reportNotLinked("kryd rollback <projectId> [deployment]");
16217
16360
  return;
16218
16361
  }
@@ -16220,7 +16363,7 @@ async function runRollback(opts) {
16220
16363
  const { deploymentId, rolledBackTo, note } = await rollbackDeploy(
16221
16364
  apiUrl,
16222
16365
  token,
16223
- project,
16366
+ project2,
16224
16367
  opts.deployment
16225
16368
  );
16226
16369
  process.stdout.write(
@@ -16241,8 +16384,8 @@ async function runRedeploy(opts) {
16241
16384
  process.exitCode = 1;
16242
16385
  return;
16243
16386
  }
16244
- const project = resolveProjectId(opts.project, opts.cwd);
16245
- if (!project) {
16387
+ const project2 = resolveProjectId(opts.project, opts.cwd);
16388
+ if (!project2) {
16246
16389
  reportNotLinked("kryd redeploy <projectId>");
16247
16390
  return;
16248
16391
  }
@@ -16250,7 +16393,7 @@ async function runRedeploy(opts) {
16250
16393
  const { deploymentId, redeployedCommit, note } = await redeployProject(
16251
16394
  apiUrl,
16252
16395
  token,
16253
- project
16396
+ project2
16254
16397
  );
16255
16398
  process.stdout.write(
16256
16399
  `Redeploying ${redeployedCommit.slice(0, 7)} \u2192 ${deploymentId}
@@ -16270,8 +16413,8 @@ async function runDbCreate(opts) {
16270
16413
  process.exitCode = 1;
16271
16414
  return;
16272
16415
  }
16273
- const project = resolveProjectId(opts.project, opts.cwd);
16274
- if (!project) {
16416
+ const project2 = resolveProjectId(opts.project, opts.cwd);
16417
+ if (!project2) {
16275
16418
  reportNotLinked("kryd db create <projectId>");
16276
16419
  return;
16277
16420
  }
@@ -16299,29 +16442,29 @@ async function runDbCreate(opts) {
16299
16442
  }
16300
16443
  try {
16301
16444
  const db2 = await attachDatabase(apiUrl, token, {
16302
- projectId: project,
16445
+ projectId: project2,
16303
16446
  mode,
16304
16447
  ...connectionString ? { connectionString } : {}
16305
16448
  });
16306
16449
  if (db2.status === "ready") {
16307
16450
  process.stdout.write(
16308
- `Attached ${db2.mode} database ${db2.id} to ${project}.
16451
+ `Attached ${db2.mode} database ${db2.id} to ${project2}.
16309
16452
  The connection string will be injected as DATABASE_URL on your next deploy.
16310
16453
  `
16311
16454
  );
16312
16455
  return;
16313
16456
  }
16314
- process.stdout.write(`Provisioning a shared database for ${project}\u2026
16457
+ process.stdout.write(`Provisioning a shared database for ${project2}\u2026
16315
16458
  `);
16316
16459
  const result = await pollResourceUntilTerminal(
16317
- () => getDatabaseStatus(apiUrl, token, project)
16460
+ () => getDatabaseStatus(apiUrl, token, project2)
16318
16461
  );
16319
16462
  reportSettleResult(result, {
16320
- readyMsg: `Attached shared database ${db2.id} to ${project}.
16463
+ readyMsg: `Attached shared database ${db2.id} to ${project2}.
16321
16464
  The connection string will be injected as DATABASE_URL on your next deploy.
16322
16465
  `,
16323
16466
  failedLabel: "Database provisioning",
16324
- retryCmd: `kryd db create ${project}`
16467
+ retryCmd: `kryd db create ${project2}`
16325
16468
  });
16326
16469
  } catch (err) {
16327
16470
  reportError(err);
@@ -16351,24 +16494,24 @@ async function runStorageCreate(opts) {
16351
16494
  process.exitCode = 1;
16352
16495
  return;
16353
16496
  }
16354
- const project = resolveProjectId(opts.project, opts.cwd);
16355
- if (!project) {
16497
+ const project2 = resolveProjectId(opts.project, opts.cwd);
16498
+ if (!project2) {
16356
16499
  reportNotLinked("kryd storage create <projectId>");
16357
16500
  return;
16358
16501
  }
16359
16502
  try {
16360
- const bucket = await createStorage(apiUrl, token, { projectId: project });
16361
- process.stdout.write(`Provisioning object storage for ${project}\u2026
16503
+ const bucket = await createStorage(apiUrl, token, { projectId: project2 });
16504
+ process.stdout.write(`Provisioning object storage for ${project2}\u2026
16362
16505
  `);
16363
16506
  const result = await pollResourceUntilTerminal(
16364
- () => getStorageStatus(apiUrl, token, project)
16507
+ () => getStorageStatus(apiUrl, token, project2)
16365
16508
  );
16366
16509
  reportSettleResult(result, {
16367
- readyMsg: `Created object storage ${bucket.id} for ${project}.
16510
+ readyMsg: `Created object storage ${bucket.id} for ${project2}.
16368
16511
  Its S3 credentials will be injected on your next deploy.
16369
16512
  `,
16370
16513
  failedLabel: "Storage provisioning",
16371
- retryCmd: `kryd storage create ${project}`
16514
+ retryCmd: `kryd storage create ${project2}`
16372
16515
  });
16373
16516
  } catch (err) {
16374
16517
  reportError(err);
@@ -16391,6 +16534,152 @@ function reportDetachResult(result, opts) {
16391
16534
  process.exitCode = 1;
16392
16535
  }
16393
16536
  }
16537
+ function isProjectId(arg) {
16538
+ return arg.startsWith("proj_");
16539
+ }
16540
+ async function resolveProjectIdByName(apiUrl, token, name) {
16541
+ let projects;
16542
+ try {
16543
+ projects = await listProjects(apiUrl, token);
16544
+ } catch (err) {
16545
+ reportError(err);
16546
+ return null;
16547
+ }
16548
+ const matches = projects.filter((p) => p.name === name);
16549
+ if (matches.length === 0) {
16550
+ process.stderr.write(
16551
+ `No project named ${name} on this account. Run \`kryd project list\` to see the names.
16552
+ `
16553
+ );
16554
+ process.exitCode = 1;
16555
+ return null;
16556
+ }
16557
+ if (matches.length === 1) return matches[0].id;
16558
+ const listed = matches.map((p) => ` ${p.id} ${p.status}
16559
+ `).join("");
16560
+ process.stderr.write(
16561
+ `${matches.length} projects are named ${name}:
16562
+ ${listed}Re-run \`kryd project rm <id>\` with the one you mean.
16563
+ `
16564
+ );
16565
+ process.exitCode = 1;
16566
+ return null;
16567
+ }
16568
+ async function runProjectRemove(opts) {
16569
+ const apiUrl = resolveApiUrl(opts.apiUrl);
16570
+ const token = loadConfig().token;
16571
+ if (!token) {
16572
+ process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
16573
+ process.exitCode = 1;
16574
+ return;
16575
+ }
16576
+ let projectId;
16577
+ if (opts.project !== void 0 && !isProjectId(opts.project)) {
16578
+ projectId = await resolveProjectIdByName(apiUrl, token, opts.project);
16579
+ if (!projectId) return;
16580
+ } else {
16581
+ projectId = resolveProjectId(opts.project, opts.cwd);
16582
+ }
16583
+ if (!projectId) {
16584
+ reportNotLinked("kryd project rm <name|id>");
16585
+ return;
16586
+ }
16587
+ let project2;
16588
+ let resources;
16589
+ try {
16590
+ project2 = await getProject(apiUrl, token, projectId);
16591
+ if (!project2) {
16592
+ process.stderr.write(
16593
+ `No project with id ${projectId} on this account. Run \`kryd project list\` to see your projects.
16594
+ `
16595
+ );
16596
+ process.exitCode = 1;
16597
+ return;
16598
+ }
16599
+ if (project2.status === "deleting") {
16600
+ process.stdout.write(`${project2.name} is already being deleted \u2014 following it.
16601
+ `);
16602
+ return followProjectDeletion(apiUrl, token, projectId, project2.name, opts);
16603
+ }
16604
+ resources = project2.status === "delete_failed" ? ["whatever the previous attempt did not manage to remove"] : await describeProjectResources(apiUrl, token, projectId);
16605
+ } catch (err) {
16606
+ reportError(err);
16607
+ return;
16608
+ }
16609
+ if (!opts.yes) {
16610
+ const io = opts.io ?? defaultPromptIO();
16611
+ if (!isInteractive(io)) {
16612
+ process.stderr.write(
16613
+ `Refusing to delete ${project2.name} without confirmation. Re-run with --yes to confirm non-interactively.
16614
+ `
16615
+ );
16616
+ process.exitCode = 1;
16617
+ return;
16618
+ }
16619
+ io.output.write(
16620
+ `This permanently deletes ${project2.name} and:
16621
+ ` + resources.map((r) => ` \u2022 ${r}
16622
+ `).join("") + "This cannot be undone.\n"
16623
+ );
16624
+ const typed = await promptLine(
16625
+ `Type ${project2.name} to confirm: `,
16626
+ io
16627
+ );
16628
+ if (typed !== project2.name) {
16629
+ process.stdout.write(`Left ${project2.name} in place.
16630
+ `);
16631
+ return;
16632
+ }
16633
+ }
16634
+ try {
16635
+ const accepted = await deleteProject(apiUrl, token, projectId);
16636
+ process.stdout.write(
16637
+ accepted.alreadyInProgress ? `${project2.name} was already being deleted \u2014 following it.
16638
+ ` : `Deleting ${project2.name}\u2026
16639
+ `
16640
+ );
16641
+ await followProjectDeletion(apiUrl, token, projectId, project2.name, opts);
16642
+ } catch (err) {
16643
+ reportError(err);
16644
+ }
16645
+ }
16646
+ async function followProjectDeletion(apiUrl, token, projectId, name, opts) {
16647
+ const result = await pollProjectDeleted(
16648
+ () => getProject(apiUrl, token, projectId),
16649
+ opts.pollOpts
16650
+ );
16651
+ if (result.gone) {
16652
+ const cleared = clearProjectLinkIfMatches(projectId, opts.cwd);
16653
+ process.stdout.write(
16654
+ `Deleted ${name}.
16655
+ ` + (cleared ? "Removed the local .kryd/project.json link.\n" : "")
16656
+ );
16657
+ return;
16658
+ }
16659
+ if (result.status === "delete_failed") {
16660
+ process.stderr.write(
16661
+ `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.
16662
+ Re-run \`kryd project rm ${projectId}\` to resume from where it stopped.
16663
+ `
16664
+ );
16665
+ process.exitCode = 1;
16666
+ return;
16667
+ }
16668
+ process.stderr.write(
16669
+ `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.
16670
+ `
16671
+ );
16672
+ process.exitCode = 1;
16673
+ }
16674
+ async function describeProjectResources(apiUrl, token, projectId) {
16675
+ const out = ["its deployed app, its containers and its built images"];
16676
+ const db2 = await getDatabaseStatus(apiUrl, token, projectId).catch(() => null);
16677
+ if (db2) out.push("its Postgres database \u2014 and all the data in it");
16678
+ const storage2 = await getStorageStatus(apiUrl, token, projectId).catch(() => null);
16679
+ if (storage2) out.push("its object-storage bucket \u2014 and every object in it");
16680
+ out.push("its Git repository on the forge, and its push token");
16681
+ return out;
16682
+ }
16394
16683
  async function runDbDetach(opts) {
16395
16684
  const apiUrl = resolveApiUrl(opts.apiUrl);
16396
16685
  const token = loadConfig().token;
@@ -16399,23 +16688,23 @@ async function runDbDetach(opts) {
16399
16688
  process.exitCode = 1;
16400
16689
  return;
16401
16690
  }
16402
- const project = resolveProjectId(opts.project, opts.cwd);
16403
- if (!project) {
16691
+ const project2 = resolveProjectId(opts.project, opts.cwd);
16692
+ if (!project2) {
16404
16693
  reportNotLinked("kryd db detach <projectId>");
16405
16694
  return;
16406
16695
  }
16407
16696
  try {
16408
- await detachDatabase(apiUrl, token, project);
16409
- process.stdout.write(`Detaching the database from ${project}\u2026
16697
+ await detachDatabase(apiUrl, token, project2);
16698
+ process.stdout.write(`Detaching the database from ${project2}\u2026
16410
16699
  `);
16411
16700
  const result = await pollResourceDetached(
16412
- () => getDatabaseStatus(apiUrl, token, project)
16701
+ () => getDatabaseStatus(apiUrl, token, project2)
16413
16702
  );
16414
16703
  reportDetachResult(result, {
16415
- goneMsg: `Detached the database from ${project}.
16704
+ goneMsg: `Detached the database from ${project2}.
16416
16705
  `,
16417
16706
  failedLabel: "Database detach",
16418
- retryCmd: `kryd db detach ${project}`
16707
+ retryCmd: `kryd db detach ${project2}`
16419
16708
  });
16420
16709
  } catch (err) {
16421
16710
  reportError(err);
@@ -16429,15 +16718,15 @@ async function runAiEnable(opts) {
16429
16718
  process.exitCode = 1;
16430
16719
  return;
16431
16720
  }
16432
- const project = resolveProjectId(opts.project, opts.cwd);
16433
- if (!project) {
16721
+ const project2 = resolveProjectId(opts.project, opts.cwd);
16722
+ if (!project2) {
16434
16723
  reportNotLinked("kryd ai enable <projectId>");
16435
16724
  return;
16436
16725
  }
16437
16726
  try {
16438
- const status = await enableAi(apiUrl, token, { projectId: project });
16727
+ const status = await enableAi(apiUrl, token, { projectId: project2 });
16439
16728
  process.stdout.write(
16440
- `AI enabled for ${project}.
16729
+ `AI enabled for ${project2}.
16441
16730
  ` + (status.url ? `Endpoint: ${status.url} (OpenAI-compatible base URL \u2014 use it as-is)
16442
16731
  ` : "") + `AI_GATEWAY_URL + AI_GATEWAY_TOKEN will be injected on your next deploy (kryd deploy).
16443
16732
  `
@@ -16454,23 +16743,23 @@ async function runStorageDetach(opts) {
16454
16743
  process.exitCode = 1;
16455
16744
  return;
16456
16745
  }
16457
- const project = resolveProjectId(opts.project, opts.cwd);
16458
- if (!project) {
16746
+ const project2 = resolveProjectId(opts.project, opts.cwd);
16747
+ if (!project2) {
16459
16748
  reportNotLinked("kryd storage detach <projectId>");
16460
16749
  return;
16461
16750
  }
16462
16751
  try {
16463
- await detachStorage(apiUrl, token, project);
16464
- process.stdout.write(`Detaching object storage from ${project}\u2026
16752
+ await detachStorage(apiUrl, token, project2);
16753
+ process.stdout.write(`Detaching object storage from ${project2}\u2026
16465
16754
  `);
16466
16755
  const result = await pollResourceDetached(
16467
- () => getStorageStatus(apiUrl, token, project)
16756
+ () => getStorageStatus(apiUrl, token, project2)
16468
16757
  );
16469
16758
  reportDetachResult(result, {
16470
- goneMsg: `Detached object storage from ${project}.
16759
+ goneMsg: `Detached object storage from ${project2}.
16471
16760
  `,
16472
16761
  failedLabel: "Storage detach",
16473
- retryCmd: `kryd storage detach ${project}`
16762
+ retryCmd: `kryd storage detach ${project2}`
16474
16763
  });
16475
16764
  } catch (err) {
16476
16765
  reportError(err);
@@ -16500,6 +16789,40 @@ function envAnnotation(environments) {
16500
16789
  if (preview && !prod) return " \u2192 preview only";
16501
16790
  return "";
16502
16791
  }
16792
+ async function runProjectList(opts) {
16793
+ const apiUrl = resolveApiUrl(opts.apiUrl);
16794
+ const token = loadConfig().token;
16795
+ if (!token) {
16796
+ process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
16797
+ process.exitCode = 1;
16798
+ return;
16799
+ }
16800
+ let projects;
16801
+ try {
16802
+ projects = await listProjects(apiUrl, token);
16803
+ } catch (err) {
16804
+ reportError(err);
16805
+ return;
16806
+ }
16807
+ if (projects.length === 0) {
16808
+ process.stdout.write("No projects yet \u2014 run `kryd init` in a project directory.\n");
16809
+ return;
16810
+ }
16811
+ const linkedId = resolveProjectId(void 0, opts.cwd);
16812
+ const nameWidth = projects.reduce((max, p) => Math.max(max, p.name.length), 0);
16813
+ const subWidth = projects.reduce((max, p) => Math.max(max, p.subdomain.length), 0);
16814
+ const statusWidth = projects.reduce((max, p) => Math.max(max, p.status.length), 0);
16815
+ for (const p of projects) {
16816
+ const mark = p.id === linkedId ? "*" : " ";
16817
+ process.stdout.write(
16818
+ `${mark} ${p.name.padEnd(nameWidth)} ${p.subdomain.padEnd(subWidth)} ${p.status.padEnd(statusWidth)} ${p.id}
16819
+ `
16820
+ );
16821
+ }
16822
+ if (projects.some((p) => p.id === linkedId)) {
16823
+ process.stdout.write("\n* linked to this directory\n");
16824
+ }
16825
+ }
16503
16826
  function renderEnvGroup(heading, vars, opts) {
16504
16827
  const width = vars.reduce((max, v) => Math.max(max, v.key.length), 0);
16505
16828
  const dates = opts.showUpdated ? vars.map((v) => isoDate(v.updatedAt)) : [];
@@ -16523,16 +16846,16 @@ async function runEnvList(opts) {
16523
16846
  process.exitCode = 1;
16524
16847
  return;
16525
16848
  }
16526
- const project = resolveProjectId(opts.project, opts.cwd);
16527
- if (!project) {
16849
+ const project2 = resolveProjectId(opts.project, opts.cwd);
16850
+ if (!project2) {
16528
16851
  reportNotLinked("kryd env list <projectId>");
16529
16852
  return;
16530
16853
  }
16531
16854
  try {
16532
- const vars = await listEnvVars(apiUrl, token, project);
16855
+ const vars = await listEnvVars(apiUrl, token, project2);
16533
16856
  if (vars.length === 0) {
16534
16857
  process.stdout.write(
16535
- `No environment variables for ${project} yet.
16858
+ `No environment variables for ${project2} yet.
16536
16859
  Set one with \`kryd env set KEY --stdin\`, or attach a database, storage or the AI gateway.
16537
16860
  `
16538
16861
  );
@@ -16605,8 +16928,8 @@ async function runEnvPull(opts) {
16605
16928
  process.exitCode = 1;
16606
16929
  return;
16607
16930
  }
16608
- const project = resolveProjectId(opts.project, opts.cwd);
16609
- if (!project) {
16931
+ const project2 = resolveProjectId(opts.project, opts.cwd);
16932
+ if (!project2) {
16610
16933
  reportNotLinked("kryd env pull <projectId>");
16611
16934
  return;
16612
16935
  }
@@ -16624,11 +16947,11 @@ async function runEnvPull(opts) {
16624
16947
  return;
16625
16948
  }
16626
16949
  try {
16627
- const { values } = await pullEnvValues(apiUrl, token, project, environment);
16950
+ const { values } = await pullEnvValues(apiUrl, token, project2, environment);
16628
16951
  const entries = Object.entries(values).sort(
16629
16952
  ([a], [b]) => a.localeCompare(b)
16630
16953
  );
16631
- const header = `# Written by \`kryd env pull\` \u2014 your customer-set variables for ${project} (${environment}).
16954
+ const header = `# Written by \`kryd env pull\` \u2014 your customer-set variables for ${project2} (${environment}).
16632
16955
  # Regenerate with \`kryd env pull\`. Do not commit this file.
16633
16956
  `;
16634
16957
  const body = entries.map(([k, v]) => toDotenvLine(k, v)).join("\n");
@@ -16640,7 +16963,7 @@ ${body}
16640
16963
  ` : header);
16641
16964
  const count = entries.length;
16642
16965
  process.stdout.write(
16643
- count === 0 ? `No customer environment variables for ${project} (${environment}) \u2014 wrote an empty ${outRel}.
16966
+ count === 0 ? `No customer environment variables for ${project2} (${environment}) \u2014 wrote an empty ${outRel}.
16644
16967
  ` : `Wrote ${count} variable${count === 1 ? "" : "s"} to ${outRel} (${environment}), mode 0600.
16645
16968
  `
16646
16969
  );
@@ -16706,8 +17029,8 @@ async function runEnvSet(opts) {
16706
17029
  process.exitCode = 1;
16707
17030
  return;
16708
17031
  }
16709
- const project = resolveProjectId(opts.project, opts.cwd);
16710
- if (!project) {
17032
+ const project2 = resolveProjectId(opts.project, opts.cwd);
17033
+ if (!project2) {
16711
17034
  reportNotLinked("kryd env set KEY=VALUE <projectId>");
16712
17035
  return;
16713
17036
  }
@@ -16773,7 +17096,7 @@ async function runEnvSet(opts) {
16773
17096
  const written = await setEnvVar(
16774
17097
  apiUrl,
16775
17098
  token,
16776
- project,
17099
+ project2,
16777
17100
  key,
16778
17101
  resolved.value,
16779
17102
  scope,
@@ -16781,7 +17104,7 @@ async function runEnvSet(opts) {
16781
17104
  );
16782
17105
  const envTag = environment === "preview" ? " (preview)" : "";
16783
17106
  process.stdout.write(
16784
- `${written.created ? "Set" : "Updated"} ${written.key}${scope === "build" ? " (build-time)" : envTag} for ${project}.
17107
+ `${written.created ? "Set" : "Updated"} ${written.key}${scope === "build" ? " (build-time)" : envTag} for ${project2}.
16785
17108
  ` + // The public-bundle warning comes FIRST, before the applies-when note. Someone who reads one
16786
17109
  // line of output should read the consequential one.
16787
17110
  (scope === "build" ? `${BUILD_ENV_PUBLIC_WARNING}
@@ -16801,8 +17124,8 @@ async function runEnvRemove(opts) {
16801
17124
  process.exitCode = 1;
16802
17125
  return;
16803
17126
  }
16804
- const project = resolveProjectId(opts.project, opts.cwd);
16805
- if (!project) {
17127
+ const project2 = resolveProjectId(opts.project, opts.cwd);
17128
+ if (!project2) {
16806
17129
  reportNotLinked("kryd env rm KEY <projectId>");
16807
17130
  return;
16808
17131
  }
@@ -16837,10 +17160,10 @@ If it comes from a resource you attached, detach that instead (\`kryd db detach\
16837
17160
  }
16838
17161
  if (scope === "build") {
16839
17162
  try {
16840
- const vars = await listEnvVars(apiUrl, token, project, "build");
17163
+ const vars = await listEnvVars(apiUrl, token, project2, "build");
16841
17164
  if (!vars.some((v) => v.key === key && v.scope === "build")) {
16842
17165
  process.stderr.write(
16843
- `${key} is not set as a build-time variable on ${project}.
17166
+ `${key} is not set as a build-time variable on ${project2}.
16844
17167
  Run \`kryd env list\` to see what is set. (If you meant the runtime variable of that name, drop --build.)
16845
17168
  `
16846
17169
  );
@@ -16863,7 +17186,7 @@ Run \`kryd env list\` to see what is set. (If you meant the runtime variable of
16863
17186
  return;
16864
17187
  }
16865
17188
  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.`,
17189
+ 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
17190
  io
16868
17191
  );
16869
17192
  if (!confirmed) {
@@ -16873,10 +17196,10 @@ Run \`kryd env list\` to see what is set. (If you meant the runtime variable of
16873
17196
  }
16874
17197
  }
16875
17198
  try {
16876
- const removed = await removeEnvVar(apiUrl, token, project, key, scope, environment);
17199
+ const removed = await removeEnvVar(apiUrl, token, project2, key, scope, environment);
16877
17200
  const envTag = environment === "preview" ? " (preview override)" : "";
16878
17201
  process.stdout.write(
16879
- `Removed ${removed.key}${scope === "build" ? " (build-time)" : envTag} from ${project}.
17202
+ `Removed ${removed.key}${scope === "build" ? " (build-time)" : envTag} from ${project2}.
16880
17203
  ` + // The build-scope warning is deliberately bleaker than the runtime one, because the truth is.
16881
17204
  // A runtime value is gone from the container at the next deploy. A build-time value is
16882
17205
  // COMPILED INTO the live image and into every bundle already downloaded from it — removing
@@ -16889,7 +17212,7 @@ Run \`kryd env list\` to see what is set. (If you meant the runtime variable of
16889
17212
  reportError(err);
16890
17213
  }
16891
17214
  }
16892
- var CLI_VERSION = true ? "0.2.1" : "0.0.0-dev";
17215
+ var CLI_VERSION = true ? "0.4.0" : "0.0.0-dev";
16893
17216
  var program = new Command();
16894
17217
  program.name("kryd").description("Kryd CLI").version(CLI_VERSION);
16895
17218
  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 +17226,14 @@ program.command("init").description("Link this project's repo + register its pus
16903
17226
  "react-router | nextjs | vite-spa | node (auto-detected if omitted)"
16904
17227
  ).option(
16905
17228
  "--tenant <slug>",
16906
- "your tenant slug (claimed once on first init \u2014 appears in every deploy URL)"
17229
+ // KRYD-265 / KRYD-188: this used to say "claimed once on first init", which was false — signup
17230
+ // has always assigned a slug, so the flag could only agree with it or 409. It now changes the
17231
+ // slug, and only while nothing derives from it.
17232
+ "your tenant slug \u2014 appears in every deploy URL; changeable until your first project"
17233
+ ).option(
17234
+ "--public",
17235
+ // KRYD-184a. Chosen at creation; there is no command to flip it afterwards yet (KRYD-341).
17236
+ "create the repository PUBLIC \u2014 readable by anyone signed in to the forge (default: private). Set at creation only"
16907
17237
  ).option("--api-url <url>", "control-plane API base URL").action((opts) => runInit(opts));
16908
17238
  program.command("logs [target]").description(
16909
17239
  "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 +17251,29 @@ program.command("push [branch]").description(
16921
17251
  ).option("--api-url <url>", "control-plane API base URL").action(
16922
17252
  (branch, opts, cmd) => runPush({ ...opts, branch, gitArgs: cmd.args.slice(1) })
16923
17253
  );
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 }));
17254
+ 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
17255
  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 })
17256
+ (project2, deployment, opts) => runRollback({ ...opts, project: project2, deployment })
16927
17257
  );
16928
17258
  program.command("redeploy [project]").description(
16929
17259
  "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 }));
17260
+ ).option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runRedeploy({ ...opts, project: project2 }));
17261
+ var project = program.command("project").description("Manage your projects");
17262
+ project.command("list").description("List your projects \u2014 name, subdomain, status and id").option("--api-url <url>", "control-plane API base URL").action((opts) => runProjectList(opts));
17263
+ project.command("rm [project]").description(
17264
+ "Permanently delete a project and everything Kryd created for it (by name or id; asks you to type its name)"
17265
+ ).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
17266
  var db = program.command("db").description("Manage project databases");
16932
17267
  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
17268
  "--connection-string <url>",
16934
17269
  "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 }));
17270
+ ).option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runDbCreate({ ...opts, project: project2 }));
17271
+ 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
17272
  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 }));
17273
+ 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 }));
17274
+ 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
17275
  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 }));
17276
+ 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
17277
  env.command("set <key> [project]").description(
16943
17278
  "Set one of your environment variables \u2014 `KEY=VALUE`, or `KEY` with --stdin / a prompt"
16944
17279
  ).option(
@@ -16950,14 +17285,14 @@ env.command("set <key> [project]").description(
16950
17285
  ).option(
16951
17286
  "--preview",
16952
17287
  "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 }));
17288
+ ).option("--api-url <url>", "control-plane API base URL").action((key, project2, opts) => runEnvSet({ ...opts, spec: key, project: project2 }));
16954
17289
  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
17290
  "--build",
16956
17291
  "remove the build-time variable of this name rather than the runtime one (the same name can exist as both)"
16957
17292
  ).option(
16958
17293
  "--preview",
16959
17294
  "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 }));
17295
+ ).option("--api-url <url>", "control-plane API base URL").action((key, project2, opts) => runEnvRemove({ ...opts, key, project: project2 }));
16961
17296
  env.command("pull [project]").description(
16962
17297
  "Write your customer-set environment variables (values included) to a local .env file for development"
16963
17298
  ).option(
@@ -16966,9 +17301,9 @@ env.command("pull [project]").description(
16966
17301
  ).option(
16967
17302
  "--out <path>",
16968
17303
  "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 }));
17304
+ ).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
17305
  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 }));
17306
+ 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
17307
  if (import.meta.url === `file://${process.argv[1]}`) {
16973
17308
  program.parseAsync().catch((err) => {
16974
17309
  process.stderr.write(
@@ -16992,6 +17327,8 @@ export {
16992
17327
  runLogin,
16993
17328
  runLogout,
16994
17329
  runLogs,
17330
+ runProjectList,
17331
+ runProjectRemove,
16995
17332
  runPush,
16996
17333
  runRedeploy,
16997
17334
  runRollback,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kryd/cli",
3
- "version": "0.2.1",
3
+ "version": "0.4.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",
@@ -50,8 +50,8 @@
50
50
  "typescript": "^5.6.3",
51
51
  "vitest": "^2.1.8",
52
52
  "@kryd/shared-types": "0.0.0",
53
- "@kryd/config-eslint": "0.0.0",
54
- "@kryd/config-ts": "0.0.0"
53
+ "@kryd/config-ts": "0.0.0",
54
+ "@kryd/config-eslint": "0.0.0"
55
55
  },
56
56
  "scripts": {
57
57
  "build": "esbuild src/index.ts --bundle --platform=node --format=esm --external:commander --define:__KRYD_VERSION__=\\\"$npm_package_version\\\" --outfile=dist/index.js",