@kryd/cli 0.8.1 → 0.9.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 +2 -1
- package/dist/index.js +239 -32
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -20,6 +20,7 @@ kryd init # link this folder to a Kryd project (+ a `kryd` g
|
|
|
20
20
|
kryd push # push the current branch → build → deploy → live, with live progress
|
|
21
21
|
kryd logs # the full build log of the latest deploy (kryd push shows progress, not log lines)
|
|
22
22
|
kryd logs <project> --runtime # tail the live app's stdout/stderr
|
|
23
|
+
kryd logs <project> --worker # tail the workflow worker's stdout/stderr
|
|
23
24
|
```
|
|
24
25
|
|
|
25
26
|
Once you've run `kryd init` in a directory, the commands below work **arg-less** there — the folder is linked via `.kryd/project.json` (git-ignored, no secret).
|
|
@@ -32,7 +33,7 @@ Once you've run `kryd init` in a directory, the commands below work **arg-less**
|
|
|
32
33
|
| `kryd init` | Link the current repo to a Kryd project + register the deploy webhook + add a `kryd` git remote. |
|
|
33
34
|
| `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. |
|
|
34
35
|
| `kryd deploy [project]` | Re-deploy the production-branch HEAD already on the forge — no new commit. |
|
|
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
|
+
| `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; `--worker` tails the project's workflow worker, also when it has a web app). |
|
|
36
37
|
| `kryd rollback [project] [deployment]` | Roll back to a previous successful deploy — no rebuild. |
|
|
37
38
|
| `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
39
|
| `kryd storage add \| remove \| status [project]` | Attach, tear down or inspect S3-compatible object storage. `remove` destroys every object and asks first. |
|
package/dist/index.js
CHANGED
|
@@ -56,6 +56,7 @@ function parseDeployEvent(payload) {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
// ../../packages/shared-types/dist/runtime-log.js
|
|
59
|
+
var RUNTIME_REFUSAL_CODES = ["no-runtime", "pending"];
|
|
59
60
|
function parseRuntimeStreamFrame(payload) {
|
|
60
61
|
let parsed;
|
|
61
62
|
try {
|
|
@@ -67,7 +68,8 @@ function parseRuntimeStreamFrame(payload) {
|
|
|
67
68
|
return null;
|
|
68
69
|
const e = parsed;
|
|
69
70
|
if (e.kind === "error" && typeof e.message === "string") {
|
|
70
|
-
|
|
71
|
+
const code = RUNTIME_REFUSAL_CODES.includes(e.code) ? e.code : void 0;
|
|
72
|
+
return code ? { kind: "error", message: e.message, code } : { kind: "error", message: e.message };
|
|
71
73
|
}
|
|
72
74
|
if (e.kind !== "runtime" || e.stream !== "stdout" && e.stream !== "stderr" || typeof e.line !== "string") {
|
|
73
75
|
return null;
|
|
@@ -14902,6 +14904,7 @@ function isTerminalResourceStatus(status) {
|
|
|
14902
14904
|
import {
|
|
14903
14905
|
existsSync as existsSync3,
|
|
14904
14906
|
mkdirSync as mkdirSync2,
|
|
14907
|
+
readFileSync as readFileSync3,
|
|
14905
14908
|
realpathSync,
|
|
14906
14909
|
rmSync as rmSync2,
|
|
14907
14910
|
writeFileSync as writeFileSync2
|
|
@@ -15874,13 +15877,13 @@ async function streamDeploy(apiUrl, token, deploymentId, handlers, opts = {}) {
|
|
|
15874
15877
|
return terminal;
|
|
15875
15878
|
}
|
|
15876
15879
|
async function streamRuntime(apiUrl, token, id, handlers, opts = {}) {
|
|
15877
|
-
const
|
|
15880
|
+
const path = opts.resource === "deployment" ? `deployments/${id}/runtime-logs` : opts.resource === "worker" ? `projects/${id}/workflow-logs` : `projects/${id}/runtime-logs`;
|
|
15878
15881
|
const doFetch = opts.fetchImpl ?? fetch;
|
|
15879
15882
|
const reconnectMs = opts.reconnectMs ?? 1e3;
|
|
15880
15883
|
let afterNs;
|
|
15881
15884
|
while (!opts.signal?.aborted) {
|
|
15882
15885
|
try {
|
|
15883
|
-
const url2 = new URL(`${apiUrl}/${
|
|
15886
|
+
const url2 = new URL(`${apiUrl}/${path}`);
|
|
15884
15887
|
if (afterNs) url2.searchParams.set("after", afterNs);
|
|
15885
15888
|
else if (opts.since) url2.searchParams.set("since", opts.since);
|
|
15886
15889
|
const res = await doFetch(url2, {
|
|
@@ -15988,7 +15991,13 @@ function isFramework(value) {
|
|
|
15988
15991
|
return FRAMEWORKS.includes(value);
|
|
15989
15992
|
}
|
|
15990
15993
|
var DECLARATION_FILE = "kryd.json";
|
|
15991
|
-
var
|
|
15994
|
+
var DEFAULT_WORKFLOWS_PATH = "workflows";
|
|
15995
|
+
function declarationJson(decl) {
|
|
15996
|
+
return {
|
|
15997
|
+
...decl.web ? {} : { web: false },
|
|
15998
|
+
...decl.workflowsPath !== null ? { workflows: { path: decl.workflowsPath } } : {}
|
|
15999
|
+
};
|
|
16000
|
+
}
|
|
15992
16001
|
var VITE_META_FRAMEWORKS = [
|
|
15993
16002
|
"@sveltejs/kit",
|
|
15994
16003
|
"astro",
|
|
@@ -16030,22 +16039,26 @@ function readPackageJson(cwd) {
|
|
|
16030
16039
|
return null;
|
|
16031
16040
|
}
|
|
16032
16041
|
}
|
|
16033
|
-
function
|
|
16042
|
+
function readDeclaration(cwd) {
|
|
16043
|
+
let parsed;
|
|
16034
16044
|
try {
|
|
16035
|
-
|
|
16036
|
-
readFileSync2(join2(cwd, DECLARATION_FILE), "utf8")
|
|
16037
|
-
);
|
|
16038
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
16039
|
-
return null;
|
|
16040
|
-
}
|
|
16041
|
-
const kind = parsed.kind;
|
|
16042
|
-
return typeof kind === "string" ? kind : null;
|
|
16045
|
+
parsed = JSON.parse(readFileSync2(join2(cwd, DECLARATION_FILE), "utf8"));
|
|
16043
16046
|
} catch {
|
|
16044
16047
|
return null;
|
|
16045
16048
|
}
|
|
16049
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
16050
|
+
return null;
|
|
16051
|
+
}
|
|
16052
|
+
const decl = parsed;
|
|
16053
|
+
const workflows = decl.workflows;
|
|
16054
|
+
const workflowsPath = workflows && typeof workflows === "object" && typeof workflows.path === "string" ? workflows.path : null;
|
|
16055
|
+
return { web: decl.web !== false, workflowsPath };
|
|
16046
16056
|
}
|
|
16047
16057
|
function frameworkFrom(pkg, cwd) {
|
|
16048
|
-
|
|
16058
|
+
const declaration = readDeclaration(cwd);
|
|
16059
|
+
if (declaration && !declaration.web && declaration.workflowsPath !== null) {
|
|
16060
|
+
return "workflow";
|
|
16061
|
+
}
|
|
16049
16062
|
const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
|
|
16050
16063
|
const has = (name) => name in deps;
|
|
16051
16064
|
if (has("next")) return "nextjs";
|
|
@@ -16140,6 +16153,10 @@ export const hatchet = HatchetClient.init();
|
|
|
16140
16153
|
* One task, to prove the wiring end to end. Add your own beside it and list them in
|
|
16141
16154
|
* \`src/worker.ts\` \u2014 everything about how a task runs (retries, timeouts, concurrency, crons,
|
|
16142
16155
|
* DAGs) belongs to the engine's SDK, not to Kryd.
|
|
16156
|
+
*
|
|
16157
|
+
* A task can be cut off by a deploy or a restart: measured, a TypeScript worker stops within seconds
|
|
16158
|
+
* and the engine runs the task again from the start on another worker ~30 s later, even with
|
|
16159
|
+
* \`retries: 0\` \u2014 so write every task so that running it again is safe.
|
|
16143
16160
|
*/
|
|
16144
16161
|
export const greet = hatchet.task({
|
|
16145
16162
|
name: "greet",
|
|
@@ -16188,7 +16205,15 @@ async function main(): Promise<void> {
|
|
|
16188
16205
|
const { hatchet } = await import("./hatchet");
|
|
16189
16206
|
const { greet } = await import("./tasks");
|
|
16190
16207
|
|
|
16191
|
-
|
|
16208
|
+
// Kryd sets KRYD_DEPLOYMENT_ID on a deployed worker, and the label is how its deploy gate tells
|
|
16209
|
+
// THIS deploy's worker from the previous one, which keeps running while it drains. Keep the line:
|
|
16210
|
+
// the gate requires it, and a worker without it never counts as this deploy's, so the deploy
|
|
16211
|
+
// never goes live. Locally the variable is absent, so no label.
|
|
16212
|
+
const deployId = process.env.KRYD_DEPLOYMENT_ID;
|
|
16213
|
+
const worker = await hatchet.worker(WORKER_NAME, {
|
|
16214
|
+
workflows: [greet],
|
|
16215
|
+
labels: deployId ? { "kryd-deploy": deployId } : undefined,
|
|
16216
|
+
});
|
|
16192
16217
|
|
|
16193
16218
|
// start() resolves when the worker stops, so it is not awaited here; waitUntilReady() flips the
|
|
16194
16219
|
// health flag once the engine has the worker. A failed registration rejects \`running\` and the
|
|
@@ -16254,6 +16279,11 @@ from hatchet_sdk import Context, EmptyModel
|
|
|
16254
16279
|
# One task, to prove the wiring end to end. Add your own beside it and list them in main.py \u2014
|
|
16255
16280
|
# everything about how a task runs (retries, timeouts, concurrency, crons, DAGs) belongs to the
|
|
16256
16281
|
# engine's SDK, not to Kryd.
|
|
16282
|
+
#
|
|
16283
|
+
# A task can be cut off by a deploy or a restart: a Python worker finishes a running task before it
|
|
16284
|
+
# stops (measured with 33 s left), but the stop has a 90 s grace period, and a task still running
|
|
16285
|
+
# after it is killed and run again from the start on another worker, even with retries=0 (measured)
|
|
16286
|
+
# \u2014 so write every task so that running it again is safe.
|
|
16257
16287
|
@hatchet.task(name="greet")
|
|
16258
16288
|
def greet(input: EmptyModel, ctx: Context) -> dict[str, str]:
|
|
16259
16289
|
return {"greeting": "Hello, world"}
|
|
@@ -16430,6 +16460,11 @@ type GreetOutput struct {
|
|
|
16430
16460
|
// Greet is one task, to prove the wiring end to end. Add your own beside it and register them in
|
|
16431
16461
|
// main.go \u2014 everything about how a task runs (retries, timeouts, concurrency, crons, DAGs) belongs
|
|
16432
16462
|
// to the engine's SDK, not to Kryd.
|
|
16463
|
+
//
|
|
16464
|
+
// A task can be cut off by a deploy or a restart: measured, a Go worker exits within a second of the
|
|
16465
|
+
// stop signal without waiting for running tasks, and the engine runs the task again from the start
|
|
16466
|
+
// on another worker ~30 s later, even with retries set to 0 \u2014 so write every task so that running it
|
|
16467
|
+
// again is safe.
|
|
16433
16468
|
func Greet(c *hatchet.Client) *hatchet.StandaloneTask {
|
|
16434
16469
|
return c.NewStandaloneTask("greet", func(ctx hatchet.Context, input GreetInput) (GreetOutput, error) {
|
|
16435
16470
|
name := input.Name
|
|
@@ -16512,7 +16547,15 @@ func main() {
|
|
|
16512
16547
|
log.Fatalf("could not create the Hatchet client: %v", err)
|
|
16513
16548
|
}
|
|
16514
16549
|
|
|
16515
|
-
worker,
|
|
16550
|
+
// Kryd sets KRYD_DEPLOYMENT_ID on a deployed worker, and the label is how its deploy gate tells
|
|
16551
|
+
// THIS deploy's worker from the previous one, which keeps running while it drains. Keep it:
|
|
16552
|
+
// the gate requires it, and a worker without it never counts as this deploy's, so the deploy
|
|
16553
|
+
// never goes live. Locally the variable is absent, so no label.
|
|
16554
|
+
opts := []hatchet.WorkerOption{hatchet.WithWorkflows(Greet(client))}
|
|
16555
|
+
if deployID := os.Getenv("KRYD_DEPLOYMENT_ID"); deployID != "" {
|
|
16556
|
+
opts = append(opts, hatchet.WithLabels(map[string]any{"kryd-deploy": deployID}))
|
|
16557
|
+
}
|
|
16558
|
+
worker, err := client.NewWorker(workerName, opts...)
|
|
16516
16559
|
if err != nil {
|
|
16517
16560
|
log.Fatalf("could not create the worker: %v", err)
|
|
16518
16561
|
}
|
|
@@ -16571,15 +16614,19 @@ var TEMPLATES = {
|
|
|
16571
16614
|
python: PYTHON_TEMPLATE,
|
|
16572
16615
|
go: GO_TEMPLATE
|
|
16573
16616
|
};
|
|
16574
|
-
function declarationFile() {
|
|
16617
|
+
function declarationFile(decl) {
|
|
16575
16618
|
return {
|
|
16576
16619
|
path: DECLARATION_FILE,
|
|
16577
|
-
contents: `${JSON.stringify(
|
|
16620
|
+
contents: `${JSON.stringify(declarationJson(decl), null, 2)}
|
|
16578
16621
|
`
|
|
16579
16622
|
};
|
|
16580
16623
|
}
|
|
16624
|
+
var WORKER_ONLY = { web: false, workflowsPath: "." };
|
|
16625
|
+
function workerFiles(language, workerName) {
|
|
16626
|
+
return TEMPLATES[language].files(workerName);
|
|
16627
|
+
}
|
|
16581
16628
|
function scaffoldFiles(language, workerName) {
|
|
16582
|
-
return [...
|
|
16629
|
+
return [...workerFiles(language, workerName), declarationFile(WORKER_ONLY)];
|
|
16583
16630
|
}
|
|
16584
16631
|
function firstCommand(language) {
|
|
16585
16632
|
return TEMPLATES[language].firstCommand;
|
|
@@ -17313,11 +17360,14 @@ Install git (https://git-scm.com/downloads), then re-run \`kryd init\` here.
|
|
|
17313
17360
|
`;
|
|
17314
17361
|
break;
|
|
17315
17362
|
}
|
|
17316
|
-
const
|
|
17363
|
+
const declaration = readDeclaration(cwd);
|
|
17364
|
+
const workerOnly = declaration !== null ? !declaration.web && declaration.workflowsPath !== null : project2.framework === "workflow";
|
|
17365
|
+
const routing = workerOnly ? `No public URL: a workflow worker dials out to the engine and serves nothing.
|
|
17317
17366
|
Scheduled and triggered work runs on your production deploy; preview branches build but do not start a worker.
|
|
17318
17367
|
` : `Production URL: https://${project2.subdomain}.${PRODUCTION_TLD}
|
|
17319
17368
|
Preview branches deploy to https://<branch>-<hash>-${project2.subdomain}.${PREVIEW_TLD}
|
|
17320
|
-
|
|
17369
|
+
` + (declaration?.workflowsPath ? `Workflows in ${declaration.workflowsPath}/ run as a worker beside it on production deploys; previews run the web app only.
|
|
17370
|
+
` : "");
|
|
17321
17371
|
process.stdout.write(
|
|
17322
17372
|
`Linked "${name}" (${project2.framework}) \u2192 ${repo.htmlUrl}
|
|
17323
17373
|
` + routing + `${linkNote}
|
|
@@ -17452,9 +17502,16 @@ async function runRuntimeLogs(opts) {
|
|
|
17452
17502
|
return;
|
|
17453
17503
|
}
|
|
17454
17504
|
const isDeployment = opts.project?.startsWith("dpl_") ?? false;
|
|
17505
|
+
if (opts.worker && isDeployment) {
|
|
17506
|
+
process.stderr.write(
|
|
17507
|
+
"--worker takes a project, not a deployment id: use `kryd logs <project> --worker` to follow the project's worker.\n"
|
|
17508
|
+
);
|
|
17509
|
+
process.exitCode = 1;
|
|
17510
|
+
return;
|
|
17511
|
+
}
|
|
17455
17512
|
const target = isDeployment ? opts.project : resolveProjectId(opts.project, opts.cwd);
|
|
17456
17513
|
if (!target) {
|
|
17457
|
-
reportNotLinked("kryd logs <projectId> --runtime");
|
|
17514
|
+
reportNotLinked(opts.worker ? "kryd logs <projectId> --worker" : "kryd logs <projectId> --runtime");
|
|
17458
17515
|
return;
|
|
17459
17516
|
}
|
|
17460
17517
|
if (opts.since !== void 0 && !/^\d+(s|m|h|d)$/.test(opts.since.trim())) {
|
|
@@ -17483,7 +17540,7 @@ async function runRuntimeLogs(opts) {
|
|
|
17483
17540
|
{
|
|
17484
17541
|
...opts.since ? { since: opts.since } : {},
|
|
17485
17542
|
signal: controller.signal,
|
|
17486
|
-
...isDeployment ? { resource: "deployment" } : {}
|
|
17543
|
+
...isDeployment ? { resource: "deployment" } : opts.worker ? { resource: "worker" } : {}
|
|
17487
17544
|
}
|
|
17488
17545
|
);
|
|
17489
17546
|
} catch (err) {
|
|
@@ -18346,6 +18403,22 @@ async function runWorkflowInit(opts) {
|
|
|
18346
18403
|
}
|
|
18347
18404
|
const language = opts.language;
|
|
18348
18405
|
const target = opts.dir ? resolve2(cwd, opts.dir) : cwd;
|
|
18406
|
+
if (!opts.only) {
|
|
18407
|
+
scaffoldContainedWorker({
|
|
18408
|
+
language,
|
|
18409
|
+
target,
|
|
18410
|
+
...opts.path !== void 0 ? { path: opts.path } : {},
|
|
18411
|
+
...opts.name !== void 0 ? { name: opts.name } : {}
|
|
18412
|
+
});
|
|
18413
|
+
return;
|
|
18414
|
+
}
|
|
18415
|
+
if (opts.path !== void 0) {
|
|
18416
|
+
process.stderr.write(
|
|
18417
|
+
"--path is for a worker inside an app; with --only the worker IS the repository root.\n"
|
|
18418
|
+
);
|
|
18419
|
+
process.exitCode = 1;
|
|
18420
|
+
return;
|
|
18421
|
+
}
|
|
18349
18422
|
const workerName = workerNameFrom(opts.name ?? basename2(target));
|
|
18350
18423
|
const files = scaffoldFiles(language, workerName);
|
|
18351
18424
|
const clashes = files.map((f) => f.path).filter((p) => existsSync3(join3(target, p)));
|
|
@@ -18391,9 +18464,10 @@ Nothing was left behind.
|
|
|
18391
18464
|
if (repo.status === "nested") {
|
|
18392
18465
|
process.stderr.write(
|
|
18393
18466
|
`That directory is inside the git repository at ${repo.root}.
|
|
18394
|
-
A worker has to be its own repository:
|
|
18395
|
-
|
|
18467
|
+
A worker-only project (--only) has to be its own repository: git would write the deploy
|
|
18468
|
+
remote into the enclosing repo, so \`kryd push\` would push that tree instead.
|
|
18396
18469
|
The files above were written \u2014 move them to a directory of their own, or run this outside that repository.
|
|
18470
|
+
To add a worker to the app in that repository instead, run \`kryd workflow init\` (without --only) at its root.
|
|
18397
18471
|
`
|
|
18398
18472
|
);
|
|
18399
18473
|
process.exitCode = 1;
|
|
@@ -18440,6 +18514,131 @@ HATCHET_CLIENT_TOKEN is injected into your container on deploy; it is never avai
|
|
|
18440
18514
|
`
|
|
18441
18515
|
);
|
|
18442
18516
|
}
|
|
18517
|
+
function scaffoldContainedWorker(opts) {
|
|
18518
|
+
const { language, target } = opts;
|
|
18519
|
+
if (!directoryIsLinked(target)) {
|
|
18520
|
+
process.stderr.write(
|
|
18521
|
+
"This directory is not linked to a Kryd project, so there is no app to add a worker to.\nLink your app first (`kryd init`), then run this again \u2014 or pass --only for a project that runs nothing but a worker.\n"
|
|
18522
|
+
);
|
|
18523
|
+
process.exitCode = 1;
|
|
18524
|
+
return;
|
|
18525
|
+
}
|
|
18526
|
+
const raw = (opts.path ?? DEFAULT_WORKFLOWS_PATH).trim();
|
|
18527
|
+
const segments = raw.split("/").filter((seg) => seg !== "" && seg !== ".");
|
|
18528
|
+
if (raw.startsWith("/") || raw.includes("\\") || segments.includes("..") || segments.length === 0) {
|
|
18529
|
+
process.stderr.write(
|
|
18530
|
+
`--path must be a folder inside this project (e.g. "${DEFAULT_WORKFLOWS_PATH}"), not "${raw}". For a worker at the repository root, use --only in a directory of its own.
|
|
18531
|
+
`
|
|
18532
|
+
);
|
|
18533
|
+
process.exitCode = 1;
|
|
18534
|
+
return;
|
|
18535
|
+
}
|
|
18536
|
+
const folder = segments.join("/");
|
|
18537
|
+
const declarationPath = join3(target, DECLARATION_FILE);
|
|
18538
|
+
let existing = {};
|
|
18539
|
+
let existingText = null;
|
|
18540
|
+
if (existsSync3(declarationPath)) {
|
|
18541
|
+
existingText = readFileSync3(declarationPath, "utf8");
|
|
18542
|
+
let parsed;
|
|
18543
|
+
try {
|
|
18544
|
+
parsed = JSON.parse(existingText);
|
|
18545
|
+
} catch {
|
|
18546
|
+
parsed = void 0;
|
|
18547
|
+
}
|
|
18548
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
18549
|
+
process.stderr.write(
|
|
18550
|
+
`${DECLARATION_FILE} is not a JSON object, so a worker cannot be added to it. Fix or remove it, then run this again.
|
|
18551
|
+
`
|
|
18552
|
+
);
|
|
18553
|
+
process.exitCode = 1;
|
|
18554
|
+
return;
|
|
18555
|
+
}
|
|
18556
|
+
existing = parsed;
|
|
18557
|
+
if ("kind" in existing) {
|
|
18558
|
+
process.stderr.write(
|
|
18559
|
+
`${DECLARATION_FILE} still uses \`{"kind":"workflow"}\`, which is no longer read. A worker-only repository is \`{"web": false, "workflows": {"path": "."}}\`; an app with workflows in a folder is \`{"workflows": {"path": "workflows"}}\`. Update it, then run this again if you still want a worker folder. Nothing was written.
|
|
18560
|
+
`
|
|
18561
|
+
);
|
|
18562
|
+
process.exitCode = 1;
|
|
18563
|
+
return;
|
|
18564
|
+
}
|
|
18565
|
+
if ("workflows" in existing || existing["web"] === false) {
|
|
18566
|
+
process.stderr.write(
|
|
18567
|
+
`${DECLARATION_FILE} already declares a worker for this project \u2014 a project has one. Nothing was written.
|
|
18568
|
+
`
|
|
18569
|
+
);
|
|
18570
|
+
process.exitCode = 1;
|
|
18571
|
+
return;
|
|
18572
|
+
}
|
|
18573
|
+
}
|
|
18574
|
+
const workerName = workerNameFrom(opts.name ?? basename2(target));
|
|
18575
|
+
const files = workerFiles(language, workerName).map((f) => ({
|
|
18576
|
+
path: `${folder}/${f.path}`,
|
|
18577
|
+
contents: f.contents
|
|
18578
|
+
}));
|
|
18579
|
+
const clashes = files.map((f) => f.path).filter((p) => existsSync3(join3(target, p)));
|
|
18580
|
+
if (clashes.length > 0) {
|
|
18581
|
+
process.stderr.write(
|
|
18582
|
+
`Refusing to overwrite: ${clashes.join(", ")}.
|
|
18583
|
+
Nothing was written. Pick another folder with --path <folder>.
|
|
18584
|
+
`
|
|
18585
|
+
);
|
|
18586
|
+
process.exitCode = 1;
|
|
18587
|
+
return;
|
|
18588
|
+
}
|
|
18589
|
+
const declaration = { ...existing, workflows: { path: folder } };
|
|
18590
|
+
const written = [];
|
|
18591
|
+
try {
|
|
18592
|
+
for (const file2 of files) {
|
|
18593
|
+
const full = join3(target, file2.path);
|
|
18594
|
+
mkdirSync2(dirname2(full), { recursive: true });
|
|
18595
|
+
writeFileSync2(full, file2.contents);
|
|
18596
|
+
written.push(full);
|
|
18597
|
+
}
|
|
18598
|
+
writeFileSync2(declarationPath, `${JSON.stringify(declaration, null, 2)}
|
|
18599
|
+
`);
|
|
18600
|
+
} catch (err) {
|
|
18601
|
+
for (const path of written.reverse()) {
|
|
18602
|
+
try {
|
|
18603
|
+
rmSync2(path);
|
|
18604
|
+
} catch {
|
|
18605
|
+
}
|
|
18606
|
+
}
|
|
18607
|
+
try {
|
|
18608
|
+
if (existingText === null) rmSync2(declarationPath, { force: true });
|
|
18609
|
+
else writeFileSync2(declarationPath, existingText);
|
|
18610
|
+
} catch {
|
|
18611
|
+
}
|
|
18612
|
+
process.stderr.write(
|
|
18613
|
+
`Could not write the scaffold (${err instanceof Error ? err.message : String(err)}).
|
|
18614
|
+
Nothing was left behind.
|
|
18615
|
+
`
|
|
18616
|
+
);
|
|
18617
|
+
process.exitCode = 1;
|
|
18618
|
+
return;
|
|
18619
|
+
}
|
|
18620
|
+
const next = [];
|
|
18621
|
+
const first = firstCommand(language);
|
|
18622
|
+
if (first) next.push(`(cd ${folder} && ${first})`);
|
|
18623
|
+
next.push(
|
|
18624
|
+
"kryd workflow add",
|
|
18625
|
+
`git add ${DECLARATION_FILE} ${folder} && git commit -m "add workflows"`,
|
|
18626
|
+
"kryd push"
|
|
18627
|
+
);
|
|
18628
|
+
process.stdout.write(
|
|
18629
|
+
`Scaffolded a ${language} workflow worker in ${folder}/:
|
|
18630
|
+
` + files.map((f) => ` ${f.path}
|
|
18631
|
+
`).join("") + `and declared it in ${DECLARATION_FILE}: ${JSON.stringify({ workflows: { path: folder } })}
|
|
18632
|
+
|
|
18633
|
+
Every production push now builds your app AND this worker, and goes live only when both are healthy. Previews run the app only.
|
|
18634
|
+
Next: ${next.map((c) => `\`${c}\``).join(", then ")}.
|
|
18635
|
+
HATCHET_CLIENT_TOKEN is injected into your app and your worker on deploy \u2014 your app triggers tasks with it; it is never available locally.
|
|
18636
|
+
` + // The app is still built (and type-checked) from the root, so a tsconfig that includes every
|
|
18637
|
+
// file would compile the worker too — against an SDK the app does not depend on.
|
|
18638
|
+
(existsSync3(join3(target, "tsconfig.json")) ? `Add "${folder}" to the "exclude" of your tsconfig.json, so your app's build does not type-check the worker.
|
|
18639
|
+
` : "")
|
|
18640
|
+
);
|
|
18641
|
+
}
|
|
18443
18642
|
async function runStorageRemove(opts) {
|
|
18444
18643
|
const apiUrl = resolveApiUrl(opts.apiUrl);
|
|
18445
18644
|
const token = loadConfig().token;
|
|
@@ -18926,7 +19125,7 @@ Run \`kryd env list\` to see what is set. (If you meant the runtime variable of
|
|
|
18926
19125
|
await reportError(err);
|
|
18927
19126
|
}
|
|
18928
19127
|
}
|
|
18929
|
-
var CLI_VERSION = true ? "0.
|
|
19128
|
+
var CLI_VERSION = true ? "0.9.0" : "0.0.0-dev";
|
|
18930
19129
|
var program = new Command();
|
|
18931
19130
|
program.name("kryd").description("Kryd CLI").version(CLI_VERSION);
|
|
18932
19131
|
program.hook("postAction", async () => {
|
|
@@ -18948,7 +19147,7 @@ program.command("git-credential <operation>", { hidden: true }).description("git
|
|
|
18948
19147
|
});
|
|
18949
19148
|
program.command("init").description("Link this project's repo + register its push webhook").option("--name <name>", "project name (defaults to package.json name / dir)").option(
|
|
18950
19149
|
"--framework <framework>",
|
|
18951
|
-
|
|
19150
|
+
'react-router | nextjs | vite-spa | node (auto-detected if omitted; `workflow` comes from a committed kryd.json with "web": false)'
|
|
18952
19151
|
).option(
|
|
18953
19152
|
"--tenant <slug>",
|
|
18954
19153
|
// KRYD-265 / KRYD-188: this used to say "claimed once on first init", which was false — signup
|
|
@@ -18961,15 +19160,23 @@ program.command("init").description("Link this project's repo + register its pus
|
|
|
18961
19160
|
"create the repository PUBLIC \u2014 readable by anyone signed in to the forge (default: private). Set at creation only"
|
|
18962
19161
|
).option("--api-url <url>", "control-plane API base URL").action((opts) => runInit(opts));
|
|
18963
19162
|
program.command("logs [target]").description(
|
|
18964
|
-
"Follow logs. Default: a deploy's build/deploy logs ([target]=deployment id, latest if omitted). With --runtime: a container's stdout/stderr \u2014 [target]=a project id tails its live app, a deployment id (dpl_\u2026) tails THAT deploy's container (incl. a failed one, to see why it crashed)."
|
|
19163
|
+
"Follow logs. Default: a deploy's build/deploy logs ([target]=deployment id, latest if omitted). With --runtime: a container's stdout/stderr \u2014 [target]=a project id tails its live app, a deployment id (dpl_\u2026) tails THAT deploy's container (incl. a failed one, to see why it crashed). With --worker: a project's workflow worker, also when the project has a web app."
|
|
18965
19164
|
).option(
|
|
18966
19165
|
"--runtime",
|
|
18967
19166
|
"stream a container's runtime stdout/stderr instead of build/deploy logs \u2014 the live app (project id) or one deploy's container (dpl_ id)"
|
|
19167
|
+
).option(
|
|
19168
|
+
"--worker",
|
|
19169
|
+
"with a project id: stream its workflow WORKER's stdout/stderr (implies --runtime) \u2014 for a project with a web app too, --runtime shows the web app"
|
|
18968
19170
|
).option(
|
|
18969
19171
|
"--since <dur>",
|
|
18970
|
-
"with --runtime: backfill this window before going live (e.g. 30m, 2h, 1d; max 7d)"
|
|
19172
|
+
"with --runtime/--worker: backfill this window before going live (e.g. 30m, 2h, 1d; max 7d)"
|
|
18971
19173
|
).option("--api-url <url>", "control-plane API base URL").action(
|
|
18972
|
-
(target, opts) => opts.runtime
|
|
19174
|
+
(target, opts) => opts.runtime || opts.worker ? runRuntimeLogs({
|
|
19175
|
+
project: target,
|
|
19176
|
+
since: opts.since,
|
|
19177
|
+
apiUrl: opts.apiUrl,
|
|
19178
|
+
...opts.worker ? { worker: true } : {}
|
|
19179
|
+
}) : runLogs({ apiUrl: opts.apiUrl, deployment: target })
|
|
18973
19180
|
);
|
|
18974
19181
|
program.command("push [branch]").description(
|
|
18975
19182
|
"Push to the kryd remote and follow the deploy it triggers (defaults to the current branch)"
|
|
@@ -19038,11 +19245,11 @@ workflow.command("add [project]").description("Give the project a workflow tenan
|
|
|
19038
19245
|
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 }));
|
|
19039
19246
|
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 }));
|
|
19040
19247
|
workflow.command("init").description(
|
|
19041
|
-
"
|
|
19248
|
+
"Add a workflow worker to the linked app here (in ./workflows, declared in kryd.json); --only scaffolds a worker-only project instead and links it"
|
|
19042
19249
|
).requiredOption(
|
|
19043
19250
|
"--language <language>",
|
|
19044
19251
|
`worker language: ${SCAFFOLD_LANGUAGES.join(" | ")}`
|
|
19045
|
-
).option("--dir <path>", "
|
|
19252
|
+
).option("--path <folder>", "the worker's folder inside the app (default: workflows)").option("--only", "a project that runs ONLY a worker: scaffold it at the root, git init, and link").option("--dir <path>", "the project directory, instead of the current one").option("--name <name>", "worker name (and, with --only, project name); defaults to the directory name").option("--no-link", "with --only: only write the files; do not create a Kryd project").option("--api-url <url>", "control-plane API base URL").action((opts) => runWorkflowInit(opts));
|
|
19046
19253
|
function invokedDirectly() {
|
|
19047
19254
|
const entry = process.argv[1];
|
|
19048
19255
|
if (!entry) return false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kryd/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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",
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
"typescript": "^5.6.3",
|
|
51
51
|
"vitest": "^2.1.8",
|
|
52
52
|
"@kryd/shared-types": "0.0.0",
|
|
53
|
-
"@kryd/config-
|
|
54
|
-
"@kryd/config-
|
|
53
|
+
"@kryd/config-eslint": "0.0.0",
|
|
54
|
+
"@kryd/config-ts": "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",
|