@kryd/cli 0.15.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 +5 -2
  2. package/dist/index.js +436 -230
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -31,7 +31,8 @@ Once you've run `kryd init` in a directory, the commands below work **arg-less**
31
31
 
32
32
  | Command | What it does |
33
33
  |---|---|
34
- | `kryd login` / `logout` / `whoami` | Authenticate the CLI (browser flow); the token is stored in `~/.kryd`. |
34
+ | `kryd login` / `logout` / `whoami` | Authenticate the CLI (browser flow); the token is stored in `~/.kryd`. `whoami` shows the account commands run in, your role there, and your other accounts. |
35
+ | `kryd account list \| use <slug>` | You can belong to several accounts (your own, a customer's as a member). `list` shows them with your role in each, `*` marking the current one; `use` makes every following command run in that account. |
35
36
  | `kryd init` | Link the current repo to a Kryd project + register the deploy webhook + add a `kryd` git remote. |
36
37
  | `kryd push [branch]` | Push to the `kryd` remote and follow the deploy it triggers (defaults to the current branch). Shows which step the deploy is on, how long it has taken and a progress bar; `--logs` streams the full build log instead. |
37
38
  | `kryd deploy [project]` | Re-deploy the production-branch HEAD already on the forge — no new commit. |
@@ -43,7 +44,7 @@ Once you've run `kryd init` in a directory, the commands below work **arg-less**
43
44
  | `kryd ai add \| remove \| status [project]` | Give the project an authenticated EU AI-gateway endpoint (injected on the next deploy), take it away again, or show what it has. |
44
45
  | `kryd workflow add \| remove \| status [project]` | Give the project its own durable-workflow tenant — `HATCHET_CLIENT_TOKEN` is injected on the next deploy — revoke that token again, or show whether it has one and when it expires. `remove` keeps the tenant's history, crons and schedules until the project is deleted. |
45
46
  | `kryd workflow init --language <typescript\|python\|go>` | Scaffold a workflow worker here and link it as a new project. The worker ships with the small HTTP health listener Kryd's deploy gate needs — without it a deploy is torn down with "nothing listening yet" — plus a `kryd.json` that tells `kryd init` what this project is. `--no-link` writes the files only. |
46
- | `kryd token create \| list \| revoke` | Deploy tokens for CI and scripts. `create <name> [--expires 30\|90\|365\|never]` (default 90 days) prints the token once; it acts as you, inside your account, and cannot create tokens itself. Managing tokens needs `kryd login`. A token can do what you can on your projects — read env values and push — so after a leak, revoke it and also rotate the secrets it could read. |
47
+ | `kryd token create \| list \| revoke` | Deploy tokens for CI and scripts. `create <name> [--expires 30\|90\|365\|never]` (default 90 days) prints the token once; it acts as you, with your role in the **current account**, is bound to that one account for good, and cannot create tokens itself. Managing tokens needs `kryd login`. A token can do what you can on your projects — read env values and push — so after a leak, revoke it and also rotate the secrets it could read. |
47
48
  | `kryd support new \| list \| show \| reply` | Ask Kryd support a question, report a bug or a wish, and read the answer. Non-interactive (every input is a flag) and `--json` prints one JSON document on stdout — so an agent can use it too. In a linked folder, `new` adds the project and its last production deploy; when that deploy failed, the last lines of its log are sent to Kryd support too, with secret-looking values masked. `--no-context` leaves all of it out. |
48
49
 
49
50
  For an agent, for example:
@@ -60,6 +61,8 @@ kryd token create github-actions # prints kryd_… once — store it as the
60
61
  KRYD_TOKEN=kryd_… kryd deploy <project>
61
62
  ```
62
63
 
64
+ **Several accounts.** Every request names its account in a `Kryd-Account` header: the one chosen with `kryd account use <slug>` (stored in `~/.kryd/config.json`). With nothing chosen no header is sent and the API uses your only account; if you are in several it answers `account.required` and the CLI tells you to run `kryd account use`. `kryd login` chooses for you when you are in several — the account you own, else the first you joined — and says so. A deploy token never sends the header: it runs in the account it was created in, and `kryd token create` says which. When your role lacks something, the error names the missing permission (for example `deploy`).
65
+
63
66
  Run `kryd <command> --help` for options. Every project-scoped command accepts an explicit `<project>` id, or resolves it from the `.kryd` link (walking up from the current directory, git-style).
64
67
 
65
68
  ## Sovereignty
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,185 +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 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
16155
  // src/support.ts
16080
16156
  var SUPPORT_TYPES = TICKET_TYPES;
16081
16157
  var MESSAGE_MAX = 2e4;
@@ -16312,6 +16388,110 @@ async function runSupportReply(raw, opts) {
16312
16388
  }
16313
16389
  }
16314
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
+
16315
16495
  // src/tokens.ts
16316
16496
  var TOKEN_EXPIRY_CHOICES = ["30", "90", "365", "never"];
16317
16497
  var DEFAULT_TOKEN_EXPIRY = "90";
@@ -16325,7 +16505,7 @@ async function tokenFetch(apiUrl, token, path, what, body) {
16325
16505
  const res = await fetch(`${apiUrl}/api/auth/api-key/${path}`, {
16326
16506
  method: body === void 0 ? "GET" : "POST",
16327
16507
  headers: {
16328
- authorization: `Bearer ${token}`,
16508
+ ...authHeaders(token),
16329
16509
  ...body !== void 0 ? { "content-type": "application/json" } : {}
16330
16510
  },
16331
16511
  ...body !== void 0 ? { body: JSON.stringify(body) } : {}
@@ -16363,50 +16543,53 @@ async function listDeployTokens(apiUrl, token) {
16363
16543
  async function revokeDeployToken(apiUrl, token, id) {
16364
16544
  await tokenFetch(apiUrl, token, "delete", "Revoking the token", { keyId: id });
16365
16545
  }
16366
- function fail(message) {
16546
+ function fail2(message) {
16367
16547
  process.stderr.write(`${message}
16368
16548
  `);
16369
16549
  process.exitCode = 1;
16370
16550
  }
16371
16551
  function reportTokenError(err) {
16372
16552
  if (err instanceof ApiError && err.status === 401) {
16373
- fail("Not logged in, or the session expired \u2014 run `kryd login`.");
16553
+ fail2("Not logged in, or the session expired \u2014 run `kryd login`.");
16374
16554
  } else if (err instanceof ApiError && err.status === 403 && resolveToken()?.startsWith("kryd_")) {
16375
- fail(
16555
+ fail2(
16376
16556
  "A deploy token cannot manage tokens. Sign in with `kryd login` (and unset KRYD_TOKEN for this command)."
16377
16557
  );
16378
16558
  } else if (err instanceof ApiError && err.status === 404) {
16379
- fail("No such token on your account. `kryd token list` shows their ids.");
16559
+ fail2("No such token on your account. `kryd token list` shows their ids.");
16380
16560
  } else if (err instanceof ApiError && err.envelope) {
16381
- fail(err.envelope.error.message);
16561
+ fail2(err.envelope.error.message);
16562
+ process.stderr.write(accountHint(err));
16382
16563
  } else {
16383
- fail(err instanceof Error ? err.message : String(err));
16564
+ fail2(err instanceof Error ? err.message : String(err));
16384
16565
  }
16385
16566
  }
16386
16567
  function requireToken2() {
16387
16568
  const token = resolveToken();
16388
- if (!token) fail("Not logged in \u2014 run `kryd login` first.");
16569
+ if (!token) fail2("Not logged in \u2014 run `kryd login` first.");
16389
16570
  return token ?? null;
16390
16571
  }
16391
16572
  var day = (iso) => iso ? iso.slice(0, 10) : "\u2014";
16392
16573
  async function runTokenCreate(name, opts) {
16393
16574
  const trimmed = name.trim();
16394
16575
  if (trimmed.length === 0 || trimmed.length > MAX_TOKEN_NAME) {
16395
- return fail(`A token name is 1 to ${MAX_TOKEN_NAME} characters.`);
16576
+ return fail2(`A token name is 1 to ${MAX_TOKEN_NAME} characters.`);
16396
16577
  }
16397
16578
  const choice = opts.expires ?? DEFAULT_TOKEN_EXPIRY;
16398
16579
  const expiresIn = expiresInSeconds(choice);
16399
16580
  if (expiresIn === void 0) {
16400
- return fail(`--expires is one of ${TOKEN_EXPIRY_CHOICES.join(", ")} (days, or never).`);
16581
+ return fail2(`--expires is one of ${TOKEN_EXPIRY_CHOICES.join(", ")} (days, or never).`);
16401
16582
  }
16402
16583
  const token = requireToken2();
16403
16584
  if (!token) return;
16404
16585
  try {
16405
- const created = await createDeployToken(resolveApiUrl(opts.apiUrl), token, { name: trimmed, expiresIn });
16586
+ const apiUrl = resolveApiUrl(opts.apiUrl);
16587
+ const created = await createDeployToken(apiUrl, token, { name: trimmed, expiresIn });
16406
16588
  process.stdout.write(`${created.key}
16407
16589
  `);
16590
+ const account = await boundAccountLabel(apiUrl, token, created);
16408
16591
  process.stderr.write(
16409
- `Created deploy token "${trimmed}" (${created.id}), ${created.expiresAt ? `valid until ${day(created.expiresAt)}` : "without expiry"}.
16592
+ `Created deploy token "${trimmed}" (${created.id})${account ? ` for account ${account}` : ""}, ${created.expiresAt ? `valid until ${day(created.expiresAt)}` : "without expiry"}.
16410
16593
  This token will not be shown again. Store it now, e.g. as the CI secret KRYD_TOKEN.
16411
16594
  `
16412
16595
  );
