@kryd/cli 0.15.0 → 0.17.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 (3) hide show
  1. package/README.md +7 -5
  2. package/dist/index.js +748 -265
  3. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -8,6 +8,26 @@ var __export = (target, all) => {
8
8
  // src/index.ts
9
9
  import { Command } from "commander";
10
10
 
11
+ // ../../packages/shared-types/dist/api.js
12
+ var ERROR_CODES = {
13
+ validationFailed: "validation.failed",
14
+ unauthorized: "auth.unauthorized",
15
+ forbidden: "auth.forbidden",
16
+ notFound: "resource.not_found",
17
+ conflict: "resource.conflict",
18
+ unprocessable: "request.unprocessable",
19
+ badRequest: "request.bad",
20
+ internal: "internal.error",
21
+ // KRYD-516b: a request that names no account while the caller is in several (400), and a member
22
+ // whose role lacks the route's permission (403, `details.permission` names it).
23
+ accountRequired: "account.required",
24
+ permissionRequired: "auth.permission_required",
25
+ // KRYD-592: the analytics store (Umami's database) is unconfigured, down or too slow — a 503 the
26
+ // Analytics tab shows as its own error band while the rest of the dashboard carries on. (The spec
27
+ // wrote `analytics_unavailable`; dotted like every code here, which `api.test.ts` enforces.)
28
+ analyticsUnavailable: "analytics.unavailable"
29
+ };
30
+
11
31
  // ../../packages/shared-types/dist/db-url.js
12
32
  function isValidPostgresConnectionString(value) {
13
33
  if (value.length === 0 || /\s/.test(value))
@@ -14721,6 +14741,12 @@ var accountUsageSchema = external_exports.object({
14721
14741
  storageBytes: external_exports.number().int().nonnegative()
14722
14742
  });
14723
14743
 
14744
+ // ../../packages/shared-types/dist/resource.js
14745
+ var BUCKET_UPLOAD_MAX_BYTES = 50 * 1024 * 1024;
14746
+ var BUCKET_UPLOAD_MAX_SIZE_LABEL = `${BUCKET_UPLOAD_MAX_BYTES / (1024 * 1024)} MB`;
14747
+ var BUCKET_UPLOAD_MAX_TOTAL_BYTES = 100 * 1024 * 1024;
14748
+ var BUCKET_UPLOAD_TOTAL_REFUSAL = `Je kunt maximaal ${BUCKET_UPLOAD_MAX_TOTAL_BYTES / (1024 * 1024)} MB per upload versturen.`;
14749
+
14724
14750
  // ../../packages/shared-types/dist/env-vars.js
14725
14751
  var MANAGED_ENV_VAR_NAMES = [
14726
14752
  "AI_GATEWAY_TOKEN",
@@ -14875,7 +14901,44 @@ function parseTicketNumber(raw) {
14875
14901
  var TICKET_ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024;
14876
14902
  var TICKET_ATTACHMENT_MAX_SIZE_LABEL = `${TICKET_ATTACHMENT_MAX_BYTES / (1024 * 1024)} MB`;
14877
14903
 
14904
+ // ../../packages/shared-types/dist/permissions.js
14905
+ var PERMISSIONS = [
14906
+ /** Open the site of a private preview or a closed production (enforced from KRYD-575). */
14907
+ "app:view",
14908
+ /** See the account itself: name, slug. */
14909
+ "account:view",
14910
+ /** Rename the account and change its slug. */
14911
+ "account:manage",
14912
+ /** See who is a member, and the invites still open. */
14913
+ "members:view",
14914
+ /** Invite people, change a member's role, remove a member. */
14915
+ "members:manage",
14916
+ /** See usage and costs. */
14917
+ "usage:view",
14918
+ /** See projects, deploys, domains, databases, storage, AI and workflows. */
14919
+ "projects:view",
14920
+ /** Change a project: env vars, domains, databases, storage, AI, workflows, remove it. */
14921
+ "projects:manage",
14922
+ /** Read env var VALUES (secrets), not only their names. */
14923
+ "env:read",
14924
+ /** Link a repository, deploy, roll back, get a push credential, manage deploy hooks. */
14925
+ "deploy",
14926
+ /** Read build and runtime logs. */
14927
+ "logs:view",
14928
+ /** Trigger, cancel and replay workflow runs; add and remove crons and schedules. */
14929
+ "workflows:run",
14930
+ /** Open and answer support tickets. */
14931
+ "support"
14932
+ ];
14933
+ var ROLES = {
14934
+ owner: PERMISSIONS,
14935
+ member: PERMISSIONS.filter((p) => p !== "members:manage" && p !== "account:manage"),
14936
+ viewer: ["app:view"]
14937
+ };
14938
+ var ACCOUNT_HEADER = "Kryd-Account";
14939
+
14878
14940
  // ../../packages/shared-types/dist/workflow-run.js
14941
+ var WORKFLOW_RUN_PAYLOAD_MAX_BYTES = 64 * 1024;
14879
14942
  var MAX_WEBHOOK_NAME = 32;
14880
14943
  var WEBHOOK_NAME_RE = new RegExp(`^[a-z0-9](?:[a-z0-9-]{0,${MAX_WEBHOOK_NAME - 2}}[a-z0-9])?$`);
14881
14944
 
@@ -14936,6 +14999,209 @@ import {
14936
14999
  import { basename as basename2, dirname as dirname2, isAbsolute, join as join3, relative, resolve as resolve2 } from "node:path";
14937
15000
  import { pathToFileURL } from "node:url";
14938
15001
 
15002
+ // src/config.ts
15003
+ import {
15004
+ appendFileSync,
15005
+ chmodSync,
15006
+ existsSync,
15007
+ mkdirSync,
15008
+ readFileSync,
15009
+ rmSync,
15010
+ writeFileSync
15011
+ } from "node:fs";
15012
+ import { homedir } from "node:os";
15013
+ import { dirname, join } from "node:path";
15014
+ var DEFAULT_API_URL = "http://localhost:3000";
15015
+ function configPath() {
15016
+ const dir = process.env.KRYD_CONFIG_DIR ?? join(homedir(), ".kryd");
15017
+ return join(dir, "config.json");
15018
+ }
15019
+ function loadConfig() {
15020
+ let raw;
15021
+ try {
15022
+ raw = readFileSync(configPath(), "utf8");
15023
+ } catch {
15024
+ return {};
15025
+ }
15026
+ try {
15027
+ return JSON.parse(raw);
15028
+ } catch {
15029
+ process.stderr.write(
15030
+ `warning: ${configPath()} is not valid JSON \u2014 ignoring it.
15031
+ `
15032
+ );
15033
+ return {};
15034
+ }
15035
+ }
15036
+ function resolveToken() {
15037
+ const fromEnv = process.env.KRYD_TOKEN?.trim();
15038
+ return fromEnv ? fromEnv : loadConfig().token;
15039
+ }
15040
+ function writeConfig(config2) {
15041
+ const path = configPath();
15042
+ mkdirSync(dirname(path), { recursive: true, mode: 448 });
15043
+ writeFileSync(path, JSON.stringify(config2, null, 2) + "\n", { mode: 384 });
15044
+ chmodSync(path, 384);
15045
+ }
15046
+ function saveToken(token, apiUrl) {
15047
+ const config2 = loadConfig();
15048
+ config2.token = token;
15049
+ if (apiUrl) config2.apiUrl = apiUrl;
15050
+ writeConfig(config2);
15051
+ }
15052
+ function clearToken() {
15053
+ const config2 = loadConfig();
15054
+ delete config2.token;
15055
+ delete config2.account;
15056
+ delete config2.accountSlug;
15057
+ writeConfig(config2);
15058
+ }
15059
+ function saveAccount(account) {
15060
+ const config2 = loadConfig();
15061
+ config2.account = account.accountId;
15062
+ if (account.slug) config2.accountSlug = account.slug;
15063
+ else delete config2.accountSlug;
15064
+ writeConfig(config2);
15065
+ }
15066
+ function clearAccount() {
15067
+ const config2 = loadConfig();
15068
+ delete config2.account;
15069
+ delete config2.accountSlug;
15070
+ writeConfig(config2);
15071
+ }
15072
+ var DEPLOY_TOKEN_PREFIX = "kryd_";
15073
+ function accountHeader(token) {
15074
+ if (!token || token.startsWith(DEPLOY_TOKEN_PREFIX)) return {};
15075
+ const account = loadConfig().account;
15076
+ return account ? { [ACCOUNT_HEADER]: account } : {};
15077
+ }
15078
+ function authHeaders(token) {
15079
+ return { authorization: `Bearer ${token}`, ...accountHeader(token) };
15080
+ }
15081
+ var lastApiUrl = null;
15082
+ function resolveApiUrl(flag) {
15083
+ lastApiUrl = flag ?? process.env.KRYD_API_URL ?? loadConfig().apiUrl ?? DEFAULT_API_URL;
15084
+ return lastApiUrl;
15085
+ }
15086
+ function currentApiUrl() {
15087
+ return lastApiUrl ?? resolveApiUrl();
15088
+ }
15089
+ var DEFAULT_DASHBOARD_URL = "https://app.kryd.eu";
15090
+ function resolveDashboardUrl(flag) {
15091
+ return flag ?? process.env.KRYD_DASHBOARD_URL ?? DEFAULT_DASHBOARD_URL;
15092
+ }
15093
+ var PROJECT_LINK_DIR = ".kryd";
15094
+ var PROJECT_LINK_FILE = "project.json";
15095
+ function findProjectLinkDir(cwd = process.cwd()) {
15096
+ let dir = cwd;
15097
+ for (; ; ) {
15098
+ if (existsSync(join(dir, PROJECT_LINK_DIR, PROJECT_LINK_FILE))) return dir;
15099
+ if (existsSync(join(dir, ".git"))) return null;
15100
+ const parent = dirname(dir);
15101
+ if (parent === dir) return null;
15102
+ dir = parent;
15103
+ }
15104
+ }
15105
+ function loadProjectLink(cwd = process.cwd()) {
15106
+ const root = findProjectLinkDir(cwd);
15107
+ if (!root) return null;
15108
+ const path = join(root, PROJECT_LINK_DIR, PROJECT_LINK_FILE);
15109
+ let raw;
15110
+ try {
15111
+ raw = readFileSync(path, "utf8");
15112
+ } catch {
15113
+ return null;
15114
+ }
15115
+ let parsed;
15116
+ try {
15117
+ parsed = JSON.parse(raw);
15118
+ } catch {
15119
+ process.stderr.write(`warning: ${path} is not valid JSON \u2014 ignoring it.
15120
+ `);
15121
+ return null;
15122
+ }
15123
+ if (!parsed || typeof parsed.projectId !== "string" || parsed.projectId.length === 0) {
15124
+ process.stderr.write(`warning: ${path} has no projectId \u2014 ignoring it.
15125
+ `);
15126
+ return null;
15127
+ }
15128
+ return parsed;
15129
+ }
15130
+ function saveProjectLink(cwd, link) {
15131
+ const dir = join(cwd, PROJECT_LINK_DIR);
15132
+ mkdirSync(dir, { recursive: true });
15133
+ const path = join(dir, PROJECT_LINK_FILE);
15134
+ writeFileSync(path, JSON.stringify(link, null, 2) + "\n");
15135
+ return path;
15136
+ }
15137
+ function ensureIgnored(cwd, entry, aliases = []) {
15138
+ const path = join(cwd, ".gitignore");
15139
+ let current = "";
15140
+ try {
15141
+ current = readFileSync(path, "utf8");
15142
+ } catch {
15143
+ }
15144
+ const present = current.split("\n").map((l) => l.trim()).some((l) => l === entry || aliases.includes(l));
15145
+ if (present) return false;
15146
+ const prefix = current.length > 0 && !current.endsWith("\n") ? "\n" : "";
15147
+ appendFileSync(path, `${prefix}${entry}
15148
+ `);
15149
+ return true;
15150
+ }
15151
+ function ensureKrydIgnored(cwd) {
15152
+ ensureIgnored(cwd, `${PROJECT_LINK_DIR}/`, [PROJECT_LINK_DIR]);
15153
+ }
15154
+ function directoryIsLinked(cwd) {
15155
+ const path = join(cwd, PROJECT_LINK_DIR, PROJECT_LINK_FILE);
15156
+ if (!existsSync(path)) return null;
15157
+ return loadProjectLink(cwd);
15158
+ }
15159
+ function writeSecretFile(path, contents) {
15160
+ mkdirSync(dirname(path), { recursive: true });
15161
+ writeFileSync(path, contents, { mode: 384 });
15162
+ chmodSync(path, 384);
15163
+ }
15164
+ function clearProjectLinkIfMatches(projectId, cwd = process.cwd()) {
15165
+ const root = findProjectLinkDir(cwd);
15166
+ if (!root) return false;
15167
+ const link = loadProjectLink(cwd);
15168
+ if (link?.projectId !== projectId) return false;
15169
+ try {
15170
+ rmSync(join(root, PROJECT_LINK_DIR, PROJECT_LINK_FILE));
15171
+ return true;
15172
+ } catch {
15173
+ return false;
15174
+ }
15175
+ }
15176
+ function resolvedLink() {
15177
+ return lastLinkResolution;
15178
+ }
15179
+ var lastLinkResolution = null;
15180
+ function resolveProjectId(explicit, cwd = process.cwd()) {
15181
+ lastLinkResolution = null;
15182
+ if (explicit) return explicit;
15183
+ const root = findProjectLinkDir(cwd);
15184
+ if (!root) return null;
15185
+ const link = loadProjectLink(cwd);
15186
+ if (!link) return null;
15187
+ lastLinkResolution = { root, link };
15188
+ return link.projectId;
15189
+ }
15190
+ function rememberLinkAccount(root, account) {
15191
+ if (!account.accountId) return false;
15192
+ const link = directoryIsLinked(root);
15193
+ if (!link) return false;
15194
+ const slug = account.accountSlug ?? link.accountSlug;
15195
+ if (link.accountId === account.accountId && link.accountSlug === slug) return false;
15196
+ saveProjectLink(root, {
15197
+ ...link,
15198
+ accountId: account.accountId,
15199
+ // `exactOptionalPropertyTypes`: an absent key and an explicit `undefined` are different types.
15200
+ ...slug ? { accountSlug: slug } : {}
15201
+ });
15202
+ return true;
15203
+ }
15204
+
14939
15205
  // src/client.ts
14940
15206
  var ApiError = class extends Error {
14941
15207
  envelope;
@@ -14965,7 +15231,7 @@ async function parseEnvelope(res) {
14965
15231
  }
14966
15232
  async function whoami(apiUrl, token) {
14967
15233
  const res = await fetch(`${apiUrl}/me`, {
14968
- headers: { authorization: `Bearer ${token}` }
15234
+ headers: { ...authHeaders(token) }
14969
15235
  });
14970
15236
  if (!res.ok) {
14971
15237
  throw new ApiError(
@@ -14981,7 +15247,7 @@ async function linkProject(apiUrl, token, input) {
14981
15247
  method: "POST",
14982
15248
  headers: {
14983
15249
  "content-type": "application/json",
14984
- authorization: `Bearer ${token}`
15250
+ ...authHeaders(token)
14985
15251
  },
14986
15252
  body: JSON.stringify(input)
14987
15253
  });
@@ -15000,18 +15266,23 @@ async function linkProject(apiUrl, token, input) {
15000
15266
  }
15001
15267
  return parsed;
15002
15268
  }
15269
+ function environmentQuery(environment) {
15270
+ return environment === "preview" ? "?environment=preview" : "";
15271
+ }
15003
15272
  async function attachDatabase(apiUrl, token, input) {
15004
15273
  const res = await fetch(`${apiUrl}/databases`, {
15005
15274
  method: "POST",
15006
15275
  headers: {
15007
15276
  "content-type": "application/json",
15008
- authorization: `Bearer ${token}`
15277
+ ...authHeaders(token)
15009
15278
  },
15010
15279
  body: JSON.stringify({
15011
15280
  projectId: input.projectId,
15012
15281
  mode: input.mode,
15013
15282
  // Conditional (exactOptionalPropertyTypes) — only BYO carries a connection string.
15014
- ...input.connectionString ? { connectionString: input.connectionString } : {}
15283
+ ...input.connectionString ? { connectionString: input.connectionString } : {},
15284
+ // Only sent for preview, so a production attach is byte-identical to what older CLIs send.
15285
+ ...input.environment === "preview" ? { environment: "preview" } : {}
15015
15286
  })
15016
15287
  });
15017
15288
  if (!res.ok) {
@@ -15028,7 +15299,8 @@ async function attachDatabase(apiUrl, token, input) {
15028
15299
  return {
15029
15300
  id: parsed.id,
15030
15301
  mode: parsed.mode,
15031
- status: parsed.status
15302
+ status: parsed.status,
15303
+ ...parsed.environment === "production" || parsed.environment === "preview" ? { environment: parsed.environment } : {}
15032
15304
  };
15033
15305
  }
15034
15306
  async function createStorage(apiUrl, token, input) {
@@ -15036,7 +15308,7 @@ async function createStorage(apiUrl, token, input) {
15036
15308
  method: "POST",
15037
15309
  headers: {
15038
15310
  "content-type": "application/json",
15039
- authorization: `Bearer ${token}`
15311
+ ...authHeaders(token)
15040
15312
  },
15041
15313
  body: JSON.stringify({ projectId: input.projectId })
15042
15314
  });
@@ -15058,7 +15330,7 @@ async function enableAi(apiUrl, token, input) {
15058
15330
  method: "POST",
15059
15331
  headers: {
15060
15332
  "content-type": "application/json",
15061
- authorization: `Bearer ${token}`
15333
+ ...authHeaders(token)
15062
15334
  },
15063
15335
  body: JSON.stringify({ projectId: input.projectId })
15064
15336
  });
@@ -15077,7 +15349,7 @@ async function enableAi(apiUrl, token, input) {
15077
15349
  }
15078
15350
  async function getAiStatus(apiUrl, token, projectId) {
15079
15351
  const res = await fetch(`${apiUrl}/ai/${encodeURIComponent(projectId)}`, {
15080
- headers: { authorization: `Bearer ${token}` }
15352
+ headers: { ...authHeaders(token) }
15081
15353
  });
15082
15354
  if (!res.ok) {
15083
15355
  throw new ApiError(
@@ -15095,7 +15367,7 @@ async function getAiStatus(apiUrl, token, projectId) {
15095
15367
  async function removeAi(apiUrl, token, projectId) {
15096
15368
  const res = await fetch(`${apiUrl}/ai/${encodeURIComponent(projectId)}`, {
15097
15369
  method: "DELETE",
15098
- headers: { authorization: `Bearer ${token}` }
15370
+ headers: { ...authHeaders(token) }
15099
15371
  });
15100
15372
  if (!res.ok) {
15101
15373
  throw new ApiError(
@@ -15117,7 +15389,7 @@ async function addWorkflows(apiUrl, token, input) {
15117
15389
  method: "POST",
15118
15390
  headers: {
15119
15391
  "content-type": "application/json",
15120
- authorization: `Bearer ${token}`
15392
+ ...authHeaders(token)
15121
15393
  },
15122
15394
  body: JSON.stringify({ projectId: input.projectId })
15123
15395
  });
@@ -15132,7 +15404,7 @@ async function addWorkflows(apiUrl, token, input) {
15132
15404
  }
15133
15405
  async function getWorkflowStatus(apiUrl, token, projectId) {
15134
15406
  const res = await fetch(`${apiUrl}/workflows/${encodeURIComponent(projectId)}`, {
15135
- headers: { authorization: `Bearer ${token}` }
15407
+ headers: { ...authHeaders(token) }
15136
15408
  });
15137
15409
  if (!res.ok) {
15138
15410
  throw new ApiError(
@@ -15146,7 +15418,7 @@ async function getWorkflowStatus(apiUrl, token, projectId) {
15146
15418
  async function removeWorkflows(apiUrl, token, projectId) {
15147
15419
  const res = await fetch(`${apiUrl}/workflows/${encodeURIComponent(projectId)}`, {
15148
15420
  method: "DELETE",
15149
- headers: { authorization: `Bearer ${token}` }
15421
+ headers: { ...authHeaders(token) }
15150
15422
  });
15151
15423
  if (!res.ok) {
15152
15424
  throw new ApiError(
@@ -15197,7 +15469,7 @@ function domainsUrl(apiUrl, projectId) {
15197
15469
  }
15198
15470
  async function listDomains(apiUrl, token, projectId) {
15199
15471
  const res = await fetch(domainsUrl(apiUrl, projectId), {
15200
- headers: { authorization: `Bearer ${token}` }
15472
+ headers: { ...authHeaders(token) }
15201
15473
  });
15202
15474
  if (!res.ok) {
15203
15475
  throw new ApiError(`Domain listing failed (${res.status})`, await parseEnvelope(res), res.status);
@@ -15214,7 +15486,7 @@ async function listDomains(apiUrl, token, projectId) {
15214
15486
  async function addDomain(apiUrl, token, projectId, host, app, redirectTo) {
15215
15487
  const res = await fetch(domainsUrl(apiUrl, projectId), {
15216
15488
  method: "POST",
15217
- headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
15489
+ headers: { "content-type": "application/json", ...authHeaders(token) },
15218
15490
  body: JSON.stringify({ host, ...app ? { app } : {}, ...redirectTo !== void 0 ? { redirectTo } : {} })
15219
15491
  });
15220
15492
  if (!res.ok) {
@@ -15225,7 +15497,7 @@ async function addDomain(apiUrl, token, projectId, host, app, redirectTo) {
15225
15497
  async function removeDomain(apiUrl, token, projectId, host) {
15226
15498
  const res = await fetch(`${domainsUrl(apiUrl, projectId)}/${encodeURIComponent(host)}`, {
15227
15499
  method: "DELETE",
15228
- headers: { authorization: `Bearer ${token}` }
15500
+ headers: { ...authHeaders(token) }
15229
15501
  });
15230
15502
  if (!res.ok) {
15231
15503
  throw new ApiError(`Domain removal failed (${res.status})`, await parseEnvelope(res), res.status);
@@ -15237,7 +15509,7 @@ function deployHookUrl(apiUrl, projectId) {
15237
15509
  async function createDeployHook(apiUrl, token, projectId) {
15238
15510
  const res = await fetch(deployHookUrl(apiUrl, projectId), {
15239
15511
  method: "PUT",
15240
- headers: { authorization: `Bearer ${token}` }
15512
+ headers: { ...authHeaders(token) }
15241
15513
  });
15242
15514
  if (!res.ok) {
15243
15515
  throw new ApiError(`Deploy hook creation failed (${res.status})`, await parseEnvelope(res), res.status);
@@ -15251,7 +15523,7 @@ async function createDeployHook(apiUrl, token, projectId) {
15251
15523
  async function removeDeployHook(apiUrl, token, projectId) {
15252
15524
  const res = await fetch(deployHookUrl(apiUrl, projectId), {
15253
15525
  method: "DELETE",
15254
- headers: { authorization: `Bearer ${token}` }
15526
+ headers: { ...authHeaders(token) }
15255
15527
  });
15256
15528
  if (!res.ok) {
15257
15529
  throw new ApiError(`Deploy hook removal failed (${res.status})`, await parseEnvelope(res), res.status);
@@ -15264,7 +15536,7 @@ async function removeDeployHook(apiUrl, token, projectId) {
15264
15536
  }
15265
15537
  async function fetchResourceStatus(apiUrl, token, path) {
15266
15538
  const res = await fetch(`${apiUrl}${path}`, {
15267
- headers: { authorization: `Bearer ${token}` }
15539
+ headers: { ...authHeaders(token) }
15268
15540
  });
15269
15541
  if (!res.ok) {
15270
15542
  throw new ApiError(
@@ -15279,14 +15551,15 @@ async function fetchResourceStatus(apiUrl, token, path) {
15279
15551
  }
15280
15552
  return {
15281
15553
  status: parsed.status,
15282
- failureReason: typeof parsed.failureReason === "string" ? parsed.failureReason : null
15554
+ failureReason: typeof parsed.failureReason === "string" ? parsed.failureReason : null,
15555
+ ...parsed.environment === "production" || parsed.environment === "preview" ? { environment: parsed.environment } : {}
15283
15556
  };
15284
15557
  }
15285
- function getDatabaseStatus(apiUrl, token, projectId) {
15558
+ function getDatabaseStatus(apiUrl, token, projectId, environment) {
15286
15559
  return fetchResourceStatus(
15287
15560
  apiUrl,
15288
15561
  token,
15289
- `/databases/${encodeURIComponent(projectId)}`
15562
+ `/databases/${encodeURIComponent(projectId)}${environmentQuery(environment)}`
15290
15563
  );
15291
15564
  }
15292
15565
  function getStorageStatus(apiUrl, token, projectId) {
@@ -15344,10 +15617,10 @@ async function pollResourceDetached(fetchStatus, opts) {
15344
15617
  await sleep(intervalMs);
15345
15618
  }
15346
15619
  }
15347
- async function detachDatabase(apiUrl, token, projectId) {
15620
+ async function detachDatabase(apiUrl, token, projectId, environment) {
15348
15621
  const res = await fetch(
15349
- `${apiUrl}/databases/${encodeURIComponent(projectId)}`,
15350
- { method: "DELETE", headers: { authorization: `Bearer ${token}` } }
15622
+ `${apiUrl}/databases/${encodeURIComponent(projectId)}${environmentQuery(environment)}`,
15623
+ { method: "DELETE", headers: { ...authHeaders(token) } }
15351
15624
  );
15352
15625
  if (!res.ok) {
15353
15626
  throw new ApiError(
@@ -15357,9 +15630,22 @@ async function detachDatabase(apiUrl, token, projectId) {
15357
15630
  );
15358
15631
  }
15359
15632
  }
15633
+ async function resetPreviewDatabase(apiUrl, token, projectId) {
15634
+ const res = await fetch(
15635
+ `${apiUrl}/databases/${encodeURIComponent(projectId)}/reset?environment=preview`,
15636
+ { method: "POST", headers: { ...authHeaders(token) } }
15637
+ );
15638
+ if (!res.ok) {
15639
+ throw new ApiError(
15640
+ `Database reset failed (${res.status})`,
15641
+ await parseEnvelope(res),
15642
+ res.status
15643
+ );
15644
+ }
15645
+ }
15360
15646
  async function getProject(apiUrl, token, projectId) {
15361
15647
  const res = await fetch(`${apiUrl}/projects/${encodeURIComponent(projectId)}`, {
15362
- headers: { authorization: `Bearer ${token}` }
15648
+ headers: { ...authHeaders(token) }
15363
15649
  });
15364
15650
  if (res.status === 404) return null;
15365
15651
  if (!res.ok) {
@@ -15369,7 +15655,7 @@ async function getProject(apiUrl, token, projectId) {
15369
15655
  }
15370
15656
  async function listProjects(apiUrl, token) {
15371
15657
  const res = await fetch(`${apiUrl}/projects`, {
15372
- headers: { authorization: `Bearer ${token}` }
15658
+ headers: { ...authHeaders(token) }
15373
15659
  });
15374
15660
  if (!res.ok) {
15375
15661
  throw new ApiError(`Listing projects failed (${res.status})`, await parseEnvelope(res), res.status);
@@ -15379,7 +15665,7 @@ async function listProjects(apiUrl, token) {
15379
15665
  async function deleteProject(apiUrl, token, projectId) {
15380
15666
  const res = await fetch(`${apiUrl}/projects/${encodeURIComponent(projectId)}`, {
15381
15667
  method: "DELETE",
15382
- headers: { authorization: `Bearer ${token}` }
15668
+ headers: { ...authHeaders(token) }
15383
15669
  });
15384
15670
  if (!res.ok) {
15385
15671
  throw new ApiError(`Project delete failed (${res.status})`, await parseEnvelope(res), res.status);
@@ -15412,7 +15698,7 @@ async function pollProjectDeleted(read, opts) {
15412
15698
  async function detachStorage(apiUrl, token, projectId) {
15413
15699
  const res = await fetch(`${apiUrl}/storage/${encodeURIComponent(projectId)}`, {
15414
15700
  method: "DELETE",
15415
- headers: { authorization: `Bearer ${token}` }
15701
+ headers: { ...authHeaders(token) }
15416
15702
  });
15417
15703
  if (!res.ok) {
15418
15704
  throw new ApiError(
@@ -15425,7 +15711,7 @@ async function detachStorage(apiUrl, token, projectId) {
15425
15711
  async function listEnvVars(apiUrl, token, projectId, include = "all") {
15426
15712
  const res = await fetch(
15427
15713
  `${apiUrl}/projects/${encodeURIComponent(projectId)}/env?scope=${encodeURIComponent(include)}`,
15428
- { headers: { authorization: `Bearer ${token}` } }
15714
+ { headers: { ...authHeaders(token) } }
15429
15715
  );
15430
15716
  if (!res.ok) {
15431
15717
  throw new ApiError(
@@ -15478,7 +15764,7 @@ async function pullEnvValues(apiUrl, token, projectId, environment = "production
15478
15764
  const query = environment === "production" ? "" : `?environment=${encodeURIComponent(environment)}`;
15479
15765
  const res = await fetch(
15480
15766
  `${apiUrl}/projects/${encodeURIComponent(projectId)}/env/values${query}`,
15481
- { headers: { authorization: `Bearer ${token}` } }
15767
+ { headers: { ...authHeaders(token) } }
15482
15768
  );
15483
15769
  if (!res.ok) {
15484
15770
  throw new ApiError(
@@ -15512,7 +15798,7 @@ async function setEnvVar(apiUrl, token, projectId, key, value, scope = "runtime"
15512
15798
  method: "PUT",
15513
15799
  headers: {
15514
15800
  "content-type": "application/json",
15515
- authorization: `Bearer ${token}`
15801
+ ...authHeaders(token)
15516
15802
  },
15517
15803
  // `scope`/`environment` are sent only when NOT their defaults. An api older than KRYD-156/159
15518
15804
  // ignores unknown body fields, so this is belt-and-braces — but it keeps a plain runtime
@@ -15553,7 +15839,7 @@ async function removeEnvVar(apiUrl, token, projectId, key, scope = "runtime", en
15553
15839
  const query = params.toString() ? `?${params.toString()}` : "";
15554
15840
  const res = await fetch(
15555
15841
  `${apiUrl}/projects/${encodeURIComponent(projectId)}/env/${encodeURIComponent(key)}${query}`,
15556
- { method: "DELETE", headers: { authorization: `Bearer ${token}` } }
15842
+ { method: "DELETE", headers: { ...authHeaders(token) } }
15557
15843
  );
15558
15844
  if (!res.ok) {
15559
15845
  throw new ApiError(
@@ -15579,7 +15865,7 @@ async function triggerDeploy(apiUrl, token, projectId) {
15579
15865
  method: "POST",
15580
15866
  headers: {
15581
15867
  "content-type": "application/json",
15582
- authorization: `Bearer ${token}`
15868
+ ...authHeaders(token)
15583
15869
  },
15584
15870
  body: JSON.stringify({ projectId })
15585
15871
  });
@@ -15597,7 +15883,7 @@ async function rollbackDeploy(apiUrl, token, projectId, deploymentId, app) {
15597
15883
  method: "POST",
15598
15884
  headers: {
15599
15885
  "content-type": "application/json",
15600
- authorization: `Bearer ${token}`
15886
+ ...authHeaders(token)
15601
15887
  },
15602
15888
  body: JSON.stringify({
15603
15889
  projectId,
@@ -15615,7 +15901,7 @@ async function redeployProject(apiUrl, token, projectId, app) {
15615
15901
  method: "POST",
15616
15902
  headers: {
15617
15903
  "content-type": "application/json",
15618
- authorization: `Bearer ${token}`
15904
+ ...authHeaders(token)
15619
15905
  },
15620
15906
  body: JSON.stringify({ projectId, ...app ? { app } : {} })
15621
15907
  });
@@ -15627,7 +15913,7 @@ async function redeployProject(apiUrl, token, projectId, app) {
15627
15913
  async function listDeployments(apiUrl, token, projectId) {
15628
15914
  const url2 = new URL(`${apiUrl}/deployments`);
15629
15915
  if (projectId) url2.searchParams.set("projectId", projectId);
15630
- const res = await fetch(url2, { headers: { authorization: `Bearer ${token}` } });
15916
+ const res = await fetch(url2, { headers: { ...authHeaders(token) } });
15631
15917
  if (!res.ok) {
15632
15918
  throw new ApiError(`Listing deploys failed (${res.status})`, await parseEnvelope(res), res.status);
15633
15919
  }
@@ -15731,7 +16017,7 @@ async function streamDeploy(apiUrl, token, deploymentId, handlers, opts = {}) {
15731
16017
  let dropped = false;
15732
16018
  try {
15733
16019
  const res = await doFetch(`${apiUrl}/deployments/${deploymentId}/logs`, {
15734
- headers: { authorization: `Bearer ${token}`, accept: "text/event-stream" }
16020
+ headers: { ...authHeaders(token), accept: "text/event-stream" }
15735
16021
  });
15736
16022
  if (!res.ok) {
15737
16023
  throw new ApiError(`Log stream failed (${res.status})`, await parseEnvelope(res), res.status);
@@ -15773,7 +16059,7 @@ async function streamRuntime(apiUrl, token, id, handlers, opts = {}) {
15773
16059
  if (afterNs) url2.searchParams.set("after", afterNs);
15774
16060
  else if (opts.since) url2.searchParams.set("since", opts.since);
15775
16061
  const res = await doFetch(url2, {
15776
- headers: { authorization: `Bearer ${token}`, accept: "text/event-stream" },
16062
+ headers: { ...authHeaders(token), accept: "text/event-stream" },
15777
16063
  ...opts.signal ? { signal: opts.signal } : {}
15778
16064
  });
15779
16065
  if (!res.ok) {
@@ -15803,7 +16089,7 @@ async function streamRuntime(apiUrl, token, id, handlers, opts = {}) {
15803
16089
  async function fetchDeployLog(apiUrl, token, deploymentId, opts = {}) {
15804
16090
  const doFetch = opts.fetchImpl ?? fetch;
15805
16091
  const res = await doFetch(`${apiUrl}/deployments/${deploymentId}/log`, {
15806
- headers: { authorization: `Bearer ${token}` }
16092
+ headers: { ...authHeaders(token) }
15807
16093
  });
15808
16094
  if (res.status === 404) return null;
15809
16095
  if (!res.ok) {
@@ -15818,7 +16104,7 @@ async function fetchDeployLog(apiUrl, token, deploymentId, opts = {}) {
15818
16104
  async function fetchPushCredential(apiUrl, token, projectId) {
15819
16105
  const res = await fetch(
15820
16106
  `${apiUrl}/repos/${encodeURIComponent(projectId)}/push-credential`,
15821
- { headers: { authorization: `Bearer ${token}` } }
16107
+ { headers: { ...authHeaders(token) } }
15822
16108
  );
15823
16109
  if (!res.ok) {
15824
16110
  throw new ApiError(
@@ -15847,7 +16133,7 @@ async function supportFetch(apiUrl, token, path, what, init = {}) {
15847
16133
  const res = await fetch(`${apiUrl}${path}`, {
15848
16134
  method: init.method ?? "GET",
15849
16135
  headers: {
15850
- authorization: `Bearer ${token}`,
16136
+ ...authHeaders(token),
15851
16137
  ...init.body !== void 0 ? { "content-type": "application/json" } : {}
15852
16138
  },
15853
16139
  ...init.body !== void 0 ? { body: JSON.stringify(init.body) } : {}
@@ -15897,204 +16183,25 @@ async function replySupportTicket(apiUrl, token, ref, text) {
15897
16183
  return body;
15898
16184
  }
15899
16185
 
15900
- // src/config.ts
15901
- import {
15902
- appendFileSync,
15903
- chmodSync,
15904
- existsSync,
15905
- mkdirSync,
15906
- readFileSync,
15907
- rmSync,
15908
- writeFileSync
15909
- } from "node:fs";
15910
- import { homedir } from "node:os";
15911
- import { dirname, join } from "node:path";
15912
- var DEFAULT_API_URL = "http://localhost:3000";
15913
- function configPath() {
15914
- const dir = process.env.KRYD_CONFIG_DIR ?? join(homedir(), ".kryd");
15915
- return join(dir, "config.json");
16186
+ // src/support.ts
16187
+ var SUPPORT_TYPES = TICKET_TYPES;
16188
+ var MESSAGE_MAX = 2e4;
16189
+ var SUBJECT_MAX = 200;
16190
+ var LOG_TAIL_LINES = 40;
16191
+ var CONTEXT_SEPARATOR = "\n\n---\n";
16192
+ var URL_CREDENTIAL = /\b([a-z][a-z0-9+.-]*:\/\/)([^\s:@/]+):([^\s@/]+)@/gi;
16193
+ var SECRET_ASSIGNMENT = /\b([A-Za-z0-9_.-]*(?:TOKEN|SECRET|PASSWORD|PASSWD|KEY|DSN|DATABASE_URL|CREDENTIAL)[A-Za-z0-9_.-]*)(\s*[=:]\s*)(["']?)([^\s"']+)/gi;
16194
+ function maskSecrets(line) {
16195
+ return line.replace(URL_CREDENTIAL, "$1$2:***@").replace(SECRET_ASSIGNMENT, "$1$2$3***");
15916
16196
  }
15917
- function loadConfig() {
15918
- let raw;
15919
- try {
15920
- raw = readFileSync(configPath(), "utf8");
15921
- } catch {
15922
- return {};
15923
- }
15924
- try {
15925
- return JSON.parse(raw);
15926
- } catch {
15927
- process.stderr.write(
15928
- `warning: ${configPath()} is not valid JSON \u2014 ignoring it.
15929
- `
15930
- );
15931
- return {};
15932
- }
16197
+ function logTail(log, n = LOG_TAIL_LINES) {
16198
+ const lines = log.split(/\r?\n/);
16199
+ while (lines.length > 0 && lines[lines.length - 1].trim() === "") lines.pop();
16200
+ return lines.slice(-n).map(maskSecrets);
15933
16201
  }
15934
- function resolveToken() {
15935
- const fromEnv = process.env.KRYD_TOKEN?.trim();
15936
- return fromEnv ? fromEnv : loadConfig().token;
15937
- }
15938
- function writeConfig(config2) {
15939
- const path = configPath();
15940
- mkdirSync(dirname(path), { recursive: true, mode: 448 });
15941
- writeFileSync(path, JSON.stringify(config2, null, 2) + "\n", { mode: 384 });
15942
- chmodSync(path, 384);
15943
- }
15944
- function saveToken(token, apiUrl) {
15945
- const config2 = loadConfig();
15946
- config2.token = token;
15947
- if (apiUrl) config2.apiUrl = apiUrl;
15948
- writeConfig(config2);
15949
- }
15950
- function clearToken() {
15951
- const config2 = loadConfig();
15952
- delete config2.token;
15953
- writeConfig(config2);
15954
- }
15955
- var lastApiUrl = null;
15956
- function resolveApiUrl(flag) {
15957
- lastApiUrl = flag ?? process.env.KRYD_API_URL ?? loadConfig().apiUrl ?? DEFAULT_API_URL;
15958
- return lastApiUrl;
15959
- }
15960
- function currentApiUrl() {
15961
- return lastApiUrl ?? resolveApiUrl();
15962
- }
15963
- var DEFAULT_DASHBOARD_URL = "https://app.kryd.eu";
15964
- function resolveDashboardUrl(flag) {
15965
- return flag ?? process.env.KRYD_DASHBOARD_URL ?? DEFAULT_DASHBOARD_URL;
15966
- }
15967
- var PROJECT_LINK_DIR = ".kryd";
15968
- var PROJECT_LINK_FILE = "project.json";
15969
- function findProjectLinkDir(cwd = process.cwd()) {
15970
- let dir = cwd;
15971
- for (; ; ) {
15972
- if (existsSync(join(dir, PROJECT_LINK_DIR, PROJECT_LINK_FILE))) return dir;
15973
- if (existsSync(join(dir, ".git"))) return null;
15974
- const parent = dirname(dir);
15975
- if (parent === dir) return null;
15976
- dir = parent;
15977
- }
15978
- }
15979
- function loadProjectLink(cwd = process.cwd()) {
15980
- const root = findProjectLinkDir(cwd);
15981
- if (!root) return null;
15982
- const path = join(root, PROJECT_LINK_DIR, PROJECT_LINK_FILE);
15983
- let raw;
15984
- try {
15985
- raw = readFileSync(path, "utf8");
15986
- } catch {
15987
- return null;
15988
- }
15989
- let parsed;
15990
- try {
15991
- parsed = JSON.parse(raw);
15992
- } catch {
15993
- process.stderr.write(`warning: ${path} is not valid JSON \u2014 ignoring it.
15994
- `);
15995
- return null;
15996
- }
15997
- if (!parsed || typeof parsed.projectId !== "string" || parsed.projectId.length === 0) {
15998
- process.stderr.write(`warning: ${path} has no projectId \u2014 ignoring it.
15999
- `);
16000
- return null;
16001
- }
16002
- return parsed;
16003
- }
16004
- function saveProjectLink(cwd, link) {
16005
- const dir = join(cwd, PROJECT_LINK_DIR);
16006
- mkdirSync(dir, { recursive: true });
16007
- const path = join(dir, PROJECT_LINK_FILE);
16008
- writeFileSync(path, JSON.stringify(link, null, 2) + "\n");
16009
- return path;
16010
- }
16011
- function ensureIgnored(cwd, entry, aliases = []) {
16012
- const path = join(cwd, ".gitignore");
16013
- let current = "";
16014
- try {
16015
- current = readFileSync(path, "utf8");
16016
- } catch {
16017
- }
16018
- const present = current.split("\n").map((l) => l.trim()).some((l) => l === entry || aliases.includes(l));
16019
- if (present) return false;
16020
- const prefix = current.length > 0 && !current.endsWith("\n") ? "\n" : "";
16021
- appendFileSync(path, `${prefix}${entry}
16022
- `);
16023
- return true;
16024
- }
16025
- function ensureKrydIgnored(cwd) {
16026
- ensureIgnored(cwd, `${PROJECT_LINK_DIR}/`, [PROJECT_LINK_DIR]);
16027
- }
16028
- function directoryIsLinked(cwd) {
16029
- const path = join(cwd, PROJECT_LINK_DIR, PROJECT_LINK_FILE);
16030
- if (!existsSync(path)) return null;
16031
- return loadProjectLink(cwd);
16032
- }
16033
- function writeSecretFile(path, contents) {
16034
- mkdirSync(dirname(path), { recursive: true });
16035
- writeFileSync(path, contents, { mode: 384 });
16036
- chmodSync(path, 384);
16037
- }
16038
- function clearProjectLinkIfMatches(projectId, cwd = process.cwd()) {
16039
- const root = findProjectLinkDir(cwd);
16040
- if (!root) return false;
16041
- const link = loadProjectLink(cwd);
16042
- if (link?.projectId !== projectId) return false;
16043
- try {
16044
- rmSync(join(root, PROJECT_LINK_DIR, PROJECT_LINK_FILE));
16045
- return true;
16046
- } catch {
16047
- return false;
16048
- }
16049
- }
16050
- function resolvedLink() {
16051
- return lastLinkResolution;
16052
- }
16053
- var lastLinkResolution = null;
16054
- function resolveProjectId(explicit, cwd = process.cwd()) {
16055
- lastLinkResolution = null;
16056
- if (explicit) return explicit;
16057
- const root = findProjectLinkDir(cwd);
16058
- if (!root) return null;
16059
- const link = loadProjectLink(cwd);
16060
- if (!link) return null;
16061
- lastLinkResolution = { root, link };
16062
- return link.projectId;
16063
- }
16064
- function rememberLinkAccount(root, account) {
16065
- if (!account.accountId) return false;
16066
- const link = directoryIsLinked(root);
16067
- if (!link) return false;
16068
- const slug = account.accountSlug ?? link.accountSlug;
16069
- if (link.accountId === account.accountId && link.accountSlug === slug) return false;
16070
- saveProjectLink(root, {
16071
- ...link,
16072
- accountId: account.accountId,
16073
- // `exactOptionalPropertyTypes`: an absent key and an explicit `undefined` are different types.
16074
- ...slug ? { accountSlug: slug } : {}
16075
- });
16076
- return true;
16077
- }
16078
-
16079
- // src/support.ts
16080
- var SUPPORT_TYPES = TICKET_TYPES;
16081
- var MESSAGE_MAX = 2e4;
16082
- var SUBJECT_MAX = 200;
16083
- var LOG_TAIL_LINES = 40;
16084
- var CONTEXT_SEPARATOR = "\n\n---\n";
16085
- var URL_CREDENTIAL = /\b([a-z][a-z0-9+.-]*:\/\/)([^\s:@/]+):([^\s@/]+)@/gi;
16086
- var SECRET_ASSIGNMENT = /\b([A-Za-z0-9_.-]*(?:TOKEN|SECRET|PASSWORD|PASSWD|KEY|DSN|DATABASE_URL|CREDENTIAL)[A-Za-z0-9_.-]*)(\s*[=:]\s*)(["']?)([^\s"']+)/gi;
16087
- function maskSecrets(line) {
16088
- return line.replace(URL_CREDENTIAL, "$1$2:***@").replace(SECRET_ASSIGNMENT, "$1$2$3***");
16089
- }
16090
- function logTail(log, n = LOG_TAIL_LINES) {
16091
- const lines = log.split(/\r?\n/);
16092
- while (lines.length > 0 && lines[lines.length - 1].trim() === "") lines.pop();
16093
- return lines.slice(-n).map(maskSecrets);
16094
- }
16095
- function fenceFor(lines) {
16096
- const longest = Math.max(0, ...lines.map((l) => Math.max(0, ...(l.match(/`+/g) ?? []).map((r) => r.length))));
16097
- return "`".repeat(Math.max(3, longest + 1));
16202
+ function fenceFor(lines) {
16203
+ const longest = Math.max(0, ...lines.map((l) => Math.max(0, ...(l.match(/`+/g) ?? []).map((r) => r.length))));
16204
+ return "`".repeat(Math.max(3, longest + 1));
16098
16205
  }
16099
16206
  function deployLine(deploy) {
16100
16207
  if (deploy === "unreadable") return "Last production deploy: could not be read";
@@ -16312,6 +16419,110 @@ async function runSupportReply(raw, opts) {
16312
16419
  }
16313
16420
  }
16314
16421
 
16422
+ // src/account.ts
16423
+ function fail(message) {
16424
+ process.stderr.write(`${message}
16425
+ `);
16426
+ process.exitCode = 1;
16427
+ }
16428
+ function membershipLabel(m) {
16429
+ return m.slug ?? m.name ?? m.accountId;
16430
+ }
16431
+ function accountHint(err) {
16432
+ if (!(err instanceof ApiError) || !err.envelope) return "";
16433
+ const { code, details } = err.envelope.error;
16434
+ if (code === ERROR_CODES.accountRequired) {
16435
+ return "You belong to several accounts. Choose one with `kryd account use <slug>` (`kryd account list` shows them).\n";
16436
+ }
16437
+ if (code === ERROR_CODES.permissionRequired) {
16438
+ const permission = typeof details?.permission === "string" ? ` \`${details.permission}\`` : " this permission";
16439
+ const slug = loadConfig().accountSlug;
16440
+ return `Your role in ${slug ? `account ${slug}` : "this account"} lacks${permission}. Ask the account's owner, or switch with \`kryd account use <slug>\`.
16441
+ `;
16442
+ }
16443
+ const stored = loadConfig();
16444
+ if (code === ERROR_CODES.forbidden && stored.account && /not a member/i.test(err.envelope.error.message)) {
16445
+ const name = stored.accountSlug ?? stored.account;
16446
+ return `You are no longer a member of account ${name}, which this machine has chosen. See your accounts with \`kryd account list\`, then pick one with \`kryd account use <slug>\`.
16447
+ `;
16448
+ }
16449
+ return "";
16450
+ }
16451
+ async function runAccountList(opts = {}) {
16452
+ const token = resolveToken();
16453
+ if (!token) return fail("Not logged in \u2014 run `kryd login` first.");
16454
+ let memberships;
16455
+ let current;
16456
+ try {
16457
+ const me = await whoami(resolveApiUrl(opts.apiUrl), token);
16458
+ memberships = me.memberships ?? [];
16459
+ current = me.accountId;
16460
+ } catch (err) {
16461
+ return fail(err instanceof ApiError && err.envelope ? err.envelope.error.message : String(err));
16462
+ }
16463
+ if (opts.json) {
16464
+ process.stdout.write(`${JSON.stringify(memberships.map((m) => ({ ...m, current: m.accountId === current })), null, 2)}
16465
+ `);
16466
+ return;
16467
+ }
16468
+ if (memberships.length === 0) {
16469
+ process.stdout.write("You do not belong to any account.\n");
16470
+ return;
16471
+ }
16472
+ const rows = [
16473
+ ["", "SLUG", "NAME", "ROLE", "ID"],
16474
+ ...memberships.map((m) => [m.accountId === current ? "*" : "", m.slug ?? "\u2014", m.name, m.role, m.accountId])
16475
+ ];
16476
+ const widths = rows[0].map((_, i) => Math.max(...rows.map((r) => r[i].length)));
16477
+ for (const r of rows) {
16478
+ process.stdout.write(`${r.map((c, i) => c.padEnd(widths[i])).join(" ").trimEnd()}
16479
+ `);
16480
+ }
16481
+ }
16482
+ async function runAccountUse(target, opts = {}) {
16483
+ const token = resolveToken();
16484
+ if (!token) return fail("Not logged in \u2014 run `kryd login` first.");
16485
+ if (token.startsWith("kryd_")) {
16486
+ return fail("A deploy token is bound to one account and cannot switch. Unset KRYD_TOKEN to use your login.");
16487
+ }
16488
+ let memberships;
16489
+ try {
16490
+ memberships = (await whoami(resolveApiUrl(opts.apiUrl), token)).memberships ?? [];
16491
+ } catch (err) {
16492
+ return fail(err instanceof ApiError && err.envelope ? err.envelope.error.message : String(err));
16493
+ }
16494
+ const wanted = target.trim();
16495
+ const match = memberships.find((m) => m.slug === wanted || m.accountId === wanted);
16496
+ if (!match) {
16497
+ const known = memberships.map(membershipLabel).join(", ") || "none";
16498
+ return fail(`You are not a member of an account "${wanted}". Your accounts: ${known}.`);
16499
+ }
16500
+ saveAccount({ accountId: match.accountId, slug: match.slug });
16501
+ process.stdout.write(`Now using account ${membershipLabel(match)} (${match.role}).
16502
+ `);
16503
+ }
16504
+ async function settleAccountAfterLogin(apiUrl, token) {
16505
+ if (token.startsWith("kryd_")) return;
16506
+ let memberships;
16507
+ try {
16508
+ memberships = (await whoami(apiUrl, token)).memberships ?? [];
16509
+ } catch {
16510
+ return;
16511
+ }
16512
+ const stored = loadConfig().account;
16513
+ if (stored && memberships.some((m) => m.accountId === stored)) return;
16514
+ if (memberships.length <= 1) {
16515
+ if (stored) clearAccount();
16516
+ return;
16517
+ }
16518
+ const pick2 = memberships.find((m) => m.role === "owner") ?? memberships[0];
16519
+ saveAccount({ accountId: pick2.accountId, slug: pick2.slug });
16520
+ process.stdout.write(
16521
+ `You belong to ${memberships.length} accounts; using ${membershipLabel(pick2)}. Switch with \`kryd account use <slug>\`.
16522
+ `
16523
+ );
16524
+ }
16525
+
16315
16526
  // src/tokens.ts
16316
16527
  var TOKEN_EXPIRY_CHOICES = ["30", "90", "365", "never"];
16317
16528
  var DEFAULT_TOKEN_EXPIRY = "90";
@@ -16325,7 +16536,7 @@ async function tokenFetch(apiUrl, token, path, what, body) {
16325
16536
  const res = await fetch(`${apiUrl}/api/auth/api-key/${path}`, {
16326
16537
  method: body === void 0 ? "GET" : "POST",
16327
16538
  headers: {
16328
- authorization: `Bearer ${token}`,
16539
+ ...authHeaders(token),
16329
16540
  ...body !== void 0 ? { "content-type": "application/json" } : {}
16330
16541
  },
16331
16542
  ...body !== void 0 ? { body: JSON.stringify(body) } : {}
@@ -16363,50 +16574,53 @@ async function listDeployTokens(apiUrl, token) {
16363
16574
  async function revokeDeployToken(apiUrl, token, id) {
16364
16575
  await tokenFetch(apiUrl, token, "delete", "Revoking the token", { keyId: id });
16365
16576
  }
16366
- function fail(message) {
16577
+ function fail2(message) {
16367
16578
  process.stderr.write(`${message}
16368
16579
  `);
16369
16580
  process.exitCode = 1;
16370
16581
  }
16371
16582
  function reportTokenError(err) {
16372
16583
  if (err instanceof ApiError && err.status === 401) {
16373
- fail("Not logged in, or the session expired \u2014 run `kryd login`.");
16584
+ fail2("Not logged in, or the session expired \u2014 run `kryd login`.");
16374
16585
  } else if (err instanceof ApiError && err.status === 403 && resolveToken()?.startsWith("kryd_")) {
16375
- fail(
16586
+ fail2(
16376
16587
  "A deploy token cannot manage tokens. Sign in with `kryd login` (and unset KRYD_TOKEN for this command)."
16377
16588
  );
16378
16589
  } else if (err instanceof ApiError && err.status === 404) {
16379
- fail("No such token on your account. `kryd token list` shows their ids.");
16590
+ fail2("No such token on your account. `kryd token list` shows their ids.");
16380
16591
  } else if (err instanceof ApiError && err.envelope) {
16381
- fail(err.envelope.error.message);
16592
+ fail2(err.envelope.error.message);
16593
+ process.stderr.write(accountHint(err));
16382
16594
  } else {
16383
- fail(err instanceof Error ? err.message : String(err));
16595
+ fail2(err instanceof Error ? err.message : String(err));
16384
16596
  }
16385
16597
  }
16386
16598
  function requireToken2() {
16387
16599
  const token = resolveToken();
16388
- if (!token) fail("Not logged in \u2014 run `kryd login` first.");
16600
+ if (!token) fail2("Not logged in \u2014 run `kryd login` first.");
16389
16601
  return token ?? null;
16390
16602
  }
16391
16603
  var day = (iso) => iso ? iso.slice(0, 10) : "\u2014";
16392
16604
  async function runTokenCreate(name, opts) {
16393
16605
  const trimmed = name.trim();
16394
16606
  if (trimmed.length === 0 || trimmed.length > MAX_TOKEN_NAME) {
16395
- return fail(`A token name is 1 to ${MAX_TOKEN_NAME} characters.`);
16607
+ return fail2(`A token name is 1 to ${MAX_TOKEN_NAME} characters.`);
16396
16608
  }
16397
16609
  const choice = opts.expires ?? DEFAULT_TOKEN_EXPIRY;
16398
16610
  const expiresIn = expiresInSeconds(choice);
16399
16611
  if (expiresIn === void 0) {
16400
- return fail(`--expires is one of ${TOKEN_EXPIRY_CHOICES.join(", ")} (days, or never).`);
16612
+ return fail2(`--expires is one of ${TOKEN_EXPIRY_CHOICES.join(", ")} (days, or never).`);
16401
16613
  }
16402
16614
  const token = requireToken2();
16403
16615
  if (!token) return;
16404
16616
  try {
16405
- const created = await createDeployToken(resolveApiUrl(opts.apiUrl), token, { name: trimmed, expiresIn });
16617
+ const apiUrl = resolveApiUrl(opts.apiUrl);
16618
+ const created = await createDeployToken(apiUrl, token, { name: trimmed, expiresIn });
16406
16619
  process.stdout.write(`${created.key}
16407
16620
  `);
16621
+ const account = await boundAccountLabel(apiUrl, token, created);
16408
16622
  process.stderr.write(
16409
- `Created deploy token "${trimmed}" (${created.id}), ${created.expiresAt ? `valid until ${day(created.expiresAt)}` : "without expiry"}.
16623
+ `Created deploy token "${trimmed}" (${created.id})${account ? ` for account ${account}` : ""}, ${created.expiresAt ? `valid until ${day(created.expiresAt)}` : "without expiry"}.
16410
16624
  This token will not be shown again. Store it now, e.g. as the CI secret KRYD_TOKEN.
16411
16625
  `
16412
16626
  );
@@ -16414,6 +16628,17 @@ This token will not be shown again. Store it now, e.g. as the CI secret KRYD_TOK
16414
16628
  reportTokenError(err);
16415
16629
  }
16416
16630
  }
16631
+ async function boundAccountLabel(apiUrl, token, created) {
16632
+ const id = typeof created.metadata?.accountId === "string" ? created.metadata.accountId : null;
16633
+ if (!id) return null;
16634
+ try {
16635
+ const me = await whoami(apiUrl, token);
16636
+ const m = me.memberships?.find((x) => x.accountId === id);
16637
+ return m?.slug ?? id;
16638
+ } catch {
16639
+ return id;
16640
+ }
16641
+ }
16417
16642
  async function runTokenList(opts) {
16418
16643
  const token = requireToken2();
16419
16644
  if (!token) return;
@@ -17740,6 +17965,7 @@ async function reportError(err) {
17740
17965
  process.stderr.write(`${err instanceof Error ? err.message : String(err)}
17741
17966
  `);
17742
17967
  }
17968
+ process.stderr.write(accountHint(err));
17743
17969
  process.exitCode = 1;
17744
17970
  if (err instanceof ApiError) {
17745
17971
  process.stderr.write(await explainMissingProject(err));
@@ -17815,20 +18041,31 @@ async function runLogin(opts) {
17815
18041
  warnIfEnvTokenOverrides();
17816
18042
  return;
17817
18043
  }
17818
- const { token, apiUrl: learnedApiUrl } = await browserLogin(resolveDashboardUrl(opts.dashboardUrl));
18044
+ await browserLoginAndSave(apiUrl, opts.dashboardUrl);
18045
+ warnIfEnvTokenOverrides();
18046
+ }
18047
+ async function browserLoginAndSave(apiUrl, dashboardUrl) {
18048
+ const { token, apiUrl: learnedApiUrl } = await browserLogin(resolveDashboardUrl(dashboardUrl));
17819
18049
  saveToken(token, learnedApiUrl ?? apiUrl);
17820
18050
  process.stdout.write("Logged in.\n");
17821
- warnIfEnvTokenOverrides();
18051
+ await settleAccountAfterLogin(learnedApiUrl ?? apiUrl, token);
17822
18052
  }
17823
18053
  async function runWhoami(opts = {}) {
17824
18054
  const apiUrl = resolveApiUrl(opts.apiUrl);
17825
18055
  const token = resolveToken() ?? "";
17826
18056
  try {
17827
18057
  const me = await whoami(apiUrl, token);
17828
- process.stdout.write(
17829
- `${me.email} (account ${me.accountId ?? "?"}, role ${me.role ?? "?"})
18058
+ const current = me.memberships?.find((m) => m.accountId === me.accountId);
18059
+ const label = current?.slug ?? me.accountSlug ?? me.accountId ?? "?";
18060
+ process.stdout.write(`${me.email} (account ${label}, role ${current?.role ?? me.role ?? "?"})
18061
+ `);
18062
+ const others = (me.memberships ?? []).filter((m) => m.accountId !== me.accountId);
18063
+ if (others.length > 0) {
18064
+ process.stdout.write(
18065
+ `Also in: ${others.map((m) => `${m.slug ?? m.accountId} (${m.role})`).join(", ")} \u2014 switch with \`kryd account use <slug>\`.
17830
18066
  `
17831
- );
18067
+ );
18068
+ }
17832
18069
  } catch (err) {
17833
18070
  await reportError(err);
17834
18071
  }
@@ -17846,13 +18083,46 @@ function runLogout() {
17846
18083
  warnIfEnvTokenOverrides();
17847
18084
  }
17848
18085
  async function runInit(opts) {
17849
- const apiUrl = resolveApiUrl(opts.apiUrl);
18086
+ let apiUrl = resolveApiUrl(opts.apiUrl);
17850
18087
  const cwd = opts.cwd ?? process.cwd();
17851
- const token = resolveToken();
18088
+ let token = resolveToken();
17852
18089
  if (!token) {
17853
- process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
17854
- process.exitCode = 1;
17855
- return;
18090
+ const refusal = "Not logged in \u2014 run `kryd login` first.\n";
18091
+ const io = opts.io ?? defaultPromptIO();
18092
+ if (opts.offerLogin === false) {
18093
+ process.stderr.write(refusal);
18094
+ process.exitCode = 1;
18095
+ return;
18096
+ }
18097
+ if (!isInteractive(io)) {
18098
+ process.stderr.write(
18099
+ refusal + // KRYD-385 iteration 1: no KRYD_TOKEN advice — `POST /repos` refuses a deploy token (KRYD-578).
18100
+ "No terminal to log in from: ask the user to run `kryd init` (or `kryd login`) in their own terminal. A deploy token (KRYD_TOKEN) cannot create a project.\n"
18101
+ );
18102
+ process.exitCode = 1;
18103
+ return;
18104
+ }
18105
+ io.output.write("You are not logged in to Kryd; `kryd init` can log you in through your browser first.\n");
18106
+ if (!await promptConfirm("Log in now in your browser?", io)) {
18107
+ process.stderr.write(refusal);
18108
+ process.exitCode = 1;
18109
+ return;
18110
+ }
18111
+ try {
18112
+ await browserLoginAndSave(apiUrl);
18113
+ } catch (err) {
18114
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}
18115
+ `);
18116
+ process.exitCode = 1;
18117
+ return;
18118
+ }
18119
+ token = resolveToken();
18120
+ if (!token) {
18121
+ process.stderr.write(refusal);
18122
+ process.exitCode = 1;
18123
+ return;
18124
+ }
18125
+ if (!opts.apiUrl) apiUrl = resolveApiUrl();
17856
18126
  }
17857
18127
  const existingLink = directoryIsLinked(cwd);
17858
18128
  if (existingLink) {
@@ -18447,6 +18717,17 @@ async function runDbAdd(opts) {
18447
18717
  process.exitCode = 1;
18448
18718
  return;
18449
18719
  }
18720
+ if (opts.preview && opts.connectionString !== void 0) {
18721
+ process.stderr.write(
18722
+ "A preview database is always managed by Kryd \u2014 --preview cannot take a --connection-string.\n"
18723
+ );
18724
+ process.exitCode = 1;
18725
+ return;
18726
+ }
18727
+ if (opts.preview) {
18728
+ await addPreviewDatabase(apiUrl, token, project2);
18729
+ return;
18730
+ }
18450
18731
  let mode;
18451
18732
  let connectionString;
18452
18733
  if (opts.connectionString !== void 0) {
@@ -18492,6 +18773,46 @@ The connection string will be injected as DATABASE_URL on your next deploy.
18492
18773
  await reportError(err);
18493
18774
  }
18494
18775
  }
18776
+ async function addPreviewDatabase(apiUrl, token, project2) {
18777
+ try {
18778
+ const db2 = await attachDatabase(apiUrl, token, {
18779
+ projectId: project2,
18780
+ mode: "shared",
18781
+ environment: "preview"
18782
+ });
18783
+ if (db2.environment !== "preview") {
18784
+ process.stderr.write(
18785
+ `The Kryd API did not create a preview database (it does not support them yet) \u2014 run \`kryd db status ${project2}\` to see what is attached.
18786
+ `
18787
+ );
18788
+ process.exitCode = 1;
18789
+ return;
18790
+ }
18791
+ process.stdout.write(`Provisioning a preview database for ${project2}\u2026
18792
+ `);
18793
+ const result = await pollResourceUntilTerminal(
18794
+ () => getDatabaseStatus(apiUrl, token, project2, "preview")
18795
+ );
18796
+ reportSettleResult(result, {
18797
+ readyMsg: `Attached preview database ${db2.id} to ${project2}. It starts empty.
18798
+ Every preview deploy gets it as DATABASE_URL from its next deploy; production keeps its own.
18799
+ `,
18800
+ failedLabel: "Preview database provisioning",
18801
+ retryCmd: `kryd db status ${project2}`
18802
+ });
18803
+ } catch (err) {
18804
+ await reportError(err);
18805
+ }
18806
+ }
18807
+ async function readPreviewDatabase(apiUrl, token, project2) {
18808
+ try {
18809
+ const row = await getDatabaseStatus(apiUrl, token, project2, "preview");
18810
+ return row.environment === "preview" ? row : null;
18811
+ } catch (err) {
18812
+ if (err instanceof ApiError && err.status === 404) return null;
18813
+ throw err;
18814
+ }
18815
+ }
18495
18816
  function reportSettleResult(result, opts) {
18496
18817
  if (result.status === "ready") {
18497
18818
  process.stdout.write(opts.readyMsg);
@@ -18712,6 +19033,8 @@ async function describeProjectResources(apiUrl, token, projectId) {
18712
19033
  const out = ["its deployed app, its containers and its built images"];
18713
19034
  const db2 = await getDatabaseStatus(apiUrl, token, projectId).catch(() => null);
18714
19035
  if (db2) out.push("its Postgres database \u2014 and all the data in it");
19036
+ const preview = db2 ? await readPreviewDatabase(apiUrl, token, projectId).catch(() => null) : null;
19037
+ if (preview) out.push("its preview database \u2014 and all the data in it");
18715
19038
  const storage2 = await getStorageStatus(apiUrl, token, projectId).catch(() => null);
18716
19039
  if (storage2) out.push("its object-storage bucket \u2014 and every object in it");
18717
19040
  out.push("its Git repository on the forge, and its push token");
@@ -18730,6 +19053,23 @@ async function runDbRemove(opts) {
18730
19053
  reportNotLinked("kryd db remove <projectId>");
18731
19054
  return;
18732
19055
  }
19056
+ if (opts.preview) {
19057
+ await removePreviewDatabase(apiUrl, token, project2, opts);
19058
+ return;
19059
+ }
19060
+ try {
19061
+ if (await readPreviewDatabase(apiUrl, token, project2)) {
19062
+ process.stderr.write(
19063
+ `${project2} has a preview database \u2014 remove the preview first (\`kryd db remove --preview\`).
19064
+ `
19065
+ );
19066
+ process.exitCode = 1;
19067
+ return;
19068
+ }
19069
+ } catch (err) {
19070
+ await reportError(err);
19071
+ return;
19072
+ }
18733
19073
  const proceed = await confirmResourceRemoval({
18734
19074
  yes: opts.yes,
18735
19075
  io: opts.io,
@@ -18756,6 +19096,105 @@ async function runDbRemove(opts) {
18756
19096
  await reportError(err);
18757
19097
  }
18758
19098
  }
19099
+ async function removePreviewDatabase(apiUrl, token, project2, opts) {
19100
+ try {
19101
+ if (!await readPreviewDatabase(apiUrl, token, project2)) {
19102
+ process.stderr.write(`${project2} has no preview database.
19103
+ `);
19104
+ process.exitCode = 1;
19105
+ return;
19106
+ }
19107
+ } catch (err) {
19108
+ await reportError(err);
19109
+ return;
19110
+ }
19111
+ const proceed = await confirmResourceRemoval({
19112
+ yes: opts.yes,
19113
+ io: opts.io,
19114
+ subject: `the preview database from ${project2}`,
19115
+ question: `Remove the preview database from ${project2}? It is torn down and all the data in it is destroyed \u2014 this cannot be undone. Production is not touched; preview deploys use the production database again from their next deploy.`,
19116
+ declined: `Left the preview database on ${project2} in place.
19117
+ `
19118
+ });
19119
+ if (!proceed) return;
19120
+ try {
19121
+ await detachDatabase(apiUrl, token, project2, "preview");
19122
+ process.stdout.write(`Removing the preview database from ${project2}\u2026
19123
+ `);
19124
+ const result = await pollResourceDetached(
19125
+ () => getDatabaseStatus(apiUrl, token, project2, "preview")
19126
+ );
19127
+ reportDetachResult(result, {
19128
+ goneMsg: `Removed the preview database from ${project2}.
19129
+ Preview deploys use the production database again from their next deploy.
19130
+ `,
19131
+ failedLabel: "Preview database removal",
19132
+ retryCmd: `kryd db remove --preview ${project2}`
19133
+ });
19134
+ } catch (err) {
19135
+ await reportError(err);
19136
+ }
19137
+ }
19138
+ async function runDbReset(opts) {
19139
+ if (!opts.preview) {
19140
+ process.stderr.write(
19141
+ "Only the preview database can be reset \u2014 run `kryd db reset --preview`. Production is never emptied.\n"
19142
+ );
19143
+ process.exitCode = 1;
19144
+ return;
19145
+ }
19146
+ const apiUrl = resolveApiUrl(opts.apiUrl);
19147
+ const token = resolveToken();
19148
+ if (!token) {
19149
+ process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
19150
+ process.exitCode = 1;
19151
+ return;
19152
+ }
19153
+ const project2 = resolveProjectId(opts.project, opts.cwd);
19154
+ if (!project2) {
19155
+ reportNotLinked("kryd db reset --preview <projectId>");
19156
+ return;
19157
+ }
19158
+ try {
19159
+ if (!await readPreviewDatabase(apiUrl, token, project2)) {
19160
+ process.stderr.write(
19161
+ `${project2} has no preview database \u2014 add one with \`kryd db add --preview ${project2}\`.
19162
+ `
19163
+ );
19164
+ process.exitCode = 1;
19165
+ return;
19166
+ }
19167
+ } catch (err) {
19168
+ await reportError(err);
19169
+ return;
19170
+ }
19171
+ const proceed = await confirmResourceRemoval({
19172
+ yes: opts.yes,
19173
+ io: opts.io,
19174
+ subject: `everything in the preview database of ${project2}`,
19175
+ question: `Empty the preview database of ${project2}? Every table and all the data in it is dropped \u2014 this cannot be undone. Production is not touched.`,
19176
+ declined: `Left the preview database of ${project2} as it is.
19177
+ `
19178
+ });
19179
+ if (!proceed) return;
19180
+ try {
19181
+ await resetPreviewDatabase(apiUrl, token, project2);
19182
+ process.stdout.write(`Emptying the preview database of ${project2}\u2026
19183
+ `);
19184
+ const result = await pollResourceUntilTerminal(
19185
+ () => getDatabaseStatus(apiUrl, token, project2, "preview")
19186
+ );
19187
+ reportSettleResult(result, {
19188
+ readyMsg: `The preview database of ${project2} is empty again.
19189
+ Its credentials did not change, so no deploy is needed.
19190
+ `,
19191
+ failedLabel: "Preview database reset",
19192
+ retryCmd: `kryd db status ${project2}`
19193
+ });
19194
+ } catch (err) {
19195
+ await reportError(err);
19196
+ }
19197
+ }
18759
19198
  async function runAiAdd(opts) {
18760
19199
  const apiUrl = resolveApiUrl(opts.apiUrl);
18761
19200
  const token = resolveToken();
@@ -18861,12 +19300,45 @@ async function runDbStatus(opts) {
18861
19300
  reportNotLinked("kryd db status <projectId>");
18862
19301
  return;
18863
19302
  }
19303
+ let productionAttached = false;
18864
19304
  await reportResourceStatus({
18865
- read: () => getDatabaseStatus(apiUrl, token, project2),
19305
+ read: async () => {
19306
+ const row = await getDatabaseStatus(apiUrl, token, project2);
19307
+ productionAttached = true;
19308
+ return row;
19309
+ },
18866
19310
  noun: "database",
18867
19311
  project: project2,
18868
19312
  token
18869
19313
  });
19314
+ if (!productionAttached) return;
19315
+ try {
19316
+ const preview = await readPreviewDatabase(apiUrl, token, project2);
19317
+ if (!preview) {
19318
+ process.stdout.write(
19319
+ `${project2}: no preview database \u2014 preview deploys use the production database.
19320
+ `
19321
+ );
19322
+ } else if (preview.status === "failed") {
19323
+ process.stdout.write(
19324
+ `${project2}: preview database failed \u2014 ${preview.failureReason ?? "unknown error"}.
19325
+ Remove it with \`kryd db remove --preview\` and add it again.
19326
+ `
19327
+ );
19328
+ } else if (preview.status === "ready") {
19329
+ process.stdout.write(
19330
+ `${project2}: preview database ready \u2014 preview deploys get it as DATABASE_URL.
19331
+ `
19332
+ );
19333
+ } else {
19334
+ process.stdout.write(
19335
+ `${project2}: preview database ${preview.status} \u2014 preview deploys use the production database until it is ready.
19336
+ `
19337
+ );
19338
+ }
19339
+ } catch (err) {
19340
+ await reportError(err);
19341
+ }
18870
19342
  }
18871
19343
  async function runStorageStatus(opts) {
18872
19344
  const apiUrl = resolveApiUrl(opts.apiUrl);
@@ -19326,6 +19798,7 @@ HATCHET_CLIENT_TOKEN is injected into your container on production deploys (prev
19326
19798
  process.exitCode = 0;
19327
19799
  await runInit({
19328
19800
  cwd: target,
19801
+ offerLogin: false,
19329
19802
  ...opts.name ? { name: opts.name } : {},
19330
19803
  ...opts.apiUrl ? { apiUrl: opts.apiUrl } : {}
19331
19804
  });
@@ -19956,7 +20429,7 @@ Run \`kryd env list\` to see what is set. (If you meant the runtime variable of
19956
20429
  await reportError(err);
19957
20430
  }
19958
20431
  }
19959
- var CLI_VERSION = true ? "0.15.0" : "0.0.0-dev";
20432
+ var CLI_VERSION = true ? "0.17.0" : "0.0.0-dev";
19960
20433
  var program = new Command();
19961
20434
  program.name("kryd").description("Kryd CLI").version(CLI_VERSION);
19962
20435
  program.hook("postAction", async () => {
@@ -19967,6 +20440,9 @@ program.command("login").description("Sign in via the browser (default) and stor
19967
20440
  "supply a token instead of the browser flow (prefer $KRYD_TOKEN \u2014 flags are visible in `ps`)"
19968
20441
  ).option("--api-url <url>", "control-plane API base URL").option("--dashboard-url <url>", "dashboard base URL for the browser approve page (default app.kryd.eu)").action((opts) => runLogin(opts));
19969
20442
  program.command("whoami").description("Show the current account").option("--api-url <url>", "control-plane API base URL").action((opts) => runWhoami(opts));
20443
+ var accountCommand = program.command("account").description("The accounts you belong to, and which one commands run in");
20444
+ accountCommand.command("list").description("List your accounts and your role in each; * marks the one commands run in").option("--json", "print the accounts as JSON on stdout").option("--api-url <url>", "control-plane API base URL").action((opts) => runAccountList(opts));
20445
+ accountCommand.command("use <slug>").description("Run every following command in this account").option("--api-url <url>", "control-plane API base URL").action((slug, opts) => runAccountUse(slug, opts));
19970
20446
  program.command("logout").description("Clear the stored token").action(() => runLogout());
19971
20447
  program.command("git-credential <operation>", { hidden: true }).description("git credential helper (invoked by git, not by you)").option("--api-url <url>", "control-plane API base URL").action(async (operation, opts) => {
19972
20448
  const chunks = [];
@@ -19976,7 +20452,9 @@ program.command("git-credential <operation>", { hidden: true }).description("git
19976
20452
  ...opts.apiUrl ? { apiUrl: opts.apiUrl } : {}
19977
20453
  });
19978
20454
  });
19979
- program.command("init").description("Link this project's repo + register its push webhook").option("--name <name>", "project name (defaults to package.json name / dir)").option(
20455
+ program.command("init").description(
20456
+ "Link this project's repo + register its push webhook; not logged in \u2192 offers the browser login first (terminal only)"
20457
+ ).option("--name <name>", "project name (defaults to package.json name / dir)").option(
19980
20458
  "--framework <framework>",
19981
20459
  'react-router | nextjs | vite-spa | node (auto-detected if omitted; `workflow` comes from a committed kryd.json with "web": false)'
19982
20460
  ).option(
@@ -20037,9 +20515,13 @@ var db = program.command("db").description("Manage project databases");
20037
20515
  db.command("add [project]").description("Attach a shared or bring-your-own Postgres database to a project").option("--shared", "attach a shared managed database (the default)").option(
20038
20516
  "--connection-string <url>",
20039
20517
  "attach an existing database (BYO) by pasting its connection string"
20518
+ ).option(
20519
+ "--preview",
20520
+ "attach an empty preview database that every preview deploy gets instead of production's"
20040
20521
  ).option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runDbAdd({ ...opts, project: project2 }));
20041
- db.command("remove [project]").description("Tear down the project's database and everything in it (asks for confirmation)").option("--yes", "skip the confirmation prompt").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runDbRemove({ ...opts, project: project2 }));
20042
- db.command("status [project]").description("Show whether the project has a database, and what state it is in").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runDbStatus({ ...opts, project: project2 }));
20522
+ db.command("remove [project]").description("Tear down the project's database and everything in it (asks for confirmation)").option("--preview", "remove the preview database; production is not touched").option("--yes", "skip the confirmation prompt").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runDbRemove({ ...opts, project: project2 }));
20523
+ db.command("reset [project]").description("Empty the preview database: every table dropped, credentials kept (asks for confirmation)").option("--preview", "required \u2014 only the preview database can be reset").option("--yes", "skip the confirmation prompt").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runDbReset({ ...opts, project: project2 }));
20524
+ db.command("status [project]").description("Show the project's database and preview database, and what state they are in").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runDbStatus({ ...opts, project: project2 }));
20043
20525
  var storage = program.command("storage").description("Manage project object storage");
20044
20526
  storage.command("add [project]").description("Create an S3-compatible object-storage bucket for a project").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runStorageAdd({ ...opts, project: project2 }));
20045
20527
  storage.command("remove [project]").description("Tear down the project's bucket and every object in it (asks for confirmation)").option("--yes", "skip the confirmation prompt").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runStorageRemove({ ...opts, project: project2 }));
@@ -20105,7 +20587,7 @@ support.command("list").description("Your account's tickets, with their status")
20105
20587
  support.command("show <ticket>").description("One ticket and its conversation, e.g. `kryd support show T-1042`").option("--json", "print the ticket and its messages as JSON on stdout").option("--api-url <url>", "control-plane API base URL").action((ticket, opts) => runSupportShow(ticket, opts));
20106
20588
  support.command("reply <ticket>").description("Answer in a ticket's conversation \u2014 the ticket goes back to Kryd support (status open)").option("--message <text>", "your reply (required)").option("--json", "print the updated ticket as JSON on stdout").option("--api-url <url>", "control-plane API base URL").action((ticket, opts) => runSupportReply(ticket, opts));
20107
20589
  var tokenCommand = program.command("token").description("Deploy tokens for CI and scripts: set one as KRYD_TOKEN and every command uses it, no login needed");
20108
- tokenCommand.command("create <name>").description("Create a deploy token and print it once \u2014 it acts as you, inside your account").option("--expires <days>", "30 | 90 | 365 | never", DEFAULT_TOKEN_EXPIRY).option("--api-url <url>", "control-plane API base URL").action((name, opts) => runTokenCreate(name, opts));
20590
+ tokenCommand.command("create <name>").description("Create a deploy token and print it once \u2014 it acts as you, bound to the current account").option("--expires <days>", "30 | 90 | 365 | never", DEFAULT_TOKEN_EXPIRY).option("--api-url <url>", "control-plane API base URL").action((name, opts) => runTokenCreate(name, opts));
20109
20591
  tokenCommand.command("list").description("Your deploy tokens: name, first characters, created, last used, expiry \u2014 never the value").option("--json", "print the tokens as JSON on stdout").option("--api-url <url>", "control-plane API base URL").action((opts) => runTokenList(opts));
20110
20592
  tokenCommand.command("revoke <id>").description("Revoke a deploy token; anything still using it gets 401").option("--api-url <url>", "control-plane API base URL").action((id, opts) => runTokenRevoke(id, opts));
20111
20593
  function invokedDirectly() {
@@ -20134,6 +20616,7 @@ export {
20134
20616
  runAiStatus,
20135
20617
  runDbAdd,
20136
20618
  runDbRemove,
20619
+ runDbReset,
20137
20620
  runDbStatus,
20138
20621
  runDeploy,
20139
20622
  runDeployHookCreate,