@kryd/cli 0.14.0 → 0.16.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 +12 -1
  2. package/dist/index.js +616 -244
  3. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -8,6 +8,22 @@ 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
+ };
26
+
11
27
  // ../../packages/shared-types/dist/db-url.js
12
28
  function isValidPostgresConnectionString(value) {
13
29
  if (value.length === 0 || /\s/.test(value))
@@ -14875,6 +14891,42 @@ function parseTicketNumber(raw) {
14875
14891
  var TICKET_ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024;
14876
14892
  var TICKET_ATTACHMENT_MAX_SIZE_LABEL = `${TICKET_ATTACHMENT_MAX_BYTES / (1024 * 1024)} MB`;
14877
14893
 
14894
+ // ../../packages/shared-types/dist/permissions.js
14895
+ var PERMISSIONS = [
14896
+ /** Open the site of a private preview or a closed production (enforced from KRYD-575). */
14897
+ "app:view",
14898
+ /** See the account itself: name, slug. */
14899
+ "account:view",
14900
+ /** Rename the account and change its slug. */
14901
+ "account:manage",
14902
+ /** See who is a member, and the invites still open. */
14903
+ "members:view",
14904
+ /** Invite people, change a member's role, remove a member. */
14905
+ "members:manage",
14906
+ /** See usage and costs. */
14907
+ "usage:view",
14908
+ /** See projects, deploys, domains, databases, storage, AI and workflows. */
14909
+ "projects:view",
14910
+ /** Change a project: env vars, domains, databases, storage, AI, workflows, remove it. */
14911
+ "projects:manage",
14912
+ /** Read env var VALUES (secrets), not only their names. */
14913
+ "env:read",
14914
+ /** Link a repository, deploy, roll back, get a push credential, manage deploy hooks. */
14915
+ "deploy",
14916
+ /** Read build and runtime logs. */
14917
+ "logs:view",
14918
+ /** Trigger, cancel and replay workflow runs; add and remove crons and schedules. */
14919
+ "workflows:run",
14920
+ /** Open and answer support tickets. */
14921
+ "support"
14922
+ ];
14923
+ var ROLES = {
14924
+ owner: PERMISSIONS,
14925
+ member: PERMISSIONS.filter((p) => p !== "members:manage" && p !== "account:manage"),
14926
+ viewer: ["app:view"]
14927
+ };
14928
+ var ACCOUNT_HEADER = "Kryd-Account";
14929
+
14878
14930
  // ../../packages/shared-types/dist/workflow-run.js
14879
14931
  var MAX_WEBHOOK_NAME = 32;
14880
14932
  var WEBHOOK_NAME_RE = new RegExp(`^[a-z0-9](?:[a-z0-9-]{0,${MAX_WEBHOOK_NAME - 2}}[a-z0-9])?$`);
@@ -14936,6 +14988,209 @@ import {
14936
14988
  import { basename as basename2, dirname as dirname2, isAbsolute, join as join3, relative, resolve as resolve2 } from "node:path";
14937
14989
  import { pathToFileURL } from "node:url";
14938
14990
 
14991
+ // src/config.ts
14992
+ import {
14993
+ appendFileSync,
14994
+ chmodSync,
14995
+ existsSync,
14996
+ mkdirSync,
14997
+ readFileSync,
14998
+ rmSync,
14999
+ writeFileSync
15000
+ } from "node:fs";
15001
+ import { homedir } from "node:os";
15002
+ import { dirname, join } from "node:path";
15003
+ var DEFAULT_API_URL = "http://localhost:3000";
15004
+ function configPath() {
15005
+ const dir = process.env.KRYD_CONFIG_DIR ?? join(homedir(), ".kryd");
15006
+ return join(dir, "config.json");
15007
+ }
15008
+ function loadConfig() {
15009
+ let raw;
15010
+ try {
15011
+ raw = readFileSync(configPath(), "utf8");
15012
+ } catch {
15013
+ return {};
15014
+ }
15015
+ try {
15016
+ return JSON.parse(raw);
15017
+ } catch {
15018
+ process.stderr.write(
15019
+ `warning: ${configPath()} is not valid JSON \u2014 ignoring it.
15020
+ `
15021
+ );
15022
+ return {};
15023
+ }
15024
+ }
15025
+ function resolveToken() {
15026
+ const fromEnv = process.env.KRYD_TOKEN?.trim();
15027
+ return fromEnv ? fromEnv : loadConfig().token;
15028
+ }
15029
+ function writeConfig(config2) {
15030
+ const path = configPath();
15031
+ mkdirSync(dirname(path), { recursive: true, mode: 448 });
15032
+ writeFileSync(path, JSON.stringify(config2, null, 2) + "\n", { mode: 384 });
15033
+ chmodSync(path, 384);
15034
+ }
15035
+ function saveToken(token, apiUrl) {
15036
+ const config2 = loadConfig();
15037
+ config2.token = token;
15038
+ if (apiUrl) config2.apiUrl = apiUrl;
15039
+ writeConfig(config2);
15040
+ }
15041
+ function clearToken() {
15042
+ const config2 = loadConfig();
15043
+ delete config2.token;
15044
+ delete config2.account;
15045
+ delete config2.accountSlug;
15046
+ writeConfig(config2);
15047
+ }
15048
+ function saveAccount(account) {
15049
+ const config2 = loadConfig();
15050
+ config2.account = account.accountId;
15051
+ if (account.slug) config2.accountSlug = account.slug;
15052
+ else delete config2.accountSlug;
15053
+ writeConfig(config2);
15054
+ }
15055
+ function clearAccount() {
15056
+ const config2 = loadConfig();
15057
+ delete config2.account;
15058
+ delete config2.accountSlug;
15059
+ writeConfig(config2);
15060
+ }
15061
+ var DEPLOY_TOKEN_PREFIX = "kryd_";
15062
+ function accountHeader(token) {
15063
+ if (!token || token.startsWith(DEPLOY_TOKEN_PREFIX)) return {};
15064
+ const account = loadConfig().account;
15065
+ return account ? { [ACCOUNT_HEADER]: account } : {};
15066
+ }
15067
+ function authHeaders(token) {
15068
+ return { authorization: `Bearer ${token}`, ...accountHeader(token) };
15069
+ }
15070
+ var lastApiUrl = null;
15071
+ function resolveApiUrl(flag) {
15072
+ lastApiUrl = flag ?? process.env.KRYD_API_URL ?? loadConfig().apiUrl ?? DEFAULT_API_URL;
15073
+ return lastApiUrl;
15074
+ }
15075
+ function currentApiUrl() {
15076
+ return lastApiUrl ?? resolveApiUrl();
15077
+ }
15078
+ var DEFAULT_DASHBOARD_URL = "https://app.kryd.eu";
15079
+ function resolveDashboardUrl(flag) {
15080
+ return flag ?? process.env.KRYD_DASHBOARD_URL ?? DEFAULT_DASHBOARD_URL;
15081
+ }
15082
+ var PROJECT_LINK_DIR = ".kryd";
15083
+ var PROJECT_LINK_FILE = "project.json";
15084
+ function findProjectLinkDir(cwd = process.cwd()) {
15085
+ let dir = cwd;
15086
+ for (; ; ) {
15087
+ if (existsSync(join(dir, PROJECT_LINK_DIR, PROJECT_LINK_FILE))) return dir;
15088
+ if (existsSync(join(dir, ".git"))) return null;
15089
+ const parent = dirname(dir);
15090
+ if (parent === dir) return null;
15091
+ dir = parent;
15092
+ }
15093
+ }
15094
+ function loadProjectLink(cwd = process.cwd()) {
15095
+ const root = findProjectLinkDir(cwd);
15096
+ if (!root) return null;
15097
+ const path = join(root, PROJECT_LINK_DIR, PROJECT_LINK_FILE);
15098
+ let raw;
15099
+ try {
15100
+ raw = readFileSync(path, "utf8");
15101
+ } catch {
15102
+ return null;
15103
+ }
15104
+ let parsed;
15105
+ try {
15106
+ parsed = JSON.parse(raw);
15107
+ } catch {
15108
+ process.stderr.write(`warning: ${path} is not valid JSON \u2014 ignoring it.
15109
+ `);
15110
+ return null;
15111
+ }
15112
+ if (!parsed || typeof parsed.projectId !== "string" || parsed.projectId.length === 0) {
15113
+ process.stderr.write(`warning: ${path} has no projectId \u2014 ignoring it.
15114
+ `);
15115
+ return null;
15116
+ }
15117
+ return parsed;
15118
+ }
15119
+ function saveProjectLink(cwd, link) {
15120
+ const dir = join(cwd, PROJECT_LINK_DIR);
15121
+ mkdirSync(dir, { recursive: true });
15122
+ const path = join(dir, PROJECT_LINK_FILE);
15123
+ writeFileSync(path, JSON.stringify(link, null, 2) + "\n");
15124
+ return path;
15125
+ }
15126
+ function ensureIgnored(cwd, entry, aliases = []) {
15127
+ const path = join(cwd, ".gitignore");
15128
+ let current = "";
15129
+ try {
15130
+ current = readFileSync(path, "utf8");
15131
+ } catch {
15132
+ }
15133
+ const present = current.split("\n").map((l) => l.trim()).some((l) => l === entry || aliases.includes(l));
15134
+ if (present) return false;
15135
+ const prefix = current.length > 0 && !current.endsWith("\n") ? "\n" : "";
15136
+ appendFileSync(path, `${prefix}${entry}
15137
+ `);
15138
+ return true;
15139
+ }
15140
+ function ensureKrydIgnored(cwd) {
15141
+ ensureIgnored(cwd, `${PROJECT_LINK_DIR}/`, [PROJECT_LINK_DIR]);
15142
+ }
15143
+ function directoryIsLinked(cwd) {
15144
+ const path = join(cwd, PROJECT_LINK_DIR, PROJECT_LINK_FILE);
15145
+ if (!existsSync(path)) return null;
15146
+ return loadProjectLink(cwd);
15147
+ }
15148
+ function writeSecretFile(path, contents) {
15149
+ mkdirSync(dirname(path), { recursive: true });
15150
+ writeFileSync(path, contents, { mode: 384 });
15151
+ chmodSync(path, 384);
15152
+ }
15153
+ function clearProjectLinkIfMatches(projectId, cwd = process.cwd()) {
15154
+ const root = findProjectLinkDir(cwd);
15155
+ if (!root) return false;
15156
+ const link = loadProjectLink(cwd);
15157
+ if (link?.projectId !== projectId) return false;
15158
+ try {
15159
+ rmSync(join(root, PROJECT_LINK_DIR, PROJECT_LINK_FILE));
15160
+ return true;
15161
+ } catch {
15162
+ return false;
15163
+ }
15164
+ }
15165
+ function resolvedLink() {
15166
+ return lastLinkResolution;
15167
+ }
15168
+ var lastLinkResolution = null;
15169
+ function resolveProjectId(explicit, cwd = process.cwd()) {
15170
+ lastLinkResolution = null;
15171
+ if (explicit) return explicit;
15172
+ const root = findProjectLinkDir(cwd);
15173
+ if (!root) return null;
15174
+ const link = loadProjectLink(cwd);
15175
+ if (!link) return null;
15176
+ lastLinkResolution = { root, link };
15177
+ return link.projectId;
15178
+ }
15179
+ function rememberLinkAccount(root, account) {
15180
+ if (!account.accountId) return false;
15181
+ const link = directoryIsLinked(root);
15182
+ if (!link) return false;
15183
+ const slug = account.accountSlug ?? link.accountSlug;
15184
+ if (link.accountId === account.accountId && link.accountSlug === slug) return false;
15185
+ saveProjectLink(root, {
15186
+ ...link,
15187
+ accountId: account.accountId,
15188
+ // `exactOptionalPropertyTypes`: an absent key and an explicit `undefined` are different types.
15189
+ ...slug ? { accountSlug: slug } : {}
15190
+ });
15191
+ return true;
15192
+ }
15193
+
14939
15194
  // src/client.ts
14940
15195
  var ApiError = class extends Error {
14941
15196
  envelope;
@@ -14965,7 +15220,7 @@ async function parseEnvelope(res) {
14965
15220
  }
14966
15221
  async function whoami(apiUrl, token) {
14967
15222
  const res = await fetch(`${apiUrl}/me`, {
14968
- headers: { authorization: `Bearer ${token}` }
15223
+ headers: { ...authHeaders(token) }
14969
15224
  });
14970
15225
  if (!res.ok) {
14971
15226
  throw new ApiError(
@@ -14981,7 +15236,7 @@ async function linkProject(apiUrl, token, input) {
14981
15236
  method: "POST",
14982
15237
  headers: {
14983
15238
  "content-type": "application/json",
14984
- authorization: `Bearer ${token}`
15239
+ ...authHeaders(token)
14985
15240
  },
14986
15241
  body: JSON.stringify(input)
14987
15242
  });
@@ -15005,7 +15260,7 @@ async function attachDatabase(apiUrl, token, input) {
15005
15260
  method: "POST",
15006
15261
  headers: {
15007
15262
  "content-type": "application/json",
15008
- authorization: `Bearer ${token}`
15263
+ ...authHeaders(token)
15009
15264
  },
15010
15265
  body: JSON.stringify({
15011
15266
  projectId: input.projectId,
@@ -15036,7 +15291,7 @@ async function createStorage(apiUrl, token, input) {
15036
15291
  method: "POST",
15037
15292
  headers: {
15038
15293
  "content-type": "application/json",
15039
- authorization: `Bearer ${token}`
15294
+ ...authHeaders(token)
15040
15295
  },
15041
15296
  body: JSON.stringify({ projectId: input.projectId })
15042
15297
  });
@@ -15058,7 +15313,7 @@ async function enableAi(apiUrl, token, input) {
15058
15313
  method: "POST",
15059
15314
  headers: {
15060
15315
  "content-type": "application/json",
15061
- authorization: `Bearer ${token}`
15316
+ ...authHeaders(token)
15062
15317
  },
15063
15318
  body: JSON.stringify({ projectId: input.projectId })
15064
15319
  });
@@ -15077,7 +15332,7 @@ async function enableAi(apiUrl, token, input) {
15077
15332
  }
15078
15333
  async function getAiStatus(apiUrl, token, projectId) {
15079
15334
  const res = await fetch(`${apiUrl}/ai/${encodeURIComponent(projectId)}`, {
15080
- headers: { authorization: `Bearer ${token}` }
15335
+ headers: { ...authHeaders(token) }
15081
15336
  });
15082
15337
  if (!res.ok) {
15083
15338
  throw new ApiError(
@@ -15095,7 +15350,7 @@ async function getAiStatus(apiUrl, token, projectId) {
15095
15350
  async function removeAi(apiUrl, token, projectId) {
15096
15351
  const res = await fetch(`${apiUrl}/ai/${encodeURIComponent(projectId)}`, {
15097
15352
  method: "DELETE",
15098
- headers: { authorization: `Bearer ${token}` }
15353
+ headers: { ...authHeaders(token) }
15099
15354
  });
15100
15355
  if (!res.ok) {
15101
15356
  throw new ApiError(
@@ -15117,7 +15372,7 @@ async function addWorkflows(apiUrl, token, input) {
15117
15372
  method: "POST",
15118
15373
  headers: {
15119
15374
  "content-type": "application/json",
15120
- authorization: `Bearer ${token}`
15375
+ ...authHeaders(token)
15121
15376
  },
15122
15377
  body: JSON.stringify({ projectId: input.projectId })
15123
15378
  });
@@ -15132,7 +15387,7 @@ async function addWorkflows(apiUrl, token, input) {
15132
15387
  }
15133
15388
  async function getWorkflowStatus(apiUrl, token, projectId) {
15134
15389
  const res = await fetch(`${apiUrl}/workflows/${encodeURIComponent(projectId)}`, {
15135
- headers: { authorization: `Bearer ${token}` }
15390
+ headers: { ...authHeaders(token) }
15136
15391
  });
15137
15392
  if (!res.ok) {
15138
15393
  throw new ApiError(
@@ -15146,7 +15401,7 @@ async function getWorkflowStatus(apiUrl, token, projectId) {
15146
15401
  async function removeWorkflows(apiUrl, token, projectId) {
15147
15402
  const res = await fetch(`${apiUrl}/workflows/${encodeURIComponent(projectId)}`, {
15148
15403
  method: "DELETE",
15149
- headers: { authorization: `Bearer ${token}` }
15404
+ headers: { ...authHeaders(token) }
15150
15405
  });
15151
15406
  if (!res.ok) {
15152
15407
  throw new ApiError(
@@ -15197,7 +15452,7 @@ function domainsUrl(apiUrl, projectId) {
15197
15452
  }
15198
15453
  async function listDomains(apiUrl, token, projectId) {
15199
15454
  const res = await fetch(domainsUrl(apiUrl, projectId), {
15200
- headers: { authorization: `Bearer ${token}` }
15455
+ headers: { ...authHeaders(token) }
15201
15456
  });
15202
15457
  if (!res.ok) {
15203
15458
  throw new ApiError(`Domain listing failed (${res.status})`, await parseEnvelope(res), res.status);
@@ -15214,7 +15469,7 @@ async function listDomains(apiUrl, token, projectId) {
15214
15469
  async function addDomain(apiUrl, token, projectId, host, app, redirectTo) {
15215
15470
  const res = await fetch(domainsUrl(apiUrl, projectId), {
15216
15471
  method: "POST",
15217
- headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
15472
+ headers: { "content-type": "application/json", ...authHeaders(token) },
15218
15473
  body: JSON.stringify({ host, ...app ? { app } : {}, ...redirectTo !== void 0 ? { redirectTo } : {} })
15219
15474
  });
15220
15475
  if (!res.ok) {
@@ -15225,7 +15480,7 @@ async function addDomain(apiUrl, token, projectId, host, app, redirectTo) {
15225
15480
  async function removeDomain(apiUrl, token, projectId, host) {
15226
15481
  const res = await fetch(`${domainsUrl(apiUrl, projectId)}/${encodeURIComponent(host)}`, {
15227
15482
  method: "DELETE",
15228
- headers: { authorization: `Bearer ${token}` }
15483
+ headers: { ...authHeaders(token) }
15229
15484
  });
15230
15485
  if (!res.ok) {
15231
15486
  throw new ApiError(`Domain removal failed (${res.status})`, await parseEnvelope(res), res.status);
@@ -15237,7 +15492,7 @@ function deployHookUrl(apiUrl, projectId) {
15237
15492
  async function createDeployHook(apiUrl, token, projectId) {
15238
15493
  const res = await fetch(deployHookUrl(apiUrl, projectId), {
15239
15494
  method: "PUT",
15240
- headers: { authorization: `Bearer ${token}` }
15495
+ headers: { ...authHeaders(token) }
15241
15496
  });
15242
15497
  if (!res.ok) {
15243
15498
  throw new ApiError(`Deploy hook creation failed (${res.status})`, await parseEnvelope(res), res.status);
@@ -15251,7 +15506,7 @@ async function createDeployHook(apiUrl, token, projectId) {
15251
15506
  async function removeDeployHook(apiUrl, token, projectId) {
15252
15507
  const res = await fetch(deployHookUrl(apiUrl, projectId), {
15253
15508
  method: "DELETE",
15254
- headers: { authorization: `Bearer ${token}` }
15509
+ headers: { ...authHeaders(token) }
15255
15510
  });
15256
15511
  if (!res.ok) {
15257
15512
  throw new ApiError(`Deploy hook removal failed (${res.status})`, await parseEnvelope(res), res.status);
@@ -15264,7 +15519,7 @@ async function removeDeployHook(apiUrl, token, projectId) {
15264
15519
  }
15265
15520
  async function fetchResourceStatus(apiUrl, token, path) {
15266
15521
  const res = await fetch(`${apiUrl}${path}`, {
15267
- headers: { authorization: `Bearer ${token}` }
15522
+ headers: { ...authHeaders(token) }
15268
15523
  });
15269
15524
  if (!res.ok) {
15270
15525
  throw new ApiError(
@@ -15347,7 +15602,7 @@ async function pollResourceDetached(fetchStatus, opts) {
15347
15602
  async function detachDatabase(apiUrl, token, projectId) {
15348
15603
  const res = await fetch(
15349
15604
  `${apiUrl}/databases/${encodeURIComponent(projectId)}`,
15350
- { method: "DELETE", headers: { authorization: `Bearer ${token}` } }
15605
+ { method: "DELETE", headers: { ...authHeaders(token) } }
15351
15606
  );
15352
15607
  if (!res.ok) {
15353
15608
  throw new ApiError(
@@ -15359,7 +15614,7 @@ async function detachDatabase(apiUrl, token, projectId) {
15359
15614
  }
15360
15615
  async function getProject(apiUrl, token, projectId) {
15361
15616
  const res = await fetch(`${apiUrl}/projects/${encodeURIComponent(projectId)}`, {
15362
- headers: { authorization: `Bearer ${token}` }
15617
+ headers: { ...authHeaders(token) }
15363
15618
  });
15364
15619
  if (res.status === 404) return null;
15365
15620
  if (!res.ok) {
@@ -15369,7 +15624,7 @@ async function getProject(apiUrl, token, projectId) {
15369
15624
  }
15370
15625
  async function listProjects(apiUrl, token) {
15371
15626
  const res = await fetch(`${apiUrl}/projects`, {
15372
- headers: { authorization: `Bearer ${token}` }
15627
+ headers: { ...authHeaders(token) }
15373
15628
  });
15374
15629
  if (!res.ok) {
15375
15630
  throw new ApiError(`Listing projects failed (${res.status})`, await parseEnvelope(res), res.status);
@@ -15379,7 +15634,7 @@ async function listProjects(apiUrl, token) {
15379
15634
  async function deleteProject(apiUrl, token, projectId) {
15380
15635
  const res = await fetch(`${apiUrl}/projects/${encodeURIComponent(projectId)}`, {
15381
15636
  method: "DELETE",
15382
- headers: { authorization: `Bearer ${token}` }
15637
+ headers: { ...authHeaders(token) }
15383
15638
  });
15384
15639
  if (!res.ok) {
15385
15640
  throw new ApiError(`Project delete failed (${res.status})`, await parseEnvelope(res), res.status);
@@ -15412,7 +15667,7 @@ async function pollProjectDeleted(read, opts) {
15412
15667
  async function detachStorage(apiUrl, token, projectId) {
15413
15668
  const res = await fetch(`${apiUrl}/storage/${encodeURIComponent(projectId)}`, {
15414
15669
  method: "DELETE",
15415
- headers: { authorization: `Bearer ${token}` }
15670
+ headers: { ...authHeaders(token) }
15416
15671
  });
15417
15672
  if (!res.ok) {
15418
15673
  throw new ApiError(
@@ -15425,7 +15680,7 @@ async function detachStorage(apiUrl, token, projectId) {
15425
15680
  async function listEnvVars(apiUrl, token, projectId, include = "all") {
15426
15681
  const res = await fetch(
15427
15682
  `${apiUrl}/projects/${encodeURIComponent(projectId)}/env?scope=${encodeURIComponent(include)}`,
15428
- { headers: { authorization: `Bearer ${token}` } }
15683
+ { headers: { ...authHeaders(token) } }
15429
15684
  );
15430
15685
  if (!res.ok) {
15431
15686
  throw new ApiError(
@@ -15478,7 +15733,7 @@ async function pullEnvValues(apiUrl, token, projectId, environment = "production
15478
15733
  const query = environment === "production" ? "" : `?environment=${encodeURIComponent(environment)}`;
15479
15734
  const res = await fetch(
15480
15735
  `${apiUrl}/projects/${encodeURIComponent(projectId)}/env/values${query}`,
15481
- { headers: { authorization: `Bearer ${token}` } }
15736
+ { headers: { ...authHeaders(token) } }
15482
15737
  );
15483
15738
  if (!res.ok) {
15484
15739
  throw new ApiError(
@@ -15512,7 +15767,7 @@ async function setEnvVar(apiUrl, token, projectId, key, value, scope = "runtime"
15512
15767
  method: "PUT",
15513
15768
  headers: {
15514
15769
  "content-type": "application/json",
15515
- authorization: `Bearer ${token}`
15770
+ ...authHeaders(token)
15516
15771
  },
15517
15772
  // `scope`/`environment` are sent only when NOT their defaults. An api older than KRYD-156/159
15518
15773
  // ignores unknown body fields, so this is belt-and-braces — but it keeps a plain runtime
@@ -15553,7 +15808,7 @@ async function removeEnvVar(apiUrl, token, projectId, key, scope = "runtime", en
15553
15808
  const query = params.toString() ? `?${params.toString()}` : "";
15554
15809
  const res = await fetch(
15555
15810
  `${apiUrl}/projects/${encodeURIComponent(projectId)}/env/${encodeURIComponent(key)}${query}`,
15556
- { method: "DELETE", headers: { authorization: `Bearer ${token}` } }
15811
+ { method: "DELETE", headers: { ...authHeaders(token) } }
15557
15812
  );
15558
15813
  if (!res.ok) {
15559
15814
  throw new ApiError(
@@ -15579,7 +15834,7 @@ async function triggerDeploy(apiUrl, token, projectId) {
15579
15834
  method: "POST",
15580
15835
  headers: {
15581
15836
  "content-type": "application/json",
15582
- authorization: `Bearer ${token}`
15837
+ ...authHeaders(token)
15583
15838
  },
15584
15839
  body: JSON.stringify({ projectId })
15585
15840
  });
@@ -15597,7 +15852,7 @@ async function rollbackDeploy(apiUrl, token, projectId, deploymentId, app) {
15597
15852
  method: "POST",
15598
15853
  headers: {
15599
15854
  "content-type": "application/json",
15600
- authorization: `Bearer ${token}`
15855
+ ...authHeaders(token)
15601
15856
  },
15602
15857
  body: JSON.stringify({
15603
15858
  projectId,
@@ -15615,7 +15870,7 @@ async function redeployProject(apiUrl, token, projectId, app) {
15615
15870
  method: "POST",
15616
15871
  headers: {
15617
15872
  "content-type": "application/json",
15618
- authorization: `Bearer ${token}`
15873
+ ...authHeaders(token)
15619
15874
  },
15620
15875
  body: JSON.stringify({ projectId, ...app ? { app } : {} })
15621
15876
  });
@@ -15627,7 +15882,7 @@ async function redeployProject(apiUrl, token, projectId, app) {
15627
15882
  async function listDeployments(apiUrl, token, projectId) {
15628
15883
  const url2 = new URL(`${apiUrl}/deployments`);
15629
15884
  if (projectId) url2.searchParams.set("projectId", projectId);
15630
- const res = await fetch(url2, { headers: { authorization: `Bearer ${token}` } });
15885
+ const res = await fetch(url2, { headers: { ...authHeaders(token) } });
15631
15886
  if (!res.ok) {
15632
15887
  throw new ApiError(`Listing deploys failed (${res.status})`, await parseEnvelope(res), res.status);
15633
15888
  }
@@ -15731,7 +15986,7 @@ async function streamDeploy(apiUrl, token, deploymentId, handlers, opts = {}) {
15731
15986
  let dropped = false;
15732
15987
  try {
15733
15988
  const res = await doFetch(`${apiUrl}/deployments/${deploymentId}/logs`, {
15734
- headers: { authorization: `Bearer ${token}`, accept: "text/event-stream" }
15989
+ headers: { ...authHeaders(token), accept: "text/event-stream" }
15735
15990
  });
15736
15991
  if (!res.ok) {
15737
15992
  throw new ApiError(`Log stream failed (${res.status})`, await parseEnvelope(res), res.status);
@@ -15773,7 +16028,7 @@ async function streamRuntime(apiUrl, token, id, handlers, opts = {}) {
15773
16028
  if (afterNs) url2.searchParams.set("after", afterNs);
15774
16029
  else if (opts.since) url2.searchParams.set("since", opts.since);
15775
16030
  const res = await doFetch(url2, {
15776
- headers: { authorization: `Bearer ${token}`, accept: "text/event-stream" },
16031
+ headers: { ...authHeaders(token), accept: "text/event-stream" },
15777
16032
  ...opts.signal ? { signal: opts.signal } : {}
15778
16033
  });
15779
16034
  if (!res.ok) {
@@ -15803,7 +16058,7 @@ async function streamRuntime(apiUrl, token, id, handlers, opts = {}) {
15803
16058
  async function fetchDeployLog(apiUrl, token, deploymentId, opts = {}) {
15804
16059
  const doFetch = opts.fetchImpl ?? fetch;
15805
16060
  const res = await doFetch(`${apiUrl}/deployments/${deploymentId}/log`, {
15806
- headers: { authorization: `Bearer ${token}` }
16061
+ headers: { ...authHeaders(token) }
15807
16062
  });
15808
16063
  if (res.status === 404) return null;
15809
16064
  if (!res.ok) {
@@ -15818,7 +16073,7 @@ async function fetchDeployLog(apiUrl, token, deploymentId, opts = {}) {
15818
16073
  async function fetchPushCredential(apiUrl, token, projectId) {
15819
16074
  const res = await fetch(
15820
16075
  `${apiUrl}/repos/${encodeURIComponent(projectId)}/push-credential`,
15821
- { headers: { authorization: `Bearer ${token}` } }
16076
+ { headers: { ...authHeaders(token) } }
15822
16077
  );
15823
16078
  if (!res.ok) {
15824
16079
  throw new ApiError(
@@ -15847,7 +16102,7 @@ async function supportFetch(apiUrl, token, path, what, init = {}) {
15847
16102
  const res = await fetch(`${apiUrl}${path}`, {
15848
16103
  method: init.method ?? "GET",
15849
16104
  headers: {
15850
- authorization: `Bearer ${token}`,
16105
+ ...authHeaders(token),
15851
16106
  ...init.body !== void 0 ? { "content-type": "application/json" } : {}
15852
16107
  },
15853
16108
  ...init.body !== void 0 ? { body: JSON.stringify(init.body) } : {}
@@ -15897,181 +16152,6 @@ async function replySupportTicket(apiUrl, token, ref, text) {
15897
16152
  return body;
15898
16153
  }
15899
16154
 
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");
15916
- }
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
- }
15933
- }
15934
- function writeConfig(config2) {
15935
- const path = configPath();
15936
- mkdirSync(dirname(path), { recursive: true, mode: 448 });
15937
- writeFileSync(path, JSON.stringify(config2, null, 2) + "\n", { mode: 384 });
15938
- chmodSync(path, 384);
15939
- }
15940
- function saveToken(token, apiUrl) {
15941
- const config2 = loadConfig();
15942
- config2.token = token;
15943
- if (apiUrl) config2.apiUrl = apiUrl;
15944
- writeConfig(config2);
15945
- }
15946
- function clearToken() {
15947
- const config2 = loadConfig();
15948
- delete config2.token;
15949
- writeConfig(config2);
15950
- }
15951
- var lastApiUrl = null;
15952
- function resolveApiUrl(flag) {
15953
- lastApiUrl = flag ?? process.env.KRYD_API_URL ?? loadConfig().apiUrl ?? DEFAULT_API_URL;
15954
- return lastApiUrl;
15955
- }
15956
- function currentApiUrl() {
15957
- return lastApiUrl ?? resolveApiUrl();
15958
- }
15959
- var DEFAULT_DASHBOARD_URL = "https://app.kryd.eu";
15960
- function resolveDashboardUrl(flag) {
15961
- return flag ?? process.env.KRYD_DASHBOARD_URL ?? DEFAULT_DASHBOARD_URL;
15962
- }
15963
- var PROJECT_LINK_DIR = ".kryd";
15964
- var PROJECT_LINK_FILE = "project.json";
15965
- function findProjectLinkDir(cwd = process.cwd()) {
15966
- let dir = cwd;
15967
- for (; ; ) {
15968
- if (existsSync(join(dir, PROJECT_LINK_DIR, PROJECT_LINK_FILE))) return dir;
15969
- if (existsSync(join(dir, ".git"))) return null;
15970
- const parent = dirname(dir);
15971
- if (parent === dir) return null;
15972
- dir = parent;
15973
- }
15974
- }
15975
- function loadProjectLink(cwd = process.cwd()) {
15976
- const root = findProjectLinkDir(cwd);
15977
- if (!root) return null;
15978
- const path = join(root, PROJECT_LINK_DIR, PROJECT_LINK_FILE);
15979
- let raw;
15980
- try {
15981
- raw = readFileSync(path, "utf8");
15982
- } catch {
15983
- return null;
15984
- }
15985
- let parsed;
15986
- try {
15987
- parsed = JSON.parse(raw);
15988
- } catch {
15989
- process.stderr.write(`warning: ${path} is not valid JSON \u2014 ignoring it.
15990
- `);
15991
- return null;
15992
- }
15993
- if (!parsed || typeof parsed.projectId !== "string" || parsed.projectId.length === 0) {
15994
- process.stderr.write(`warning: ${path} has no projectId \u2014 ignoring it.
15995
- `);
15996
- return null;
15997
- }
15998
- return parsed;
15999
- }
16000
- function saveProjectLink(cwd, link) {
16001
- const dir = join(cwd, PROJECT_LINK_DIR);
16002
- mkdirSync(dir, { recursive: true });
16003
- const path = join(dir, PROJECT_LINK_FILE);
16004
- writeFileSync(path, JSON.stringify(link, null, 2) + "\n");
16005
- return path;
16006
- }
16007
- function ensureIgnored(cwd, entry, aliases = []) {
16008
- const path = join(cwd, ".gitignore");
16009
- let current = "";
16010
- try {
16011
- current = readFileSync(path, "utf8");
16012
- } catch {
16013
- }
16014
- const present = current.split("\n").map((l) => l.trim()).some((l) => l === entry || aliases.includes(l));
16015
- if (present) return false;
16016
- const prefix = current.length > 0 && !current.endsWith("\n") ? "\n" : "";
16017
- appendFileSync(path, `${prefix}${entry}
16018
- `);
16019
- return true;
16020
- }
16021
- function ensureKrydIgnored(cwd) {
16022
- ensureIgnored(cwd, `${PROJECT_LINK_DIR}/`, [PROJECT_LINK_DIR]);
16023
- }
16024
- function directoryIsLinked(cwd) {
16025
- const path = join(cwd, PROJECT_LINK_DIR, PROJECT_LINK_FILE);
16026
- if (!existsSync(path)) return null;
16027
- return loadProjectLink(cwd);
16028
- }
16029
- function writeSecretFile(path, contents) {
16030
- mkdirSync(dirname(path), { recursive: true });
16031
- writeFileSync(path, contents, { mode: 384 });
16032
- chmodSync(path, 384);
16033
- }
16034
- function clearProjectLinkIfMatches(projectId, cwd = process.cwd()) {
16035
- const root = findProjectLinkDir(cwd);
16036
- if (!root) return false;
16037
- const link = loadProjectLink(cwd);
16038
- if (link?.projectId !== projectId) return false;
16039
- try {
16040
- rmSync(join(root, PROJECT_LINK_DIR, PROJECT_LINK_FILE));
16041
- return true;
16042
- } catch {
16043
- return false;
16044
- }
16045
- }
16046
- function resolvedLink() {
16047
- return lastLinkResolution;
16048
- }
16049
- var lastLinkResolution = null;
16050
- function resolveProjectId(explicit, cwd = process.cwd()) {
16051
- lastLinkResolution = null;
16052
- if (explicit) return explicit;
16053
- const root = findProjectLinkDir(cwd);
16054
- if (!root) return null;
16055
- const link = loadProjectLink(cwd);
16056
- if (!link) return null;
16057
- lastLinkResolution = { root, link };
16058
- return link.projectId;
16059
- }
16060
- function rememberLinkAccount(root, account) {
16061
- if (!account.accountId) return false;
16062
- const link = directoryIsLinked(root);
16063
- if (!link) return false;
16064
- const slug = account.accountSlug ?? link.accountSlug;
16065
- if (link.accountId === account.accountId && link.accountSlug === slug) return false;
16066
- saveProjectLink(root, {
16067
- ...link,
16068
- accountId: account.accountId,
16069
- // `exactOptionalPropertyTypes`: an absent key and an explicit `undefined` are different types.
16070
- ...slug ? { accountSlug: slug } : {}
16071
- });
16072
- return true;
16073
- }
16074
-
16075
16155
  // src/support.ts
16076
16156
  var SUPPORT_TYPES = TICKET_TYPES;
16077
16157
  var MESSAGE_MAX = 2e4;
@@ -16156,7 +16236,7 @@ function reportSupportError(err) {
16156
16236
  process.exitCode = 1;
16157
16237
  }
16158
16238
  function requireToken() {
16159
- const token = loadConfig().token;
16239
+ const token = resolveToken();
16160
16240
  if (!token) usage("Not logged in \u2014 run `kryd login` first.");
16161
16241
  return token ?? null;
16162
16242
  }
@@ -16308,6 +16388,272 @@ async function runSupportReply(raw, opts) {
16308
16388
  }
16309
16389
  }
16310
16390
 
16391
+ // src/account.ts
16392
+ function fail(message) {
16393
+ process.stderr.write(`${message}
16394
+ `);
16395
+ process.exitCode = 1;
16396
+ }
16397
+ function membershipLabel(m) {
16398
+ return m.slug ?? m.name ?? m.accountId;
16399
+ }
16400
+ function accountHint(err) {
16401
+ if (!(err instanceof ApiError) || !err.envelope) return "";
16402
+ const { code, details } = err.envelope.error;
16403
+ if (code === ERROR_CODES.accountRequired) {
16404
+ return "You belong to several accounts. Choose one with `kryd account use <slug>` (`kryd account list` shows them).\n";
16405
+ }
16406
+ if (code === ERROR_CODES.permissionRequired) {
16407
+ const permission = typeof details?.permission === "string" ? ` \`${details.permission}\`` : " this permission";
16408
+ const slug = loadConfig().accountSlug;
16409
+ return `Your role in ${slug ? `account ${slug}` : "this account"} lacks${permission}. Ask the account's owner, or switch with \`kryd account use <slug>\`.
16410
+ `;
16411
+ }
16412
+ const stored = loadConfig();
16413
+ if (code === ERROR_CODES.forbidden && stored.account && /not a member/i.test(err.envelope.error.message)) {
16414
+ const name = stored.accountSlug ?? stored.account;
16415
+ 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>\`.
16416
+ `;
16417
+ }
16418
+ return "";
16419
+ }
16420
+ async function runAccountList(opts = {}) {
16421
+ const token = resolveToken();
16422
+ if (!token) return fail("Not logged in \u2014 run `kryd login` first.");
16423
+ let memberships;
16424
+ let current;
16425
+ try {
16426
+ const me = await whoami(resolveApiUrl(opts.apiUrl), token);
16427
+ memberships = me.memberships ?? [];
16428
+ current = me.accountId;
16429
+ } catch (err) {
16430
+ return fail(err instanceof ApiError && err.envelope ? err.envelope.error.message : String(err));
16431
+ }
16432
+ if (opts.json) {
16433
+ process.stdout.write(`${JSON.stringify(memberships.map((m) => ({ ...m, current: m.accountId === current })), null, 2)}
16434
+ `);
16435
+ return;
16436
+ }
16437
+ if (memberships.length === 0) {
16438
+ process.stdout.write("You do not belong to any account.\n");
16439
+ return;
16440
+ }
16441
+ const rows = [
16442
+ ["", "SLUG", "NAME", "ROLE", "ID"],
16443
+ ...memberships.map((m) => [m.accountId === current ? "*" : "", m.slug ?? "\u2014", m.name, m.role, m.accountId])
16444
+ ];
16445
+ const widths = rows[0].map((_, i) => Math.max(...rows.map((r) => r[i].length)));
16446
+ for (const r of rows) {
16447
+ process.stdout.write(`${r.map((c, i) => c.padEnd(widths[i])).join(" ").trimEnd()}
16448
+ `);
16449
+ }
16450
+ }
16451
+ async function runAccountUse(target, opts = {}) {
16452
+ const token = resolveToken();
16453
+ if (!token) return fail("Not logged in \u2014 run `kryd login` first.");
16454
+ if (token.startsWith("kryd_")) {
16455
+ return fail("A deploy token is bound to one account and cannot switch. Unset KRYD_TOKEN to use your login.");
16456
+ }
16457
+ let memberships;
16458
+ try {
16459
+ memberships = (await whoami(resolveApiUrl(opts.apiUrl), token)).memberships ?? [];
16460
+ } catch (err) {
16461
+ return fail(err instanceof ApiError && err.envelope ? err.envelope.error.message : String(err));
16462
+ }
16463
+ const wanted = target.trim();
16464
+ const match = memberships.find((m) => m.slug === wanted || m.accountId === wanted);
16465
+ if (!match) {
16466
+ const known = memberships.map(membershipLabel).join(", ") || "none";
16467
+ return fail(`You are not a member of an account "${wanted}". Your accounts: ${known}.`);
16468
+ }
16469
+ saveAccount({ accountId: match.accountId, slug: match.slug });
16470
+ process.stdout.write(`Now using account ${membershipLabel(match)} (${match.role}).
16471
+ `);
16472
+ }
16473
+ async function settleAccountAfterLogin(apiUrl, token) {
16474
+ if (token.startsWith("kryd_")) return;
16475
+ let memberships;
16476
+ try {
16477
+ memberships = (await whoami(apiUrl, token)).memberships ?? [];
16478
+ } catch {
16479
+ return;
16480
+ }
16481
+ const stored = loadConfig().account;
16482
+ if (stored && memberships.some((m) => m.accountId === stored)) return;
16483
+ if (memberships.length <= 1) {
16484
+ if (stored) clearAccount();
16485
+ return;
16486
+ }
16487
+ const pick2 = memberships.find((m) => m.role === "owner") ?? memberships[0];
16488
+ saveAccount({ accountId: pick2.accountId, slug: pick2.slug });
16489
+ process.stdout.write(
16490
+ `You belong to ${memberships.length} accounts; using ${membershipLabel(pick2)}. Switch with \`kryd account use <slug>\`.
16491
+ `
16492
+ );
16493
+ }
16494
+
16495
+ // src/tokens.ts
16496
+ var TOKEN_EXPIRY_CHOICES = ["30", "90", "365", "never"];
16497
+ var DEFAULT_TOKEN_EXPIRY = "90";
16498
+ var MAX_TOKEN_NAME = 32;
16499
+ function expiresInSeconds(choice) {
16500
+ if (choice === "never") return null;
16501
+ if (!TOKEN_EXPIRY_CHOICES.includes(choice)) return void 0;
16502
+ return Number(choice) * 24 * 60 * 60;
16503
+ }
16504
+ async function tokenFetch(apiUrl, token, path, what, body) {
16505
+ const res = await fetch(`${apiUrl}/api/auth/api-key/${path}`, {
16506
+ method: body === void 0 ? "GET" : "POST",
16507
+ headers: {
16508
+ ...authHeaders(token),
16509
+ ...body !== void 0 ? { "content-type": "application/json" } : {}
16510
+ },
16511
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {}
16512
+ });
16513
+ if (!res.ok) {
16514
+ const parsed = await res.json().catch(() => void 0);
16515
+ const message = typeof parsed?.message === "string" ? parsed.message : `${what} failed (${res.status})`;
16516
+ const code = typeof parsed?.code === "string" ? parsed.code : `http.${res.status}`;
16517
+ throw new ApiError(`${what} failed (${res.status})`, { error: { code, message } }, res.status);
16518
+ }
16519
+ try {
16520
+ return await res.json();
16521
+ } catch {
16522
+ throw new ApiError(`The API returned an unexpected answer to "${what.toLowerCase()}".`);
16523
+ }
16524
+ }
16525
+ var isToken = (v) => {
16526
+ const t = v;
16527
+ return typeof t?.id === "string" && typeof t?.createdAt === "string";
16528
+ };
16529
+ async function createDeployToken(apiUrl, token, input) {
16530
+ const body = await tokenFetch(apiUrl, token, "create", "Creating the token", input);
16531
+ if (!isToken(body) || typeof body.key !== "string" || body.key.length === 0) {
16532
+ throw new ApiError("The API returned an unexpected answer to creating the token.");
16533
+ }
16534
+ return body;
16535
+ }
16536
+ async function listDeployTokens(apiUrl, token) {
16537
+ const body = await tokenFetch(apiUrl, token, "list", "Listing the tokens");
16538
+ if (!Array.isArray(body?.apiKeys) || !body.apiKeys.every(isToken)) {
16539
+ throw new ApiError("The API returned an unexpected answer to listing the tokens.");
16540
+ }
16541
+ return body.apiKeys;
16542
+ }
16543
+ async function revokeDeployToken(apiUrl, token, id) {
16544
+ await tokenFetch(apiUrl, token, "delete", "Revoking the token", { keyId: id });
16545
+ }
16546
+ function fail2(message) {
16547
+ process.stderr.write(`${message}
16548
+ `);
16549
+ process.exitCode = 1;
16550
+ }
16551
+ function reportTokenError(err) {
16552
+ if (err instanceof ApiError && err.status === 401) {
16553
+ fail2("Not logged in, or the session expired \u2014 run `kryd login`.");
16554
+ } else if (err instanceof ApiError && err.status === 403 && resolveToken()?.startsWith("kryd_")) {
16555
+ fail2(
16556
+ "A deploy token cannot manage tokens. Sign in with `kryd login` (and unset KRYD_TOKEN for this command)."
16557
+ );
16558
+ } else if (err instanceof ApiError && err.status === 404) {
16559
+ fail2("No such token on your account. `kryd token list` shows their ids.");
16560
+ } else if (err instanceof ApiError && err.envelope) {
16561
+ fail2(err.envelope.error.message);
16562
+ process.stderr.write(accountHint(err));
16563
+ } else {
16564
+ fail2(err instanceof Error ? err.message : String(err));
16565
+ }
16566
+ }
16567
+ function requireToken2() {
16568
+ const token = resolveToken();
16569
+ if (!token) fail2("Not logged in \u2014 run `kryd login` first.");
16570
+ return token ?? null;
16571
+ }
16572
+ var day = (iso) => iso ? iso.slice(0, 10) : "\u2014";
16573
+ async function runTokenCreate(name, opts) {
16574
+ const trimmed = name.trim();
16575
+ if (trimmed.length === 0 || trimmed.length > MAX_TOKEN_NAME) {
16576
+ return fail2(`A token name is 1 to ${MAX_TOKEN_NAME} characters.`);
16577
+ }
16578
+ const choice = opts.expires ?? DEFAULT_TOKEN_EXPIRY;
16579
+ const expiresIn = expiresInSeconds(choice);
16580
+ if (expiresIn === void 0) {
16581
+ return fail2(`--expires is one of ${TOKEN_EXPIRY_CHOICES.join(", ")} (days, or never).`);
16582
+ }
16583
+ const token = requireToken2();
16584
+ if (!token) return;
16585
+ try {
16586
+ const apiUrl = resolveApiUrl(opts.apiUrl);
16587
+ const created = await createDeployToken(apiUrl, token, { name: trimmed, expiresIn });
16588
+ process.stdout.write(`${created.key}
16589
+ `);
16590
+ const account = await boundAccountLabel(apiUrl, token, created);
16591
+ process.stderr.write(
16592
+ `Created deploy token "${trimmed}" (${created.id})${account ? ` for account ${account}` : ""}, ${created.expiresAt ? `valid until ${day(created.expiresAt)}` : "without expiry"}.
16593
+ This token will not be shown again. Store it now, e.g. as the CI secret KRYD_TOKEN.
16594
+ `
16595
+ );
16596
+ } catch (err) {
16597
+ reportTokenError(err);
16598
+ }
16599
+ }
16600
+ async function boundAccountLabel(apiUrl, token, created) {
16601
+ const id = typeof created.metadata?.accountId === "string" ? created.metadata.accountId : null;
16602
+ if (!id) return null;
16603
+ try {
16604
+ const me = await whoami(apiUrl, token);
16605
+ const m = me.memberships?.find((x) => x.accountId === id);
16606
+ return m?.slug ?? id;
16607
+ } catch {
16608
+ return id;
16609
+ }
16610
+ }
16611
+ async function runTokenList(opts) {
16612
+ const token = requireToken2();
16613
+ if (!token) return;
16614
+ try {
16615
+ const list = await listDeployTokens(resolveApiUrl(opts.apiUrl), token);
16616
+ if (opts.json) {
16617
+ process.stdout.write(`${JSON.stringify(list, null, 2)}
16618
+ `);
16619
+ return;
16620
+ }
16621
+ if (list.length === 0) {
16622
+ process.stdout.write("No deploy tokens. Create one with `kryd token create <name>`.\n");
16623
+ return;
16624
+ }
16625
+ const rows = [
16626
+ ["ID", "NAME", "STARTS WITH", "CREATED", "LAST USED", "EXPIRES"],
16627
+ ...list.map((t) => [
16628
+ t.id,
16629
+ t.name ?? "\u2014",
16630
+ t.start ? `${t.start}\u2026` : "\u2014",
16631
+ day(t.createdAt),
16632
+ day(t.lastRequest),
16633
+ t.expiresAt ? day(t.expiresAt) : "never"
16634
+ ])
16635
+ ];
16636
+ const widths = rows[0].map((_, i) => Math.max(...rows.map((r) => r[i].length)));
16637
+ for (const r of rows) {
16638
+ process.stdout.write(`${r.map((c, i) => c.padEnd(widths[i])).join(" ").trimEnd()}
16639
+ `);
16640
+ }
16641
+ } catch (err) {
16642
+ reportTokenError(err);
16643
+ }
16644
+ }
16645
+ async function runTokenRevoke(id, opts) {
16646
+ const token = requireToken2();
16647
+ if (!token) return;
16648
+ try {
16649
+ await revokeDeployToken(resolveApiUrl(opts.apiUrl), token, id);
16650
+ process.stdout.write(`Revoked ${id}. Anything still using it now gets 401.
16651
+ `);
16652
+ } catch (err) {
16653
+ reportTokenError(err);
16654
+ }
16655
+ }
16656
+
16311
16657
  // src/browser-login.ts
16312
16658
  import { spawn } from "node:child_process";
16313
16659
  import { randomUUID } from "node:crypto";
@@ -16396,7 +16742,7 @@ async function runGitCredential(operation, opts = {}) {
16396
16742
  const input = parseCredentialInput(opts.stdin ?? "");
16397
16743
  if (input.protocol && input.protocol !== "https") return;
16398
16744
  const cwd = opts.cwd ?? process.cwd();
16399
- const token = loadConfig().token;
16745
+ const token = resolveToken();
16400
16746
  if (!token) return;
16401
16747
  const projectId = resolveProjectId(void 0, cwd);
16402
16748
  if (!projectId) return;
@@ -17588,6 +17934,7 @@ async function reportError(err) {
17588
17934
  process.stderr.write(`${err instanceof Error ? err.message : String(err)}
17589
17935
  `);
17590
17936
  }
17937
+ process.stderr.write(accountHint(err));
17591
17938
  process.exitCode = 1;
17592
17939
  if (err instanceof ApiError) {
17593
17940
  process.stderr.write(await explainMissingProject(err));
@@ -17607,7 +17954,7 @@ async function explainMissingProject(err) {
17607
17954
  if (err.status !== 404) return "";
17608
17955
  const resolution = resolvedLink();
17609
17956
  if (!resolution) return "";
17610
- const token = loadConfig().token;
17957
+ const token = resolveToken();
17611
17958
  if (!token) return "";
17612
17959
  if (await projectIsVisible(token, resolution.link.projectId) !== false) return "";
17613
17960
  let session;
@@ -17635,7 +17982,7 @@ async function backfillLinkAccount() {
17635
17982
  const resolution = resolvedLink();
17636
17983
  if (!resolution) return;
17637
17984
  if (resolution.link.accountId && resolution.link.accountSlug) return;
17638
- const token = loadConfig().token;
17985
+ const token = resolveToken();
17639
17986
  if (!token) return;
17640
17987
  if (await projectIsVisible(token, resolution.link.projectId) !== true) return;
17641
17988
  try {
@@ -17660,33 +18007,51 @@ async function runLogin(opts) {
17660
18007
  if (pastedToken) {
17661
18008
  saveToken(pastedToken, apiUrl);
17662
18009
  process.stdout.write("Token saved.\n");
18010
+ warnIfEnvTokenOverrides();
17663
18011
  return;
17664
18012
  }
17665
18013
  const { token, apiUrl: learnedApiUrl } = await browserLogin(resolveDashboardUrl(opts.dashboardUrl));
17666
18014
  saveToken(token, learnedApiUrl ?? apiUrl);
17667
18015
  process.stdout.write("Logged in.\n");
18016
+ await settleAccountAfterLogin(learnedApiUrl ?? apiUrl, token);
18017
+ warnIfEnvTokenOverrides();
17668
18018
  }
17669
18019
  async function runWhoami(opts = {}) {
17670
18020
  const apiUrl = resolveApiUrl(opts.apiUrl);
17671
- const token = loadConfig().token ?? "";
18021
+ const token = resolveToken() ?? "";
17672
18022
  try {
17673
18023
  const me = await whoami(apiUrl, token);
17674
- process.stdout.write(
17675
- `${me.email} (account ${me.accountId ?? "?"}, role ${me.role ?? "?"})
18024
+ const current = me.memberships?.find((m) => m.accountId === me.accountId);
18025
+ const label = current?.slug ?? me.accountSlug ?? me.accountId ?? "?";
18026
+ process.stdout.write(`${me.email} (account ${label}, role ${current?.role ?? me.role ?? "?"})
18027
+ `);
18028
+ const others = (me.memberships ?? []).filter((m) => m.accountId !== me.accountId);
18029
+ if (others.length > 0) {
18030
+ process.stdout.write(
18031
+ `Also in: ${others.map((m) => `${m.slug ?? m.accountId} (${m.role})`).join(", ")} \u2014 switch with \`kryd account use <slug>\`.
17676
18032
  `
17677
- );
18033
+ );
18034
+ }
17678
18035
  } catch (err) {
17679
18036
  await reportError(err);
17680
18037
  }
17681
18038
  }
18039
+ function warnIfEnvTokenOverrides() {
18040
+ if (process.env.KRYD_TOKEN?.trim()) {
18041
+ process.stderr.write(
18042
+ "KRYD_TOKEN is set in this shell and overrides the stored login; commands keep using it until you unset it.\n"
18043
+ );
18044
+ }
18045
+ }
17682
18046
  function runLogout() {
17683
18047
  clearToken();
17684
18048
  process.stdout.write("Logged out.\n");
18049
+ warnIfEnvTokenOverrides();
17685
18050
  }
17686
18051
  async function runInit(opts) {
17687
18052
  const apiUrl = resolveApiUrl(opts.apiUrl);
17688
18053
  const cwd = opts.cwd ?? process.cwd();
17689
- const token = loadConfig().token;
18054
+ const token = resolveToken();
17690
18055
  if (!token) {
17691
18056
  process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
17692
18057
  process.exitCode = 1;
@@ -17909,7 +18274,7 @@ async function replayStoredLog(apiUrl, token, deploymentId) {
17909
18274
  }
17910
18275
  async function runLogs(opts) {
17911
18276
  const apiUrl = resolveApiUrl(opts.apiUrl);
17912
- const token = loadConfig().token;
18277
+ const token = resolveToken();
17913
18278
  if (!token) {
17914
18279
  process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
17915
18280
  process.exitCode = 1;
@@ -17961,7 +18326,7 @@ async function runLogs(opts) {
17961
18326
  }
17962
18327
  async function runRuntimeLogs(opts) {
17963
18328
  const apiUrl = resolveApiUrl(opts.apiUrl);
17964
- const token = loadConfig().token;
18329
+ const token = resolveToken();
17965
18330
  if (!token) {
17966
18331
  process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
17967
18332
  process.exitCode = 1;
@@ -18018,7 +18383,7 @@ async function runRuntimeLogs(opts) {
18018
18383
  }
18019
18384
  async function runDeploy(opts) {
18020
18385
  const apiUrl = resolveApiUrl(opts.apiUrl);
18021
- const token = loadConfig().token;
18386
+ const token = resolveToken();
18022
18387
  if (!token) {
18023
18388
  process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
18024
18389
  process.exitCode = 1;
@@ -18054,7 +18419,7 @@ function renderFor(logs) {
18054
18419
  var LOGS_FLAG_HELP = "stream the full build log line by line instead of the progress display";
18055
18420
  async function runPush(opts) {
18056
18421
  const apiUrl = resolveApiUrl(opts.apiUrl);
18057
- const token = loadConfig().token;
18422
+ const token = resolveToken();
18058
18423
  if (!token) {
18059
18424
  process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
18060
18425
  process.exitCode = 1;
@@ -18206,7 +18571,7 @@ async function followApps(apiUrl, token, deployments, render, estimateFrom) {
18206
18571
  }
18207
18572
  async function runRollback(opts) {
18208
18573
  const apiUrl = resolveApiUrl(opts.apiUrl);
18209
- const token = loadConfig().token;
18574
+ const token = resolveToken();
18210
18575
  if (!token) {
18211
18576
  process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
18212
18577
  process.exitCode = 1;
@@ -18237,7 +18602,7 @@ async function runRollback(opts) {
18237
18602
  }
18238
18603
  async function runRedeploy(opts) {
18239
18604
  const apiUrl = resolveApiUrl(opts.apiUrl);
18240
- const token = loadConfig().token;
18605
+ const token = resolveToken();
18241
18606
  if (!token) {
18242
18607
  process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
18243
18608
  process.exitCode = 1;
@@ -18267,7 +18632,7 @@ ${note}
18267
18632
  }
18268
18633
  async function runDbAdd(opts) {
18269
18634
  const apiUrl = resolveApiUrl(opts.apiUrl);
18270
- const token = loadConfig().token;
18635
+ const token = resolveToken();
18271
18636
  if (!token) {
18272
18637
  process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
18273
18638
  process.exitCode = 1;
@@ -18348,7 +18713,7 @@ function reportSettleResult(result, opts) {
18348
18713
  }
18349
18714
  async function runStorageAdd(opts) {
18350
18715
  const apiUrl = resolveApiUrl(opts.apiUrl);
18351
- const token = loadConfig().token;
18716
+ const token = resolveToken();
18352
18717
  if (!token) {
18353
18718
  process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
18354
18719
  process.exitCode = 1;
@@ -18442,7 +18807,7 @@ ${listed}Re-run \`kryd project rm <id>\` with the one you mean.
18442
18807
  }
18443
18808
  async function runProjectRemove(opts) {
18444
18809
  const apiUrl = resolveApiUrl(opts.apiUrl);
18445
- const token = loadConfig().token;
18810
+ const token = resolveToken();
18446
18811
  if (!token) {
18447
18812
  process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
18448
18813
  process.exitCode = 1;
@@ -18557,7 +18922,7 @@ async function describeProjectResources(apiUrl, token, projectId) {
18557
18922
  }
18558
18923
  async function runDbRemove(opts) {
18559
18924
  const apiUrl = resolveApiUrl(opts.apiUrl);
18560
- const token = loadConfig().token;
18925
+ const token = resolveToken();
18561
18926
  if (!token) {
18562
18927
  process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
18563
18928
  process.exitCode = 1;
@@ -18596,7 +18961,7 @@ async function runDbRemove(opts) {
18596
18961
  }
18597
18962
  async function runAiAdd(opts) {
18598
18963
  const apiUrl = resolveApiUrl(opts.apiUrl);
18599
- const token = loadConfig().token;
18964
+ const token = resolveToken();
18600
18965
  if (!token) {
18601
18966
  process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
18602
18967
  process.exitCode = 1;
@@ -18621,7 +18986,7 @@ async function runAiAdd(opts) {
18621
18986
  }
18622
18987
  async function runAiRemove(opts) {
18623
18988
  const apiUrl = resolveApiUrl(opts.apiUrl);
18624
- const token = loadConfig().token;
18989
+ const token = resolveToken();
18625
18990
  if (!token) {
18626
18991
  process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
18627
18992
  process.exitCode = 1;
@@ -18688,7 +19053,7 @@ Remove it before attaching another.
18688
19053
  }
18689
19054
  async function runDbStatus(opts) {
18690
19055
  const apiUrl = resolveApiUrl(opts.apiUrl);
18691
- const token = loadConfig().token;
19056
+ const token = resolveToken();
18692
19057
  if (!token) {
18693
19058
  process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
18694
19059
  process.exitCode = 1;
@@ -18708,7 +19073,7 @@ async function runDbStatus(opts) {
18708
19073
  }
18709
19074
  async function runStorageStatus(opts) {
18710
19075
  const apiUrl = resolveApiUrl(opts.apiUrl);
18711
- const token = loadConfig().token;
19076
+ const token = resolveToken();
18712
19077
  if (!token) {
18713
19078
  process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
18714
19079
  process.exitCode = 1;
@@ -18728,7 +19093,7 @@ async function runStorageStatus(opts) {
18728
19093
  }
18729
19094
  async function runAiStatus(opts) {
18730
19095
  const apiUrl = resolveApiUrl(opts.apiUrl);
18731
- const token = loadConfig().token;
19096
+ const token = resolveToken();
18732
19097
  if (!token) {
18733
19098
  process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
18734
19099
  process.exitCode = 1;
@@ -18764,7 +19129,7 @@ function daysUntil(iso) {
18764
19129
  var TOKEN_EXPIRY_WARNING_DAYS = 30;
18765
19130
  async function runWorkflowAdd(opts) {
18766
19131
  const apiUrl = resolveApiUrl(opts.apiUrl);
18767
- const token = loadConfig().token;
19132
+ const token = resolveToken();
18768
19133
  if (!token) {
18769
19134
  process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
18770
19135
  process.exitCode = 1;
@@ -18797,7 +19162,7 @@ HATCHET_CLIENT_TOKEN will be injected on your next production deploy (kryd deplo
18797
19162
  }
18798
19163
  async function runWorkflowRemove(opts) {
18799
19164
  const apiUrl = resolveApiUrl(opts.apiUrl);
18800
- const token = loadConfig().token;
19165
+ const token = resolveToken();
18801
19166
  if (!token) {
18802
19167
  process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
18803
19168
  process.exitCode = 1;
@@ -18842,7 +19207,7 @@ Workflow history, crons and schedules stay until the project is deleted.
18842
19207
  }
18843
19208
  async function runWorkflowStatus(opts) {
18844
19209
  const apiUrl = resolveApiUrl(opts.apiUrl);
18845
- const token = loadConfig().token;
19210
+ const token = resolveToken();
18846
19211
  if (!token) {
18847
19212
  process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
18848
19213
  process.exitCode = 1;
@@ -18901,7 +19266,7 @@ function formatDomainRecords(records) {
18901
19266
  }
18902
19267
  function domainContext(opts, usage2) {
18903
19268
  const apiUrl = resolveApiUrl(opts.apiUrl);
18904
- const token = loadConfig().token;
19269
+ const token = resolveToken();
18905
19270
  if (!token) {
18906
19271
  process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
18907
19272
  process.exitCode = 1;
@@ -19308,7 +19673,7 @@ HATCHET_CLIENT_TOKEN is injected into your app and your worker on production dep
19308
19673
  }
19309
19674
  async function runStorageRemove(opts) {
19310
19675
  const apiUrl = resolveApiUrl(opts.apiUrl);
19311
- const token = loadConfig().token;
19676
+ const token = resolveToken();
19312
19677
  if (!token) {
19313
19678
  process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
19314
19679
  process.exitCode = 1;
@@ -19371,7 +19736,7 @@ function envAnnotation(environments) {
19371
19736
  }
19372
19737
  async function runProjectList(opts) {
19373
19738
  const apiUrl = resolveApiUrl(opts.apiUrl);
19374
- const token = loadConfig().token;
19739
+ const token = resolveToken();
19375
19740
  if (!token) {
19376
19741
  process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
19377
19742
  process.exitCode = 1;
@@ -19422,7 +19787,7 @@ ${rows}`;
19422
19787
  }
19423
19788
  async function runEnvList(opts) {
19424
19789
  const apiUrl = resolveApiUrl(opts.apiUrl);
19425
- const token = loadConfig().token;
19790
+ const token = resolveToken();
19426
19791
  if (!token) {
19427
19792
  process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
19428
19793
  process.exitCode = 1;
@@ -19504,7 +19869,7 @@ function toDotenvLine(key, value) {
19504
19869
  }
19505
19870
  async function runEnvPull(opts) {
19506
19871
  const apiUrl = resolveApiUrl(opts.apiUrl);
19507
- const token = loadConfig().token;
19872
+ const token = resolveToken();
19508
19873
  if (!token) {
19509
19874
  process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
19510
19875
  process.exitCode = 1;
@@ -19605,7 +19970,7 @@ async function runEnvSet(opts) {
19605
19970
  const scope = opts.build ? "build" : "runtime";
19606
19971
  const environment = opts.preview ? "preview" : "production";
19607
19972
  const apiUrl = resolveApiUrl(opts.apiUrl);
19608
- const token = loadConfig().token;
19973
+ const token = resolveToken();
19609
19974
  if (!token) {
19610
19975
  process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
19611
19976
  process.exitCode = 1;
@@ -19700,7 +20065,7 @@ async function runEnvRemove(opts) {
19700
20065
  const scope = opts.build ? "build" : "runtime";
19701
20066
  const environment = opts.preview ? "preview" : "production";
19702
20067
  const apiUrl = resolveApiUrl(opts.apiUrl);
19703
- const token = loadConfig().token;
20068
+ const token = resolveToken();
19704
20069
  if (!token) {
19705
20070
  process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
19706
20071
  process.exitCode = 1;
@@ -19794,7 +20159,7 @@ Run \`kryd env list\` to see what is set. (If you meant the runtime variable of
19794
20159
  await reportError(err);
19795
20160
  }
19796
20161
  }
19797
- var CLI_VERSION = true ? "0.14.0" : "0.0.0-dev";
20162
+ var CLI_VERSION = true ? "0.16.0" : "0.0.0-dev";
19798
20163
  var program = new Command();
19799
20164
  program.name("kryd").description("Kryd CLI").version(CLI_VERSION);
19800
20165
  program.hook("postAction", async () => {
@@ -19805,6 +20170,9 @@ program.command("login").description("Sign in via the browser (default) and stor
19805
20170
  "supply a token instead of the browser flow (prefer $KRYD_TOKEN \u2014 flags are visible in `ps`)"
19806
20171
  ).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));
19807
20172
  program.command("whoami").description("Show the current account").option("--api-url <url>", "control-plane API base URL").action((opts) => runWhoami(opts));
20173
+ var accountCommand = program.command("account").description("The accounts you belong to, and which one commands run in");
20174
+ 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));
20175
+ 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));
19808
20176
  program.command("logout").description("Clear the stored token").action(() => runLogout());
19809
20177
  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) => {
19810
20178
  const chunks = [];
@@ -19942,6 +20310,10 @@ support.command("new").description(
19942
20310
  support.command("list").description("Your account's tickets, with their status").option("--json", "print { items } as JSON on stdout").option("--api-url <url>", "control-plane API base URL").action((opts) => runSupportList(opts));
19943
20311
  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));
19944
20312
  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));
20313
+ 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");
20314
+ 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));
20315
+ 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));
20316
+ 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));
19945
20317
  function invokedDirectly() {
19946
20318
  const entry = process.argv[1];
19947
20319
  if (!entry) return false;