@@ -16414,6 +16597,17 @@ This token will not be shown again. Store it now, e.g. as the CI secret KRYD_TOK
16414
16597
  reportTokenError(err);
16415
16598
  }
16416
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
+ }
16417
16611
  async function runTokenList(opts) {
16418
16612
  const token = requireToken2();
16419
16613
  if (!token) return;
@@ -17740,6 +17934,7 @@ async function reportError(err) {
17740
17934
  process.stderr.write(`${err instanceof Error ? err.message : String(err)}
17741
17935
  `);
17742
17936
  }
17937
+ process.stderr.write(accountHint(err));
17743
17938
  process.exitCode = 1;
17744
17939
  if (err instanceof ApiError) {
17745
17940
  process.stderr.write(await explainMissingProject(err));
@@ -17818,6 +18013,7 @@ async function runLogin(opts) {
17818
18013
  const { token, apiUrl: learnedApiUrl } = await browserLogin(resolveDashboardUrl(opts.dashboardUrl));
17819
18014
  saveToken(token, learnedApiUrl ?? apiUrl);
17820
18015
  process.stdout.write("Logged in.\n");
18016
+ await settleAccountAfterLogin(learnedApiUrl ?? apiUrl, token);
17821
18017
  warnIfEnvTokenOverrides();
17822
18018
  }
17823
18019
  async function runWhoami(opts = {}) {
@@ -17825,10 +18021,17 @@ async function runWhoami(opts = {}) {
17825
18021
  const token = resolveToken() ?? "";
17826
18022
  try {
17827
18023
  const me = await whoami(apiUrl, token);
17828
- process.stdout.write(
17829
- `${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>\`.
17830
18032
  `
17831
- );
18033
+ );
18034
+ }
17832
18035
  } catch (err) {
17833
18036
  await reportError(err);
17834
18037
  }
@@ -19956,7 +20159,7 @@ Run \`kryd env list\` to see what is set. (If you meant the runtime variable of
19956
20159
  await reportError(err);
19957
20160
  }
19958
20161
  }
19959
- var CLI_VERSION = true ? "0.15.0" : "0.0.0-dev";
20162
+ var CLI_VERSION = true ? "0.16.0" : "0.0.0-dev";
19960
20163
  var program = new Command();
19961
20164
  program.name("kryd").description("Kryd CLI").version(CLI_VERSION);
19962
20165
  program.hook("postAction", async () => {
@@ -19967,6 +20170,9 @@ program.command("login").description("Sign in via the browser (default) and stor
19967
20170
  "supply a token instead of the browser flow (prefer $KRYD_TOKEN \u2014 flags are visible in `ps`)"
19968
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));
19969
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));
19970
20176
  program.command("logout").description("Clear the stored token").action(() => runLogout());
19971
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) => {
19972
20178
  const chunks = [];
@@ -20105,7 +20311,7 @@ support.command("list").description("Your account's tickets, with their status")
20105
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));
20106
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));
20107
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");
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));
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));
20109
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));
20110
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));
20111
20317
  function invokedDirectly() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kryd/cli",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "Kryd CLI — git push → live on the European cloud for your AI: SSL, managed Postgres & object storage, and an EU-hosted AI gateway. Your code and model calls stay in the EU. Beta: sign up at https://kryd.eu/beta",
5
5
  "keywords": [
6
6
  "kryd",