@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.
- package/README.md +5 -2
- package/dist/index.js +436 -230
- 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,
|
|
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: {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: {
|
|
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: {
|
|
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
|
-
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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",
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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
|
-
|
|
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: {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
16559
|
+
fail2("No such token on your account. `kryd token list` shows their ids.");
|
|
16380
16560
|
} else if (err instanceof ApiError && err.envelope) {
|
|
16381
|
-
|
|
16561
|
+
fail2(err.envelope.error.message);
|
|
16562
|
+
process.stderr.write(accountHint(err));
|
|
16382
16563
|
} else {
|
|
16383
|
-
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
17829
|
-
|
|
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.
|
|
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,
|
|
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.
|
|
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",
|