@kryd/cli 0.5.1 → 0.7.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 +4 -3
- package/dist/index.js +424 -35
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -34,10 +34,11 @@ Once you've run `kryd init` in a directory, the commands below work **arg-less**
|
|
|
34
34
|
| `kryd deploy [project]` | Re-deploy the production-branch HEAD already on the forge — no new commit. |
|
|
35
35
|
| `kryd logs [target]` | Follow a deploy's build/deploy log **in full** — this is where the build output lives (`--runtime` tails the live container instead). |
|
|
36
36
|
| `kryd rollback [project] [deployment]` | Roll back to a previous successful deploy — no rebuild. |
|
|
37
|
-
| `kryd db
|
|
38
|
-
| `kryd storage
|
|
37
|
+
| `kryd db add \| remove \| status [project]` | Attach, tear down or inspect managed Postgres (shared or bring-your-own). `remove` destroys the data and asks first. |
|
|
38
|
+
| `kryd storage add \| remove \| status [project]` | Attach, tear down or inspect S3-compatible object storage. `remove` destroys every object and asks first. |
|
|
39
39
|
| `kryd env list \| set \| rm [project]` | Manage your own environment variables. `set KEY --stdin` (or a prompt) keeps a secret out of `ps` and your shell history; values are never printed back. Add `--build` for build-time variables (`VITE_*`, `NEXT_PUBLIC_*`) — these are compiled into your public bundle, so they must never be secrets, and they take effect at your next build rather than your next deploy. |
|
|
40
|
-
| `kryd ai
|
|
40
|
+
| `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. |
|
|
41
|
+
| `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. |
|
|
41
42
|
|
|
42
43
|
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).
|
|
43
44
|
|
package/dist/index.js
CHANGED
|
@@ -14729,6 +14729,7 @@ var MANAGED_ENV_VAR_NAMES = [
|
|
|
14729
14729
|
"DATABASE_URL",
|
|
14730
14730
|
"DEPLOY_TOKEN",
|
|
14731
14731
|
"FORGE_PASSWORD",
|
|
14732
|
+
"HATCHET_CLIENT_TOKEN",
|
|
14732
14733
|
"PUSH_TOKEN",
|
|
14733
14734
|
"WEBHOOK_SECRET"
|
|
14734
14735
|
];
|
|
@@ -15266,6 +15267,87 @@ async function enableAi(apiUrl, token, input) {
|
|
|
15266
15267
|
}
|
|
15267
15268
|
return { enabled: true, url: parsed.url };
|
|
15268
15269
|
}
|
|
15270
|
+
async function getAiStatus(apiUrl, token, projectId) {
|
|
15271
|
+
const res = await fetch(`${apiUrl}/ai/${encodeURIComponent(projectId)}`, {
|
|
15272
|
+
headers: { authorization: `Bearer ${token}` }
|
|
15273
|
+
});
|
|
15274
|
+
if (!res.ok) {
|
|
15275
|
+
throw new ApiError(
|
|
15276
|
+
`AI status check failed (${res.status})`,
|
|
15277
|
+
await parseEnvelope(res),
|
|
15278
|
+
res.status
|
|
15279
|
+
);
|
|
15280
|
+
}
|
|
15281
|
+
const parsed = await res.json().catch(() => void 0);
|
|
15282
|
+
if (!parsed || typeof parsed.enabled !== "boolean" || typeof parsed.url !== "string" && parsed.url !== null) {
|
|
15283
|
+
throw new ApiError("The API returned an unexpected AI-status response.");
|
|
15284
|
+
}
|
|
15285
|
+
return { enabled: parsed.enabled, url: parsed.url };
|
|
15286
|
+
}
|
|
15287
|
+
async function removeAi(apiUrl, token, projectId) {
|
|
15288
|
+
const res = await fetch(`${apiUrl}/ai/${encodeURIComponent(projectId)}`, {
|
|
15289
|
+
method: "DELETE",
|
|
15290
|
+
headers: { authorization: `Bearer ${token}` }
|
|
15291
|
+
});
|
|
15292
|
+
if (!res.ok) {
|
|
15293
|
+
throw new ApiError(
|
|
15294
|
+
`AI removal failed (${res.status})`,
|
|
15295
|
+
await parseEnvelope(res),
|
|
15296
|
+
res.status
|
|
15297
|
+
);
|
|
15298
|
+
}
|
|
15299
|
+
}
|
|
15300
|
+
function parseWorkflowStatus(body, label) {
|
|
15301
|
+
const parsed = body;
|
|
15302
|
+
if (!parsed || typeof parsed.enabled !== "boolean" || typeof parsed.tokenExpiresAt !== "string" && parsed.tokenExpiresAt !== null) {
|
|
15303
|
+
throw new ApiError(`The API returned an unexpected ${label} response.`);
|
|
15304
|
+
}
|
|
15305
|
+
return { enabled: parsed.enabled, tokenExpiresAt: parsed.tokenExpiresAt };
|
|
15306
|
+
}
|
|
15307
|
+
async function addWorkflows(apiUrl, token, input) {
|
|
15308
|
+
const res = await fetch(`${apiUrl}/workflows`, {
|
|
15309
|
+
method: "POST",
|
|
15310
|
+
headers: {
|
|
15311
|
+
"content-type": "application/json",
|
|
15312
|
+
authorization: `Bearer ${token}`
|
|
15313
|
+
},
|
|
15314
|
+
body: JSON.stringify({ projectId: input.projectId })
|
|
15315
|
+
});
|
|
15316
|
+
if (!res.ok) {
|
|
15317
|
+
throw new ApiError(
|
|
15318
|
+
`Workflow add failed (${res.status})`,
|
|
15319
|
+
await parseEnvelope(res),
|
|
15320
|
+
res.status
|
|
15321
|
+
);
|
|
15322
|
+
}
|
|
15323
|
+
return parseWorkflowStatus(await res.json().catch(() => void 0), "workflow-add");
|
|
15324
|
+
}
|
|
15325
|
+
async function getWorkflowStatus(apiUrl, token, projectId) {
|
|
15326
|
+
const res = await fetch(`${apiUrl}/workflows/${encodeURIComponent(projectId)}`, {
|
|
15327
|
+
headers: { authorization: `Bearer ${token}` }
|
|
15328
|
+
});
|
|
15329
|
+
if (!res.ok) {
|
|
15330
|
+
throw new ApiError(
|
|
15331
|
+
`Workflow status check failed (${res.status})`,
|
|
15332
|
+
await parseEnvelope(res),
|
|
15333
|
+
res.status
|
|
15334
|
+
);
|
|
15335
|
+
}
|
|
15336
|
+
return parseWorkflowStatus(await res.json().catch(() => void 0), "workflow-status");
|
|
15337
|
+
}
|
|
15338
|
+
async function removeWorkflows(apiUrl, token, projectId) {
|
|
15339
|
+
const res = await fetch(`${apiUrl}/workflows/${encodeURIComponent(projectId)}`, {
|
|
15340
|
+
method: "DELETE",
|
|
15341
|
+
headers: { authorization: `Bearer ${token}` }
|
|
15342
|
+
});
|
|
15343
|
+
if (!res.ok) {
|
|
15344
|
+
throw new ApiError(
|
|
15345
|
+
`Workflow removal failed (${res.status})`,
|
|
15346
|
+
await parseEnvelope(res),
|
|
15347
|
+
res.status
|
|
15348
|
+
);
|
|
15349
|
+
}
|
|
15350
|
+
}
|
|
15269
15351
|
async function fetchResourceStatus(apiUrl, token, path) {
|
|
15270
15352
|
const res = await fetch(`${apiUrl}${path}`, {
|
|
15271
15353
|
headers: { authorization: `Bearer ${token}` }
|
|
@@ -16859,7 +16941,7 @@ ${note}
|
|
|
16859
16941
|
reportError(err);
|
|
16860
16942
|
}
|
|
16861
16943
|
}
|
|
16862
|
-
async function
|
|
16944
|
+
async function runDbAdd(opts) {
|
|
16863
16945
|
const apiUrl = resolveApiUrl(opts.apiUrl);
|
|
16864
16946
|
const token = loadConfig().token;
|
|
16865
16947
|
if (!token) {
|
|
@@ -16869,7 +16951,7 @@ async function runDbCreate(opts) {
|
|
|
16869
16951
|
}
|
|
16870
16952
|
const project2 = resolveProjectId(opts.project, opts.cwd);
|
|
16871
16953
|
if (!project2) {
|
|
16872
|
-
reportNotLinked("kryd db
|
|
16954
|
+
reportNotLinked("kryd db add <projectId>");
|
|
16873
16955
|
return;
|
|
16874
16956
|
}
|
|
16875
16957
|
if (opts.shared && opts.connectionString !== void 0) {
|
|
@@ -16918,7 +17000,7 @@ The connection string will be injected as DATABASE_URL on your next deploy.
|
|
|
16918
17000
|
The connection string will be injected as DATABASE_URL on your next deploy.
|
|
16919
17001
|
`,
|
|
16920
17002
|
failedLabel: "Database provisioning",
|
|
16921
|
-
retryCmd: `kryd db
|
|
17003
|
+
retryCmd: `kryd db status ${project2}`
|
|
16922
17004
|
});
|
|
16923
17005
|
} catch (err) {
|
|
16924
17006
|
reportError(err);
|
|
@@ -16940,7 +17022,7 @@ function reportSettleResult(result, opts) {
|
|
|
16940
17022
|
);
|
|
16941
17023
|
}
|
|
16942
17024
|
}
|
|
16943
|
-
async function
|
|
17025
|
+
async function runStorageAdd(opts) {
|
|
16944
17026
|
const apiUrl = resolveApiUrl(opts.apiUrl);
|
|
16945
17027
|
const token = loadConfig().token;
|
|
16946
17028
|
if (!token) {
|
|
@@ -16950,7 +17032,7 @@ async function runStorageCreate(opts) {
|
|
|
16950
17032
|
}
|
|
16951
17033
|
const project2 = resolveProjectId(opts.project, opts.cwd);
|
|
16952
17034
|
if (!project2) {
|
|
16953
|
-
reportNotLinked("kryd storage
|
|
17035
|
+
reportNotLinked("kryd storage add <projectId>");
|
|
16954
17036
|
return;
|
|
16955
17037
|
}
|
|
16956
17038
|
try {
|
|
@@ -16965,7 +17047,7 @@ async function runStorageCreate(opts) {
|
|
|
16965
17047
|
Its S3 credentials will be injected on your next deploy.
|
|
16966
17048
|
`,
|
|
16967
17049
|
failedLabel: "Storage provisioning",
|
|
16968
|
-
retryCmd: `kryd storage
|
|
17050
|
+
retryCmd: `kryd storage status ${project2}`
|
|
16969
17051
|
});
|
|
16970
17052
|
} catch (err) {
|
|
16971
17053
|
reportError(err);
|
|
@@ -16982,11 +17064,26 @@ function reportDetachResult(result, opts) {
|
|
|
16982
17064
|
process.exitCode = 1;
|
|
16983
17065
|
} else {
|
|
16984
17066
|
process.stderr.write(
|
|
16985
|
-
`Could not confirm
|
|
17067
|
+
`Could not confirm removal \u2014 still tearing down (or the worker stalled). Re-run \`${opts.retryCmd}\` to confirm the resource is gone.
|
|
17068
|
+
`
|
|
17069
|
+
);
|
|
17070
|
+
process.exitCode = 1;
|
|
17071
|
+
}
|
|
17072
|
+
}
|
|
17073
|
+
async function confirmResourceRemoval(opts) {
|
|
17074
|
+
if (opts.yes) return true;
|
|
17075
|
+
const io = opts.io ?? defaultPromptIO();
|
|
17076
|
+
if (!isInteractive(io)) {
|
|
17077
|
+
process.stderr.write(
|
|
17078
|
+
`Refusing to remove ${opts.subject} without confirmation. Re-run with --yes to confirm non-interactively.
|
|
16986
17079
|
`
|
|
16987
17080
|
);
|
|
16988
17081
|
process.exitCode = 1;
|
|
17082
|
+
return false;
|
|
16989
17083
|
}
|
|
17084
|
+
if (await promptConfirm(opts.question, io)) return true;
|
|
17085
|
+
process.stdout.write(opts.declined);
|
|
17086
|
+
return false;
|
|
16990
17087
|
}
|
|
16991
17088
|
function isProjectId(arg) {
|
|
16992
17089
|
return arg.startsWith("proj_");
|
|
@@ -17134,7 +17231,7 @@ async function describeProjectResources(apiUrl, token, projectId) {
|
|
|
17134
17231
|
out.push("its Git repository on the forge, and its push token");
|
|
17135
17232
|
return out;
|
|
17136
17233
|
}
|
|
17137
|
-
async function
|
|
17234
|
+
async function runDbRemove(opts) {
|
|
17138
17235
|
const apiUrl = resolveApiUrl(opts.apiUrl);
|
|
17139
17236
|
const token = loadConfig().token;
|
|
17140
17237
|
if (!token) {
|
|
@@ -17144,27 +17241,36 @@ async function runDbDetach(opts) {
|
|
|
17144
17241
|
}
|
|
17145
17242
|
const project2 = resolveProjectId(opts.project, opts.cwd);
|
|
17146
17243
|
if (!project2) {
|
|
17147
|
-
reportNotLinked("kryd db
|
|
17244
|
+
reportNotLinked("kryd db remove <projectId>");
|
|
17148
17245
|
return;
|
|
17149
17246
|
}
|
|
17247
|
+
const proceed = await confirmResourceRemoval({
|
|
17248
|
+
yes: opts.yes,
|
|
17249
|
+
io: opts.io,
|
|
17250
|
+
subject: `the database from ${project2}`,
|
|
17251
|
+
question: `Remove the database from ${project2}? It is torn down and all the data in it is destroyed \u2014 this cannot be undone.`,
|
|
17252
|
+
declined: `Left the database on ${project2} in place.
|
|
17253
|
+
`
|
|
17254
|
+
});
|
|
17255
|
+
if (!proceed) return;
|
|
17150
17256
|
try {
|
|
17151
17257
|
await detachDatabase(apiUrl, token, project2);
|
|
17152
|
-
process.stdout.write(`
|
|
17258
|
+
process.stdout.write(`Removing the database from ${project2}\u2026
|
|
17153
17259
|
`);
|
|
17154
17260
|
const result = await pollResourceDetached(
|
|
17155
17261
|
() => getDatabaseStatus(apiUrl, token, project2)
|
|
17156
17262
|
);
|
|
17157
17263
|
reportDetachResult(result, {
|
|
17158
|
-
goneMsg: `
|
|
17264
|
+
goneMsg: `Removed the database from ${project2}.
|
|
17159
17265
|
`,
|
|
17160
|
-
failedLabel: "Database
|
|
17161
|
-
retryCmd: `kryd db
|
|
17266
|
+
failedLabel: "Database removal",
|
|
17267
|
+
retryCmd: `kryd db remove ${project2}`
|
|
17162
17268
|
});
|
|
17163
17269
|
} catch (err) {
|
|
17164
17270
|
reportError(err);
|
|
17165
17271
|
}
|
|
17166
17272
|
}
|
|
17167
|
-
async function
|
|
17273
|
+
async function runAiAdd(opts) {
|
|
17168
17274
|
const apiUrl = resolveApiUrl(opts.apiUrl);
|
|
17169
17275
|
const token = loadConfig().token;
|
|
17170
17276
|
if (!token) {
|
|
@@ -17174,13 +17280,13 @@ async function runAiEnable(opts) {
|
|
|
17174
17280
|
}
|
|
17175
17281
|
const project2 = resolveProjectId(opts.project, opts.cwd);
|
|
17176
17282
|
if (!project2) {
|
|
17177
|
-
reportNotLinked("kryd ai
|
|
17283
|
+
reportNotLinked("kryd ai add <projectId>");
|
|
17178
17284
|
return;
|
|
17179
17285
|
}
|
|
17180
17286
|
try {
|
|
17181
17287
|
const status = await enableAi(apiUrl, token, { projectId: project2 });
|
|
17182
17288
|
process.stdout.write(
|
|
17183
|
-
`AI
|
|
17289
|
+
`AI endpoint added to ${project2}.
|
|
17184
17290
|
` + (status.url ? `Endpoint: ${status.url} (OpenAI-compatible base URL \u2014 use it as-is)
|
|
17185
17291
|
` : "") + `AI_GATEWAY_URL + AI_GATEWAY_TOKEN will be injected on your next deploy (kryd deploy).
|
|
17186
17292
|
`
|
|
@@ -17189,7 +17295,7 @@ async function runAiEnable(opts) {
|
|
|
17189
17295
|
reportError(err);
|
|
17190
17296
|
}
|
|
17191
17297
|
}
|
|
17192
|
-
async function
|
|
17298
|
+
async function runAiRemove(opts) {
|
|
17193
17299
|
const apiUrl = resolveApiUrl(opts.apiUrl);
|
|
17194
17300
|
const token = loadConfig().token;
|
|
17195
17301
|
if (!token) {
|
|
@@ -17199,21 +17305,289 @@ async function runStorageDetach(opts) {
|
|
|
17199
17305
|
}
|
|
17200
17306
|
const project2 = resolveProjectId(opts.project, opts.cwd);
|
|
17201
17307
|
if (!project2) {
|
|
17202
|
-
reportNotLinked("kryd
|
|
17308
|
+
reportNotLinked("kryd ai remove <projectId>");
|
|
17309
|
+
return;
|
|
17310
|
+
}
|
|
17311
|
+
let current;
|
|
17312
|
+
try {
|
|
17313
|
+
current = await getAiStatus(apiUrl, token, project2);
|
|
17314
|
+
} catch (err) {
|
|
17315
|
+
reportError(err);
|
|
17316
|
+
return;
|
|
17317
|
+
}
|
|
17318
|
+
if (!current.enabled) {
|
|
17319
|
+
process.stdout.write(`${project2}: no AI endpoint \u2014 nothing to remove.
|
|
17320
|
+
`);
|
|
17203
17321
|
return;
|
|
17204
17322
|
}
|
|
17323
|
+
const proceed = await confirmResourceRemoval({
|
|
17324
|
+
yes: opts.yes,
|
|
17325
|
+
io: opts.io,
|
|
17326
|
+
subject: `the AI endpoint from ${project2}`,
|
|
17327
|
+
question: `Remove the AI endpoint from ${project2}? AI_GATEWAY_URL and AI_GATEWAY_TOKEN stop being injected from your next deploy.`,
|
|
17328
|
+
declined: `Left the AI endpoint on ${project2} in place.
|
|
17329
|
+
`
|
|
17330
|
+
});
|
|
17331
|
+
if (!proceed) return;
|
|
17332
|
+
try {
|
|
17333
|
+
await removeAi(apiUrl, token, project2);
|
|
17334
|
+
process.stdout.write(
|
|
17335
|
+
`AI endpoint removed from ${project2}.
|
|
17336
|
+
AI_GATEWAY_URL and AI_GATEWAY_TOKEN stop being injected from your next deploy (kryd deploy).
|
|
17337
|
+
`
|
|
17338
|
+
);
|
|
17339
|
+
} catch (err) {
|
|
17340
|
+
reportError(err);
|
|
17341
|
+
}
|
|
17342
|
+
}
|
|
17343
|
+
async function reportResourceStatus(opts) {
|
|
17344
|
+
try {
|
|
17345
|
+
const result = await opts.read();
|
|
17346
|
+
if (result.status === "failed") {
|
|
17347
|
+
process.stdout.write(
|
|
17348
|
+
`${opts.project}: ${opts.noun} failed \u2014 ${result.failureReason ?? "unknown error"}.
|
|
17349
|
+
Remove it before attaching another.
|
|
17350
|
+
`
|
|
17351
|
+
);
|
|
17352
|
+
return;
|
|
17353
|
+
}
|
|
17354
|
+
process.stdout.write(`${opts.project}: ${opts.noun} ${result.status}.
|
|
17355
|
+
`);
|
|
17356
|
+
} catch (err) {
|
|
17357
|
+
if (err instanceof ApiError && err.status === 404) {
|
|
17358
|
+
process.stdout.write(`${opts.project}: no ${opts.noun} attached.
|
|
17359
|
+
`);
|
|
17360
|
+
return;
|
|
17361
|
+
}
|
|
17362
|
+
reportError(err);
|
|
17363
|
+
}
|
|
17364
|
+
}
|
|
17365
|
+
async function runDbStatus(opts) {
|
|
17366
|
+
const apiUrl = resolveApiUrl(opts.apiUrl);
|
|
17367
|
+
const token = loadConfig().token;
|
|
17368
|
+
if (!token) {
|
|
17369
|
+
process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
|
|
17370
|
+
process.exitCode = 1;
|
|
17371
|
+
return;
|
|
17372
|
+
}
|
|
17373
|
+
const project2 = resolveProjectId(opts.project, opts.cwd);
|
|
17374
|
+
if (!project2) {
|
|
17375
|
+
reportNotLinked("kryd db status <projectId>");
|
|
17376
|
+
return;
|
|
17377
|
+
}
|
|
17378
|
+
await reportResourceStatus({
|
|
17379
|
+
read: () => getDatabaseStatus(apiUrl, token, project2),
|
|
17380
|
+
noun: "database",
|
|
17381
|
+
project: project2
|
|
17382
|
+
});
|
|
17383
|
+
}
|
|
17384
|
+
async function runStorageStatus(opts) {
|
|
17385
|
+
const apiUrl = resolveApiUrl(opts.apiUrl);
|
|
17386
|
+
const token = loadConfig().token;
|
|
17387
|
+
if (!token) {
|
|
17388
|
+
process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
|
|
17389
|
+
process.exitCode = 1;
|
|
17390
|
+
return;
|
|
17391
|
+
}
|
|
17392
|
+
const project2 = resolveProjectId(opts.project, opts.cwd);
|
|
17393
|
+
if (!project2) {
|
|
17394
|
+
reportNotLinked("kryd storage status <projectId>");
|
|
17395
|
+
return;
|
|
17396
|
+
}
|
|
17397
|
+
await reportResourceStatus({
|
|
17398
|
+
read: () => getStorageStatus(apiUrl, token, project2),
|
|
17399
|
+
noun: "object storage",
|
|
17400
|
+
project: project2
|
|
17401
|
+
});
|
|
17402
|
+
}
|
|
17403
|
+
async function runAiStatus(opts) {
|
|
17404
|
+
const apiUrl = resolveApiUrl(opts.apiUrl);
|
|
17405
|
+
const token = loadConfig().token;
|
|
17406
|
+
if (!token) {
|
|
17407
|
+
process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
|
|
17408
|
+
process.exitCode = 1;
|
|
17409
|
+
return;
|
|
17410
|
+
}
|
|
17411
|
+
const project2 = resolveProjectId(opts.project, opts.cwd);
|
|
17412
|
+
if (!project2) {
|
|
17413
|
+
reportNotLinked("kryd ai status <projectId>");
|
|
17414
|
+
return;
|
|
17415
|
+
}
|
|
17416
|
+
try {
|
|
17417
|
+
const status = await getAiStatus(apiUrl, token, project2);
|
|
17418
|
+
if (!status.enabled) {
|
|
17419
|
+
process.stdout.write(`${project2}: no AI endpoint.
|
|
17420
|
+
`);
|
|
17421
|
+
return;
|
|
17422
|
+
}
|
|
17423
|
+
process.stdout.write(
|
|
17424
|
+
status.url ? `${project2}: AI endpoint ${status.url}
|
|
17425
|
+
` : `${project2}: AI endpoint enabled, but its URL could not be read back right now.
|
|
17426
|
+
`
|
|
17427
|
+
);
|
|
17428
|
+
} catch (err) {
|
|
17429
|
+
reportError(err);
|
|
17430
|
+
}
|
|
17431
|
+
}
|
|
17432
|
+
function isoDay(iso) {
|
|
17433
|
+
return iso.slice(0, 10);
|
|
17434
|
+
}
|
|
17435
|
+
function daysUntil(iso) {
|
|
17436
|
+
return Math.floor((new Date(iso).getTime() - Date.now()) / 864e5);
|
|
17437
|
+
}
|
|
17438
|
+
var TOKEN_EXPIRY_WARNING_DAYS = 30;
|
|
17439
|
+
async function runWorkflowAdd(opts) {
|
|
17440
|
+
const apiUrl = resolveApiUrl(opts.apiUrl);
|
|
17441
|
+
const token = loadConfig().token;
|
|
17442
|
+
if (!token) {
|
|
17443
|
+
process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
|
|
17444
|
+
process.exitCode = 1;
|
|
17445
|
+
return;
|
|
17446
|
+
}
|
|
17447
|
+
const project2 = resolveProjectId(opts.project, opts.cwd);
|
|
17448
|
+
if (!project2) {
|
|
17449
|
+
reportNotLinked("kryd workflow add <projectId>");
|
|
17450
|
+
return;
|
|
17451
|
+
}
|
|
17452
|
+
try {
|
|
17453
|
+
const current = await getWorkflowStatus(apiUrl, token, project2);
|
|
17454
|
+
if (current.enabled) {
|
|
17455
|
+
process.stdout.write(
|
|
17456
|
+
`${project2} already has workflows \u2014 HATCHET_CLIENT_TOKEN is injected on deploy` + (current.tokenExpiresAt ? ` and the token expires ${isoDay(current.tokenExpiresAt)}.
|
|
17457
|
+
` : ".\n") + `Nothing was changed. To rotate the token, run \`kryd workflow remove\` and then \`kryd workflow add\`.
|
|
17458
|
+
`
|
|
17459
|
+
);
|
|
17460
|
+
return;
|
|
17461
|
+
}
|
|
17462
|
+
const status = await addWorkflows(apiUrl, token, { projectId: project2 });
|
|
17463
|
+
process.stdout.write(
|
|
17464
|
+
`Workflows added to ${project2}.
|
|
17465
|
+
HATCHET_CLIENT_TOKEN will be injected on your next deploy (kryd deploy)` + (status.tokenExpiresAt ? `; the token expires ${isoDay(status.tokenExpiresAt)}.
|
|
17466
|
+
` : ".\n")
|
|
17467
|
+
);
|
|
17468
|
+
} catch (err) {
|
|
17469
|
+
reportError(err);
|
|
17470
|
+
}
|
|
17471
|
+
}
|
|
17472
|
+
async function runWorkflowRemove(opts) {
|
|
17473
|
+
const apiUrl = resolveApiUrl(opts.apiUrl);
|
|
17474
|
+
const token = loadConfig().token;
|
|
17475
|
+
if (!token) {
|
|
17476
|
+
process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
|
|
17477
|
+
process.exitCode = 1;
|
|
17478
|
+
return;
|
|
17479
|
+
}
|
|
17480
|
+
const project2 = resolveProjectId(opts.project, opts.cwd);
|
|
17481
|
+
if (!project2) {
|
|
17482
|
+
reportNotLinked("kryd workflow remove <projectId>");
|
|
17483
|
+
return;
|
|
17484
|
+
}
|
|
17485
|
+
let current;
|
|
17486
|
+
try {
|
|
17487
|
+
current = await getWorkflowStatus(apiUrl, token, project2);
|
|
17488
|
+
} catch (err) {
|
|
17489
|
+
reportError(err);
|
|
17490
|
+
return;
|
|
17491
|
+
}
|
|
17492
|
+
if (!current.enabled) {
|
|
17493
|
+
process.stdout.write(`${project2}: no workflow token \u2014 nothing to remove.
|
|
17494
|
+
`);
|
|
17495
|
+
return;
|
|
17496
|
+
}
|
|
17497
|
+
const proceed = await confirmResourceRemoval({
|
|
17498
|
+
yes: opts.yes,
|
|
17499
|
+
io: opts.io,
|
|
17500
|
+
subject: `workflows from ${project2}`,
|
|
17501
|
+
question: `Remove workflows from ${project2}? The token is revoked \u2014 a running worker loses its connection within seconds \u2014 and HATCHET_CLIENT_TOKEN stops being injected from your next deploy. Workflow history, crons and schedules stay until the project is deleted.`,
|
|
17502
|
+
declined: `Left the workflows on ${project2} in place.
|
|
17503
|
+
`
|
|
17504
|
+
});
|
|
17505
|
+
if (!proceed) return;
|
|
17506
|
+
try {
|
|
17507
|
+
await removeWorkflows(apiUrl, token, project2);
|
|
17508
|
+
process.stdout.write(
|
|
17509
|
+
`Workflows removed from ${project2}: the token is revoked (the engine refuses it within seconds; the HTTP API within a minute) and HATCHET_CLIENT_TOKEN stops being injected from your next deploy (kryd deploy).
|
|
17510
|
+
Workflow history, crons and schedules stay until the project is deleted.
|
|
17511
|
+
`
|
|
17512
|
+
);
|
|
17513
|
+
} catch (err) {
|
|
17514
|
+
reportError(err);
|
|
17515
|
+
}
|
|
17516
|
+
}
|
|
17517
|
+
async function runWorkflowStatus(opts) {
|
|
17518
|
+
const apiUrl = resolveApiUrl(opts.apiUrl);
|
|
17519
|
+
const token = loadConfig().token;
|
|
17520
|
+
if (!token) {
|
|
17521
|
+
process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
|
|
17522
|
+
process.exitCode = 1;
|
|
17523
|
+
return;
|
|
17524
|
+
}
|
|
17525
|
+
const project2 = resolveProjectId(opts.project, opts.cwd);
|
|
17526
|
+
if (!project2) {
|
|
17527
|
+
reportNotLinked("kryd workflow status <projectId>");
|
|
17528
|
+
return;
|
|
17529
|
+
}
|
|
17530
|
+
try {
|
|
17531
|
+
const status = await getWorkflowStatus(apiUrl, token, project2);
|
|
17532
|
+
if (!status.enabled) {
|
|
17533
|
+
process.stdout.write(`${project2}: no workflows.
|
|
17534
|
+
`);
|
|
17535
|
+
return;
|
|
17536
|
+
}
|
|
17537
|
+
if (!status.tokenExpiresAt) {
|
|
17538
|
+
process.stdout.write(`${project2}: workflows enabled.
|
|
17539
|
+
`);
|
|
17540
|
+
return;
|
|
17541
|
+
}
|
|
17542
|
+
const days = daysUntil(status.tokenExpiresAt);
|
|
17543
|
+
process.stdout.write(
|
|
17544
|
+
`${project2}: workflows enabled \u2014 the token expires ${isoDay(status.tokenExpiresAt)}.
|
|
17545
|
+
`
|
|
17546
|
+
);
|
|
17547
|
+
if (days <= TOKEN_EXPIRY_WARNING_DAYS) {
|
|
17548
|
+
process.stdout.write(
|
|
17549
|
+
(days < 0 ? `\u26A0\uFE0F The token has EXPIRED; your worker cannot connect.` : `\u26A0\uFE0F The token expires in ${days} day${days === 1 ? "" : "s"}.`) + ` Rotate it: \`kryd workflow remove\`, then \`kryd workflow add\`, then deploy.
|
|
17550
|
+
`
|
|
17551
|
+
);
|
|
17552
|
+
}
|
|
17553
|
+
} catch (err) {
|
|
17554
|
+
reportError(err);
|
|
17555
|
+
}
|
|
17556
|
+
}
|
|
17557
|
+
async function runStorageRemove(opts) {
|
|
17558
|
+
const apiUrl = resolveApiUrl(opts.apiUrl);
|
|
17559
|
+
const token = loadConfig().token;
|
|
17560
|
+
if (!token) {
|
|
17561
|
+
process.stderr.write("Not logged in \u2014 run `kryd login` first.\n");
|
|
17562
|
+
process.exitCode = 1;
|
|
17563
|
+
return;
|
|
17564
|
+
}
|
|
17565
|
+
const project2 = resolveProjectId(opts.project, opts.cwd);
|
|
17566
|
+
if (!project2) {
|
|
17567
|
+
reportNotLinked("kryd storage remove <projectId>");
|
|
17568
|
+
return;
|
|
17569
|
+
}
|
|
17570
|
+
const proceed = await confirmResourceRemoval({
|
|
17571
|
+
yes: opts.yes,
|
|
17572
|
+
io: opts.io,
|
|
17573
|
+
subject: `object storage from ${project2}`,
|
|
17574
|
+
question: `Remove object storage from ${project2}? The bucket is torn down and every object in it is destroyed \u2014 this cannot be undone.`,
|
|
17575
|
+
declined: `Left the object storage on ${project2} in place.
|
|
17576
|
+
`
|
|
17577
|
+
});
|
|
17578
|
+
if (!proceed) return;
|
|
17205
17579
|
try {
|
|
17206
17580
|
await detachStorage(apiUrl, token, project2);
|
|
17207
|
-
process.stdout.write(`
|
|
17581
|
+
process.stdout.write(`Removing object storage from ${project2}\u2026
|
|
17208
17582
|
`);
|
|
17209
17583
|
const result = await pollResourceDetached(
|
|
17210
17584
|
() => getStorageStatus(apiUrl, token, project2)
|
|
17211
17585
|
);
|
|
17212
17586
|
reportDetachResult(result, {
|
|
17213
|
-
goneMsg: `
|
|
17587
|
+
goneMsg: `Removed object storage from ${project2}.
|
|
17214
17588
|
`,
|
|
17215
|
-
failedLabel: "Storage
|
|
17216
|
-
retryCmd: `kryd storage
|
|
17589
|
+
failedLabel: "Storage removal",
|
|
17590
|
+
retryCmd: `kryd storage remove ${project2}`
|
|
17217
17591
|
});
|
|
17218
17592
|
} catch (err) {
|
|
17219
17593
|
reportError(err);
|
|
@@ -17599,7 +17973,7 @@ async function runEnvRemove(opts) {
|
|
|
17599
17973
|
if (isReservedEnvVarName(key)) {
|
|
17600
17974
|
process.stderr.write(
|
|
17601
17975
|
`${key} is reserved by Kryd and cannot be removed here.
|
|
17602
|
-
If it comes from a resource you attached,
|
|
17976
|
+
If it comes from a resource you attached, remove that instead (\`kryd db remove\`, \`kryd storage remove\`).
|
|
17603
17977
|
`
|
|
17604
17978
|
);
|
|
17605
17979
|
process.exitCode = 1;
|
|
@@ -17666,7 +18040,7 @@ Run \`kryd env list\` to see what is set. (If you meant the runtime variable of
|
|
|
17666
18040
|
reportError(err);
|
|
17667
18041
|
}
|
|
17668
18042
|
}
|
|
17669
|
-
var CLI_VERSION = true ? "0.
|
|
18043
|
+
var CLI_VERSION = true ? "0.7.0" : "0.0.0-dev";
|
|
17670
18044
|
var program = new Command();
|
|
17671
18045
|
program.name("kryd").description("Kryd CLI").version(CLI_VERSION);
|
|
17672
18046
|
program.command("login").description("Sign in via the browser (default) and store a token").option("--email <email>", "account email (with --password; non-interactive escape hatch)").option("--password <password>", "account password (with --email; visible in `ps` \u2014 prefer the browser flow)").option(
|
|
@@ -17718,14 +18092,16 @@ project.command("rm [project]").description(
|
|
|
17718
18092
|
"Permanently delete a project and everything Kryd created for it (by name or id; asks you to type its name)"
|
|
17719
18093
|
).option("--yes", "skip the confirmation (for non-interactive use)").option("--api-url <url>", "control-plane API base URL").action((projectArg, opts) => runProjectRemove({ ...opts, project: projectArg }));
|
|
17720
18094
|
var db = program.command("db").description("Manage project databases");
|
|
17721
|
-
db.command("
|
|
18095
|
+
db.command("add [project]").description("Attach a shared or bring-your-own Postgres database to a project").option("--shared", "attach a shared managed database (the default)").option(
|
|
17722
18096
|
"--connection-string <url>",
|
|
17723
18097
|
"attach an existing database (BYO) by pasting its connection string"
|
|
17724
|
-
).option("--api-url <url>", "control-plane API base URL").action((project2, opts) =>
|
|
17725
|
-
db.command("
|
|
18098
|
+
).option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runDbAdd({ ...opts, project: project2 }));
|
|
18099
|
+
db.command("remove [project]").description("Tear down the project's database and everything in it (asks for confirmation)").option("--yes", "skip the confirmation prompt").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runDbRemove({ ...opts, project: project2 }));
|
|
18100
|
+
db.command("status [project]").description("Show whether the project has a database, and what state it is in").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runDbStatus({ ...opts, project: project2 }));
|
|
17726
18101
|
var storage = program.command("storage").description("Manage project object storage");
|
|
17727
|
-
storage.command("
|
|
17728
|
-
storage.command("
|
|
18102
|
+
storage.command("add [project]").description("Create an S3-compatible object-storage bucket for a project").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runStorageAdd({ ...opts, project: project2 }));
|
|
18103
|
+
storage.command("remove [project]").description("Tear down the project's bucket and every object in it (asks for confirmation)").option("--yes", "skip the confirmation prompt").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runStorageRemove({ ...opts, project: project2 }));
|
|
18104
|
+
storage.command("status [project]").description("Show whether the project has object storage, and what state it is in").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runStorageStatus({ ...opts, project: project2 }));
|
|
17729
18105
|
var env = program.command("env").description("Manage your app's environment variables");
|
|
17730
18106
|
env.command("list [project]").description("List the project's environment variables (names and origin \u2014 never values)").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runEnvList({ ...opts, project: project2 }));
|
|
17731
18107
|
env.command("set <key> [project]").description(
|
|
@@ -17757,7 +18133,13 @@ env.command("pull [project]").description(
|
|
|
17757
18133
|
"file to write, relative to the project root (default: .env.kryd \u2014 never your hand-maintained .env)"
|
|
17758
18134
|
).option("--force", "overwrite the --out file if it already exists").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runEnvPull({ ...opts, project: project2 }));
|
|
17759
18135
|
var ai = program.command("ai").description("Manage the app's EU AI gateway");
|
|
17760
|
-
ai.command("
|
|
18136
|
+
ai.command("add [project]").description("Give the project an authenticated EU AI endpoint (injects on next deploy)").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runAiAdd({ ...opts, project: project2 }));
|
|
18137
|
+
ai.command("remove [project]").description("Stop injecting the project's AI endpoint (asks for confirmation)").option("--yes", "skip the confirmation prompt").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runAiRemove({ ...opts, project: project2 }));
|
|
18138
|
+
ai.command("status [project]").description("Show whether the project has an AI endpoint, and what it is").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runAiStatus({ ...opts, project: project2 }));
|
|
18139
|
+
var workflow = program.command("workflow").description("Manage the project's durable workflows (its own workflow tenant)");
|
|
18140
|
+
workflow.command("add [project]").description("Give the project a workflow tenant and inject HATCHET_CLIENT_TOKEN on next deploy").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runWorkflowAdd({ ...opts, project: project2 }));
|
|
18141
|
+
workflow.command("remove [project]").description("Revoke the project's workflow token and stop injecting it (asks for confirmation)").option("--yes", "skip the confirmation prompt").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runWorkflowRemove({ ...opts, project: project2 }));
|
|
18142
|
+
workflow.command("status [project]").description("Show whether the project has workflows, and when its token expires").option("--api-url <url>", "control-plane API base URL").action((project2, opts) => runWorkflowStatus({ ...opts, project: project2 }));
|
|
17761
18143
|
function invokedDirectly() {
|
|
17762
18144
|
const entry = process.argv[1];
|
|
17763
18145
|
if (!entry) return false;
|
|
@@ -17778,9 +18160,12 @@ if (invokedDirectly()) {
|
|
|
17778
18160
|
}
|
|
17779
18161
|
export {
|
|
17780
18162
|
program,
|
|
17781
|
-
|
|
17782
|
-
|
|
17783
|
-
|
|
18163
|
+
runAiAdd,
|
|
18164
|
+
runAiRemove,
|
|
18165
|
+
runAiStatus,
|
|
18166
|
+
runDbAdd,
|
|
18167
|
+
runDbRemove,
|
|
18168
|
+
runDbStatus,
|
|
17784
18169
|
runDeploy,
|
|
17785
18170
|
runEnvList,
|
|
17786
18171
|
runEnvPull,
|
|
@@ -17796,8 +18181,12 @@ export {
|
|
|
17796
18181
|
runRedeploy,
|
|
17797
18182
|
runRollback,
|
|
17798
18183
|
runRuntimeLogs,
|
|
17799
|
-
|
|
17800
|
-
|
|
18184
|
+
runStorageAdd,
|
|
18185
|
+
runStorageRemove,
|
|
18186
|
+
runStorageStatus,
|
|
17801
18187
|
runWhoami,
|
|
18188
|
+
runWorkflowAdd,
|
|
18189
|
+
runWorkflowRemove,
|
|
18190
|
+
runWorkflowStatus,
|
|
17802
18191
|
splitKeyValue
|
|
17803
18192
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kryd/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Kryd CLI — push a React / Vite / Next.js app to the European cloud for your AI: git push → live with SSL, one-click managed Postgres & object storage, and an EU-hosted AI gateway already wired in. Your code and your model calls stay in the EU.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"kryd",
|
|
@@ -49,9 +49,9 @@
|
|
|
49
49
|
"tsx": "^4.19.2",
|
|
50
50
|
"typescript": "^5.6.3",
|
|
51
51
|
"vitest": "^2.1.8",
|
|
52
|
-
"@kryd/
|
|
52
|
+
"@kryd/config-eslint": "0.0.0",
|
|
53
53
|
"@kryd/config-ts": "0.0.0",
|
|
54
|
-
"@kryd/
|
|
54
|
+
"@kryd/shared-types": "0.0.0"
|
|
55
55
|
},
|
|
56
56
|
"scripts": {
|
|
57
57
|
"build": "esbuild src/index.ts --bundle --platform=node --format=esm --external:commander --define:__KRYD_VERSION__=\\\"$npm_package_version\\\" --outfile=dist/index.js",
|