@kryd/cli 0.7.0 → 0.8.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 +1 -0
- package/dist/index.js +732 -25
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -39,6 +39,7 @@ Once you've run `kryd init` in a directory, the commands below work **arg-less**
|
|
|
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
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
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. |
|
|
42
|
+
| `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. |
|
|
42
43
|
|
|
43
44
|
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).
|
|
44
45
|
|
package/dist/index.js
CHANGED
|
@@ -14895,8 +14895,14 @@ function isTerminalResourceStatus(status) {
|
|
|
14895
14895
|
}
|
|
14896
14896
|
|
|
14897
14897
|
// src/index.ts
|
|
14898
|
-
import {
|
|
14899
|
-
|
|
14898
|
+
import {
|
|
14899
|
+
existsSync as existsSync3,
|
|
14900
|
+
mkdirSync as mkdirSync2,
|
|
14901
|
+
realpathSync,
|
|
14902
|
+
rmSync as rmSync2,
|
|
14903
|
+
writeFileSync as writeFileSync2
|
|
14904
|
+
} from "node:fs";
|
|
14905
|
+
import { basename as basename2, dirname as dirname2, isAbsolute, join as join3, relative, resolve as resolve2 } from "node:path";
|
|
14900
14906
|
import { pathToFileURL } from "node:url";
|
|
14901
14907
|
|
|
14902
14908
|
// src/config.ts
|
|
@@ -15018,6 +15024,11 @@ function ensureIgnored(cwd, entry, aliases = []) {
|
|
|
15018
15024
|
function ensureKrydIgnored(cwd) {
|
|
15019
15025
|
ensureIgnored(cwd, `${PROJECT_LINK_DIR}/`, [PROJECT_LINK_DIR]);
|
|
15020
15026
|
}
|
|
15027
|
+
function directoryIsLinked(cwd) {
|
|
15028
|
+
const path = join(cwd, PROJECT_LINK_DIR, PROJECT_LINK_FILE);
|
|
15029
|
+
if (!existsSync(path)) return null;
|
|
15030
|
+
return loadProjectLink(cwd);
|
|
15031
|
+
}
|
|
15021
15032
|
function writeSecretFile(path, contents) {
|
|
15022
15033
|
mkdirSync(dirname(path), { recursive: true });
|
|
15023
15034
|
writeFileSync(path, contents, { mode: 384 });
|
|
@@ -15058,7 +15069,7 @@ function browserLogin(dashboardUrl, opts = {}) {
|
|
|
15058
15069
|
const state = randomUUID();
|
|
15059
15070
|
const timeoutMs = opts.timeoutMs ?? 12e4;
|
|
15060
15071
|
const shouldOpen = opts.open ?? true;
|
|
15061
|
-
return new Promise((
|
|
15072
|
+
return new Promise((resolve3, reject) => {
|
|
15062
15073
|
let settled = false;
|
|
15063
15074
|
const settle = (fn) => {
|
|
15064
15075
|
if (settled) return;
|
|
@@ -15090,7 +15101,7 @@ function browserLogin(dashboardUrl, opts = {}) {
|
|
|
15090
15101
|
res.end(SUCCESS_HTML, () => {
|
|
15091
15102
|
settle(() => {
|
|
15092
15103
|
shutdown();
|
|
15093
|
-
|
|
15104
|
+
resolve3({ token, ...apiUrl ? { apiUrl } : {} });
|
|
15094
15105
|
});
|
|
15095
15106
|
});
|
|
15096
15107
|
});
|
|
@@ -15764,14 +15775,14 @@ async function* readSseFrames(body) {
|
|
|
15764
15775
|
}
|
|
15765
15776
|
function abortableSleep(ms, signal) {
|
|
15766
15777
|
if (signal?.aborted) return Promise.resolve();
|
|
15767
|
-
return new Promise((
|
|
15778
|
+
return new Promise((resolve3) => {
|
|
15768
15779
|
const onAbort = () => {
|
|
15769
15780
|
clearTimeout(timer);
|
|
15770
|
-
|
|
15781
|
+
resolve3();
|
|
15771
15782
|
};
|
|
15772
15783
|
const timer = setTimeout(() => {
|
|
15773
15784
|
signal?.removeEventListener("abort", onAbort);
|
|
15774
|
-
|
|
15785
|
+
resolve3();
|
|
15775
15786
|
}, ms);
|
|
15776
15787
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
15777
15788
|
});
|
|
@@ -15873,11 +15884,14 @@ var FRAMEWORKS = [
|
|
|
15873
15884
|
"react-router",
|
|
15874
15885
|
"nextjs",
|
|
15875
15886
|
"vite-spa",
|
|
15876
|
-
"node"
|
|
15887
|
+
"node",
|
|
15888
|
+
"workflow"
|
|
15877
15889
|
];
|
|
15878
15890
|
function isFramework(value) {
|
|
15879
15891
|
return FRAMEWORKS.includes(value);
|
|
15880
15892
|
}
|
|
15893
|
+
var DECLARATION_FILE = "kryd.json";
|
|
15894
|
+
var DECLARATION_KIND_WORKFLOW = "workflow";
|
|
15881
15895
|
var VITE_META_FRAMEWORKS = [
|
|
15882
15896
|
"@sveltejs/kit",
|
|
15883
15897
|
"astro",
|
|
@@ -15919,7 +15933,22 @@ function readPackageJson(cwd) {
|
|
|
15919
15933
|
return null;
|
|
15920
15934
|
}
|
|
15921
15935
|
}
|
|
15936
|
+
function declaredKind(cwd) {
|
|
15937
|
+
try {
|
|
15938
|
+
const parsed = JSON.parse(
|
|
15939
|
+
readFileSync2(join2(cwd, DECLARATION_FILE), "utf8")
|
|
15940
|
+
);
|
|
15941
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
15942
|
+
return null;
|
|
15943
|
+
}
|
|
15944
|
+
const kind = parsed.kind;
|
|
15945
|
+
return typeof kind === "string" ? kind : null;
|
|
15946
|
+
} catch {
|
|
15947
|
+
return null;
|
|
15948
|
+
}
|
|
15949
|
+
}
|
|
15922
15950
|
function frameworkFrom(pkg, cwd) {
|
|
15951
|
+
if (declaredKind(cwd) === DECLARATION_KIND_WORKFLOW) return "workflow";
|
|
15923
15952
|
const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
|
|
15924
15953
|
const has = (name) => name in deps;
|
|
15925
15954
|
if (has("next")) return "nextjs";
|
|
@@ -15941,6 +15970,528 @@ function inspectProject(cwd) {
|
|
|
15941
15970
|
return { framework: frameworkFrom(pkg, cwd), name: nameFrom(pkg, cwd) };
|
|
15942
15971
|
}
|
|
15943
15972
|
|
|
15973
|
+
// src/scaffold/typescript.ts
|
|
15974
|
+
var TYPESCRIPT_TEMPLATE = {
|
|
15975
|
+
files: (workerName) => [
|
|
15976
|
+
{
|
|
15977
|
+
path: "package.json",
|
|
15978
|
+
contents: `${JSON.stringify(
|
|
15979
|
+
{
|
|
15980
|
+
name: workerName,
|
|
15981
|
+
private: true,
|
|
15982
|
+
version: "0.1.0",
|
|
15983
|
+
scripts: {
|
|
15984
|
+
build: "tsc",
|
|
15985
|
+
start: "node dist/worker.js"
|
|
15986
|
+
},
|
|
15987
|
+
engines: { node: ">=20" },
|
|
15988
|
+
dependencies: {
|
|
15989
|
+
"@hatchet-dev/typescript-sdk": "^1.33.0"
|
|
15990
|
+
},
|
|
15991
|
+
devDependencies: {
|
|
15992
|
+
"@types/node": "^22.10.2",
|
|
15993
|
+
typescript: "^5.7.2"
|
|
15994
|
+
}
|
|
15995
|
+
},
|
|
15996
|
+
null,
|
|
15997
|
+
2
|
|
15998
|
+
)}
|
|
15999
|
+
`
|
|
16000
|
+
},
|
|
16001
|
+
{
|
|
16002
|
+
path: "tsconfig.json",
|
|
16003
|
+
contents: `${JSON.stringify(
|
|
16004
|
+
{
|
|
16005
|
+
compilerOptions: {
|
|
16006
|
+
// CommonJS because the SDK is: it ships no "exports" map and no "type": "module".
|
|
16007
|
+
module: "commonjs",
|
|
16008
|
+
moduleResolution: "node",
|
|
16009
|
+
target: "es2022",
|
|
16010
|
+
lib: ["es2022"],
|
|
16011
|
+
outDir: "dist",
|
|
16012
|
+
rootDir: "src",
|
|
16013
|
+
strict: true,
|
|
16014
|
+
esModuleInterop: true,
|
|
16015
|
+
skipLibCheck: true
|
|
16016
|
+
},
|
|
16017
|
+
include: ["src"]
|
|
16018
|
+
},
|
|
16019
|
+
null,
|
|
16020
|
+
2
|
|
16021
|
+
)}
|
|
16022
|
+
`
|
|
16023
|
+
},
|
|
16024
|
+
{
|
|
16025
|
+
path: "src/hatchet.ts",
|
|
16026
|
+
contents: `import { HatchetClient } from "@hatchet-dev/typescript-sdk/v1";
|
|
16027
|
+
|
|
16028
|
+
/**
|
|
16029
|
+
* Reads HATCHET_CLIENT_TOKEN from the environment and needs nothing else: the token carries the
|
|
16030
|
+
* engine's addresses. Kryd injects it on every deploy once you have run \`kryd workflow add\`.
|
|
16031
|
+
*
|
|
16032
|
+
* It lives in its own file so tasks and the worker can share one client without importing each
|
|
16033
|
+
* other in a circle.
|
|
16034
|
+
*/
|
|
16035
|
+
export const hatchet = HatchetClient.init();
|
|
16036
|
+
`
|
|
16037
|
+
},
|
|
16038
|
+
{
|
|
16039
|
+
path: "src/tasks.ts",
|
|
16040
|
+
contents: `import { hatchet } from "./hatchet";
|
|
16041
|
+
|
|
16042
|
+
/**
|
|
16043
|
+
* One task, to prove the wiring end to end. Add your own beside it and list them in
|
|
16044
|
+
* \`src/worker.ts\` \u2014 everything about how a task runs (retries, timeouts, concurrency, crons,
|
|
16045
|
+
* DAGs) belongs to the engine's SDK, not to Kryd.
|
|
16046
|
+
*/
|
|
16047
|
+
export const greet = hatchet.task({
|
|
16048
|
+
name: "greet",
|
|
16049
|
+
fn: (input: { name?: string }) => ({
|
|
16050
|
+
greeting: \`Hello, \${input.name ?? "world"}\`,
|
|
16051
|
+
}),
|
|
16052
|
+
});
|
|
16053
|
+
`
|
|
16054
|
+
},
|
|
16055
|
+
{
|
|
16056
|
+
path: "src/worker.ts",
|
|
16057
|
+
contents: `import { createServer } from "node:http";
|
|
16058
|
+
|
|
16059
|
+
const WORKER_NAME = ${JSON.stringify(workerName)};
|
|
16060
|
+
|
|
16061
|
+
/*
|
|
16062
|
+
* The health listener, and the one piece of this file that is about Kryd rather than about workflows.
|
|
16063
|
+
*
|
|
16064
|
+
* A worker holds an outbound gRPC session and listens on no port of its own, but Kryd decides a
|
|
16065
|
+
* deploy is live by asking GET / for a 2xx. Without this the deploy is torn down with
|
|
16066
|
+
* "connection refused (nothing listening yet)".
|
|
16067
|
+
*
|
|
16068
|
+
* \u{1F6A8} It answers 503 until the engine has registered the worker, on purpose. Answering 200 straight
|
|
16069
|
+
* away would mark the deploy live a moment before a failed registration crashes the process \u2014 which
|
|
16070
|
+
* is exactly what happened while this was being measured.
|
|
16071
|
+
*/
|
|
16072
|
+
let registered = false;
|
|
16073
|
+
|
|
16074
|
+
createServer((_req, res) => {
|
|
16075
|
+
res.writeHead(registered ? 200 : 503, { "content-type": "application/json" });
|
|
16076
|
+
res.end(
|
|
16077
|
+
JSON.stringify({
|
|
16078
|
+
worker: WORKER_NAME,
|
|
16079
|
+
status: registered ? "registered" : "starting",
|
|
16080
|
+
}),
|
|
16081
|
+
);
|
|
16082
|
+
}).listen(Number(process.env.PORT ?? 8080), "0.0.0.0");
|
|
16083
|
+
|
|
16084
|
+
async function main(): Promise<void> {
|
|
16085
|
+
// Imported here rather than at the top of the file, and that is deliberate: creating the client
|
|
16086
|
+
// reads and decodes HATCHET_CLIENT_TOKEN, and a token it cannot parse makes it throw during
|
|
16087
|
+
// module evaluation \u2014 before the listener above ever binds. The deploy would then fail with
|
|
16088
|
+
// "connection refused (nothing listening yet)", which is the one message this whole scaffold
|
|
16089
|
+
// exists to stop you seeing. Binding first means a bad token shows up as a 503 plus a readable
|
|
16090
|
+
// error in your deploy log.
|
|
16091
|
+
const { hatchet } = await import("./hatchet");
|
|
16092
|
+
const { greet } = await import("./tasks");
|
|
16093
|
+
|
|
16094
|
+
const worker = await hatchet.worker(WORKER_NAME, { workflows: [greet] });
|
|
16095
|
+
|
|
16096
|
+
// start() resolves when the worker stops, so it is not awaited here; waitUntilReady() flips the
|
|
16097
|
+
// health flag once the engine has the worker. A failed registration rejects \`running\` and the
|
|
16098
|
+
// process exits non-zero, which fails the deploy loudly instead of going live and crash-looping.
|
|
16099
|
+
const running = worker.start();
|
|
16100
|
+
void worker.waitUntilReady().then(
|
|
16101
|
+
() => {
|
|
16102
|
+
registered = true;
|
|
16103
|
+
},
|
|
16104
|
+
() => {
|
|
16105
|
+
// Stays 503. The deploy gate reports it; there is nothing useful to do here.
|
|
16106
|
+
},
|
|
16107
|
+
);
|
|
16108
|
+
await running;
|
|
16109
|
+
}
|
|
16110
|
+
|
|
16111
|
+
main().catch((err: unknown) => {
|
|
16112
|
+
console.error("[worker] stopped:", err);
|
|
16113
|
+
process.exit(1);
|
|
16114
|
+
});
|
|
16115
|
+
`
|
|
16116
|
+
},
|
|
16117
|
+
{
|
|
16118
|
+
path: ".gitignore",
|
|
16119
|
+
contents: `node_modules/
|
|
16120
|
+
dist/
|
|
16121
|
+
.env
|
|
16122
|
+
.env.local
|
|
16123
|
+
`
|
|
16124
|
+
},
|
|
16125
|
+
{
|
|
16126
|
+
path: "README.md",
|
|
16127
|
+
contents: `# ${workerName}
|
|
16128
|
+
|
|
16129
|
+
A Hatchet workflow worker, ready to deploy on Kryd.
|
|
16130
|
+
|
|
16131
|
+
- \`src/tasks.ts\` holds your tasks. \`src/worker.ts\` starts the worker and serves the health
|
|
16132
|
+
endpoint Kryd's deploy gate needs \u2014 keep that listener, or your deploys will be torn down.
|
|
16133
|
+
- **\`HATCHET_CLIENT_TOKEN\` only exists inside a deployed container.** Kryd never prints it, so you
|
|
16134
|
+
cannot run this worker against your tenant from your laptop today.
|
|
16135
|
+
- Deploy it: \`kryd workflow add\` once, then \`kryd push\`.
|
|
16136
|
+
|
|
16137
|
+
Full docs: https://docs.kryd.eu/docs/workflows
|
|
16138
|
+
`
|
|
16139
|
+
}
|
|
16140
|
+
]
|
|
16141
|
+
};
|
|
16142
|
+
|
|
16143
|
+
// src/scaffold/python.ts
|
|
16144
|
+
var PYTHON_TEMPLATE = {
|
|
16145
|
+
files: (workerName) => [
|
|
16146
|
+
{
|
|
16147
|
+
path: "requirements.txt",
|
|
16148
|
+
contents: `hatchet-sdk>=1.40.1
|
|
16149
|
+
`
|
|
16150
|
+
},
|
|
16151
|
+
{
|
|
16152
|
+
path: "tasks.py",
|
|
16153
|
+
contents: `from hatchet_client import hatchet
|
|
16154
|
+
from hatchet_sdk import Context, EmptyModel
|
|
16155
|
+
|
|
16156
|
+
|
|
16157
|
+
# One task, to prove the wiring end to end. Add your own beside it and list them in main.py \u2014
|
|
16158
|
+
# everything about how a task runs (retries, timeouts, concurrency, crons, DAGs) belongs to the
|
|
16159
|
+
# engine's SDK, not to Kryd.
|
|
16160
|
+
@hatchet.task(name="greet")
|
|
16161
|
+
def greet(input: EmptyModel, ctx: Context) -> dict[str, str]:
|
|
16162
|
+
return {"greeting": "Hello, world"}
|
|
16163
|
+
`
|
|
16164
|
+
},
|
|
16165
|
+
{
|
|
16166
|
+
path: "hatchet_client.py",
|
|
16167
|
+
contents: `from hatchet_sdk import Hatchet
|
|
16168
|
+
|
|
16169
|
+
# Reads HATCHET_CLIENT_TOKEN from the environment and needs nothing else: the token carries the
|
|
16170
|
+
# engine's addresses. Kryd injects it on every deploy once you have run \`kryd workflow add\`.
|
|
16171
|
+
#
|
|
16172
|
+
# It lives in its own module so tasks and the worker can share one client without importing each
|
|
16173
|
+
# other in a circle.
|
|
16174
|
+
hatchet = Hatchet()
|
|
16175
|
+
`
|
|
16176
|
+
},
|
|
16177
|
+
{
|
|
16178
|
+
path: "main.py",
|
|
16179
|
+
contents: `"""Worker entry point.
|
|
16180
|
+
|
|
16181
|
+
\u{1F6A8} This file must stay at the project root and keep this name: Kryd's builder derives the start
|
|
16182
|
+
command by finding main.py here. Move it into a package and the deploy has no start command at all.
|
|
16183
|
+
"""
|
|
16184
|
+
|
|
16185
|
+
import json
|
|
16186
|
+
import os
|
|
16187
|
+
import threading
|
|
16188
|
+
import time
|
|
16189
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
16190
|
+
|
|
16191
|
+
WORKER_NAME = ${JSON.stringify(workerName)}
|
|
16192
|
+
|
|
16193
|
+
# The health listener, and the one piece of this file that is about Kryd rather than about workflows.
|
|
16194
|
+
#
|
|
16195
|
+
# A worker holds an outbound gRPC session and listens on no port of its own, but Kryd decides a
|
|
16196
|
+
# deploy is live by asking GET / for a 2xx. Without this the deploy is torn down with
|
|
16197
|
+
# "connection refused (nothing listening yet)".
|
|
16198
|
+
#
|
|
16199
|
+
# \u{1F6A8} It answers 503 until the engine has registered the worker, on purpose. Answering 200 straight
|
|
16200
|
+
# away would mark the deploy live a moment before a failed registration crashes the process.
|
|
16201
|
+
_registered = threading.Event()
|
|
16202
|
+
|
|
16203
|
+
|
|
16204
|
+
class _Health(BaseHTTPRequestHandler):
|
|
16205
|
+
def do_GET(self) -> None: # noqa: N802 - BaseHTTPRequestHandler's spelling
|
|
16206
|
+
ready = _registered.is_set()
|
|
16207
|
+
body = json.dumps(
|
|
16208
|
+
{"worker": WORKER_NAME, "status": "registered" if ready else "starting"}
|
|
16209
|
+
).encode()
|
|
16210
|
+
self.send_response(200 if ready else 503)
|
|
16211
|
+
self.send_header("content-type", "application/json")
|
|
16212
|
+
self.send_header("content-length", str(len(body)))
|
|
16213
|
+
self.end_headers()
|
|
16214
|
+
self.wfile.write(body)
|
|
16215
|
+
|
|
16216
|
+
def log_message(self, *args: object) -> None:
|
|
16217
|
+
# The default handler writes every request to stderr, which would drown the worker's own
|
|
16218
|
+
# logs: the deploy gate polls this endpoint every two seconds.
|
|
16219
|
+
pass
|
|
16220
|
+
|
|
16221
|
+
|
|
16222
|
+
def _serve_health() -> None:
|
|
16223
|
+
port = int(os.environ.get("PORT", "8080"))
|
|
16224
|
+
HTTPServer(("0.0.0.0", port), _Health).serve_forever()
|
|
16225
|
+
|
|
16226
|
+
|
|
16227
|
+
def _watch_registration(worker: object) -> None:
|
|
16228
|
+
# worker.start() blocks, so readiness is observed from a thread. The status enum is compared by
|
|
16229
|
+
# name rather than imported: it lives at a private-ish path in the SDK and this keeps the
|
|
16230
|
+
# scaffold working across SDK reshuffles.
|
|
16231
|
+
#
|
|
16232
|
+
# \u{1F6A8} If the SDK ever renames or drops .status, this loop would spin forever, / would answer 503
|
|
16233
|
+
# forever, and the deploy would be failed by the health gate after three minutes with nothing in
|
|
16234
|
+
# the log to explain it. Say so once instead: a deploy that fails is fine, a deploy that fails
|
|
16235
|
+
# silently is not.
|
|
16236
|
+
if not hasattr(worker, "status"):
|
|
16237
|
+
print(
|
|
16238
|
+
"[worker] this SDK build has no 'status' attribute, so readiness cannot be observed; "
|
|
16239
|
+
"the health endpoint will stay 503 and Kryd will fail the deploy.",
|
|
16240
|
+
flush=True,
|
|
16241
|
+
)
|
|
16242
|
+
return
|
|
16243
|
+
|
|
16244
|
+
while not _registered.is_set():
|
|
16245
|
+
if getattr(getattr(worker, "status", None), "name", "") == "HEALTHY":
|
|
16246
|
+
_registered.set()
|
|
16247
|
+
return
|
|
16248
|
+
time.sleep(0.2)
|
|
16249
|
+
|
|
16250
|
+
|
|
16251
|
+
def main() -> None:
|
|
16252
|
+
threading.Thread(target=_serve_health, daemon=True).start()
|
|
16253
|
+
|
|
16254
|
+
# Imported here rather than at the top of the file, and that is deliberate: creating the client
|
|
16255
|
+
# reads and decodes HATCHET_CLIENT_TOKEN, and a token it cannot parse raises during import \u2014
|
|
16256
|
+
# before the listener above ever binds. The deploy would then fail with "connection refused
|
|
16257
|
+
# (nothing listening yet)", which is the one message this whole scaffold exists to stop you
|
|
16258
|
+
# seeing. Binding first means a bad token shows up as a 503 plus a readable error in your log.
|
|
16259
|
+
from hatchet_client import hatchet
|
|
16260
|
+
from tasks import greet
|
|
16261
|
+
|
|
16262
|
+
worker = hatchet.worker(WORKER_NAME, workflows=[greet])
|
|
16263
|
+
threading.Thread(target=_watch_registration, args=(worker,), daemon=True).start()
|
|
16264
|
+
worker.start()
|
|
16265
|
+
|
|
16266
|
+
|
|
16267
|
+
if __name__ == "__main__":
|
|
16268
|
+
main()
|
|
16269
|
+
`
|
|
16270
|
+
},
|
|
16271
|
+
{
|
|
16272
|
+
path: ".gitignore",
|
|
16273
|
+
contents: `__pycache__/
|
|
16274
|
+
*.py[cod]
|
|
16275
|
+
.venv/
|
|
16276
|
+
venv/
|
|
16277
|
+
.env
|
|
16278
|
+
.env.local
|
|
16279
|
+
`
|
|
16280
|
+
},
|
|
16281
|
+
{
|
|
16282
|
+
path: "README.md",
|
|
16283
|
+
contents: `# ${workerName}
|
|
16284
|
+
|
|
16285
|
+
A Hatchet workflow worker, ready to deploy on Kryd.
|
|
16286
|
+
|
|
16287
|
+
- \`tasks.py\` holds your tasks. \`main.py\` starts the worker and serves the health endpoint Kryd's
|
|
16288
|
+
deploy gate needs \u2014 keep that listener, and keep \`main.py\` at the root under that name, or your
|
|
16289
|
+
deploys will fail.
|
|
16290
|
+
- **\`HATCHET_CLIENT_TOKEN\` only exists inside a deployed container.** Kryd never prints it, so you
|
|
16291
|
+
cannot run this worker against your tenant from your laptop today.
|
|
16292
|
+
- Deploy it: \`kryd workflow add\` once, then \`kryd push\`.
|
|
16293
|
+
|
|
16294
|
+
Full docs: https://docs.kryd.eu/docs/workflows
|
|
16295
|
+
`
|
|
16296
|
+
}
|
|
16297
|
+
]
|
|
16298
|
+
};
|
|
16299
|
+
|
|
16300
|
+
// src/scaffold/go.ts
|
|
16301
|
+
var GO_TEMPLATE = {
|
|
16302
|
+
firstCommand: "go mod tidy",
|
|
16303
|
+
files: (workerName) => [
|
|
16304
|
+
{
|
|
16305
|
+
path: "go.mod",
|
|
16306
|
+
contents: `module kryd.local/${workerName}
|
|
16307
|
+
|
|
16308
|
+
go 1.26
|
|
16309
|
+
|
|
16310
|
+
toolchain go1.26.0
|
|
16311
|
+
|
|
16312
|
+
require github.com/hatchet-dev/hatchet v0.106.10
|
|
16313
|
+
`
|
|
16314
|
+
},
|
|
16315
|
+
{
|
|
16316
|
+
path: "tasks.go",
|
|
16317
|
+
contents: `package main
|
|
16318
|
+
|
|
16319
|
+
import (
|
|
16320
|
+
hatchet "github.com/hatchet-dev/hatchet/sdks/go"
|
|
16321
|
+
)
|
|
16322
|
+
|
|
16323
|
+
// GreetInput is what a run is triggered with.
|
|
16324
|
+
type GreetInput struct {
|
|
16325
|
+
Name string \`json:"name"\`
|
|
16326
|
+
}
|
|
16327
|
+
|
|
16328
|
+
// GreetOutput is what the task returns.
|
|
16329
|
+
type GreetOutput struct {
|
|
16330
|
+
Greeting string \`json:"greeting"\`
|
|
16331
|
+
}
|
|
16332
|
+
|
|
16333
|
+
// Greet is one task, to prove the wiring end to end. Add your own beside it and register them in
|
|
16334
|
+
// main.go \u2014 everything about how a task runs (retries, timeouts, concurrency, crons, DAGs) belongs
|
|
16335
|
+
// to the engine's SDK, not to Kryd.
|
|
16336
|
+
func Greet(c *hatchet.Client) *hatchet.StandaloneTask {
|
|
16337
|
+
return c.NewStandaloneTask("greet", func(ctx hatchet.Context, input GreetInput) (GreetOutput, error) {
|
|
16338
|
+
name := input.Name
|
|
16339
|
+
if name == "" {
|
|
16340
|
+
name = "world"
|
|
16341
|
+
}
|
|
16342
|
+
return GreetOutput{Greeting: "Hello, " + name}, nil
|
|
16343
|
+
})
|
|
16344
|
+
}
|
|
16345
|
+
`
|
|
16346
|
+
},
|
|
16347
|
+
{
|
|
16348
|
+
path: "main.go",
|
|
16349
|
+
contents: `// Worker entry point.
|
|
16350
|
+
//
|
|
16351
|
+
// \u{1F6A8} This file must stay at the project root: Kryd's builder compiles the root package when one
|
|
16352
|
+
// exists, and otherwise picks the first cmd/* directory alphabetically \u2014 which is how a trigger
|
|
16353
|
+
// script ends up deployed instead of the worker.
|
|
16354
|
+
package main
|
|
16355
|
+
|
|
16356
|
+
import (
|
|
16357
|
+
"encoding/json"
|
|
16358
|
+
"log"
|
|
16359
|
+
"net/http"
|
|
16360
|
+
"os"
|
|
16361
|
+
"sync/atomic"
|
|
16362
|
+
|
|
16363
|
+
"github.com/hatchet-dev/hatchet/pkg/cmdutils"
|
|
16364
|
+
hatchet "github.com/hatchet-dev/hatchet/sdks/go"
|
|
16365
|
+
)
|
|
16366
|
+
|
|
16367
|
+
const workerName = ${JSON.stringify(workerName)}
|
|
16368
|
+
|
|
16369
|
+
// The health listener, and the one piece of this file that is about Kryd rather than about
|
|
16370
|
+
// workflows.
|
|
16371
|
+
//
|
|
16372
|
+
// A worker holds an outbound gRPC session and listens on no port of its own, but Kryd decides a
|
|
16373
|
+
// deploy is live by asking GET / for a 2xx. Without this the deploy is torn down with
|
|
16374
|
+
// "connection refused (nothing listening yet)".
|
|
16375
|
+
//
|
|
16376
|
+
// \u{1F6A8} It answers 503 until the engine has registered the worker, on purpose. Answering 200 straight
|
|
16377
|
+
// away would mark the deploy live a moment before a failed registration exits the process.
|
|
16378
|
+
var registered atomic.Bool
|
|
16379
|
+
|
|
16380
|
+
func health(w http.ResponseWriter, _ *http.Request) {
|
|
16381
|
+
ready := registered.Load()
|
|
16382
|
+
status := "starting"
|
|
16383
|
+
if ready {
|
|
16384
|
+
status = "registered"
|
|
16385
|
+
}
|
|
16386
|
+
w.Header().Set("content-type", "application/json")
|
|
16387
|
+
if ready {
|
|
16388
|
+
w.WriteHeader(http.StatusOK)
|
|
16389
|
+
} else {
|
|
16390
|
+
w.WriteHeader(http.StatusServiceUnavailable)
|
|
16391
|
+
}
|
|
16392
|
+
_ = json.NewEncoder(w).Encode(map[string]string{"worker": workerName, "status": status})
|
|
16393
|
+
}
|
|
16394
|
+
|
|
16395
|
+
func main() {
|
|
16396
|
+
port := os.Getenv("PORT")
|
|
16397
|
+
if port == "" {
|
|
16398
|
+
port = "8080"
|
|
16399
|
+
}
|
|
16400
|
+
|
|
16401
|
+
mux := http.NewServeMux()
|
|
16402
|
+
mux.HandleFunc("/", health)
|
|
16403
|
+
// Served before the worker connects, so the gate gets a 503 rather than a refused connection
|
|
16404
|
+
// while registration is still in flight.
|
|
16405
|
+
go func() {
|
|
16406
|
+
if err := http.ListenAndServe(":"+port, mux); err != nil {
|
|
16407
|
+
log.Fatalf("health listener stopped: %v", err)
|
|
16408
|
+
}
|
|
16409
|
+
}()
|
|
16410
|
+
|
|
16411
|
+
// Reads HATCHET_CLIENT_TOKEN from the environment and needs nothing else: the token carries the
|
|
16412
|
+
// engine's addresses. Kryd injects it on every deploy once you have run \`kryd workflow add\`.
|
|
16413
|
+
client, err := hatchet.NewClient()
|
|
16414
|
+
if err != nil {
|
|
16415
|
+
log.Fatalf("could not create the Hatchet client: %v", err)
|
|
16416
|
+
}
|
|
16417
|
+
|
|
16418
|
+
worker, err := client.NewWorker(workerName, hatchet.WithWorkflows(Greet(client)))
|
|
16419
|
+
if err != nil {
|
|
16420
|
+
log.Fatalf("could not create the worker: %v", err)
|
|
16421
|
+
}
|
|
16422
|
+
|
|
16423
|
+
// Start returns once the engine has registered the worker, so the flag flips exactly then.
|
|
16424
|
+
cleanup, err := worker.Start()
|
|
16425
|
+
if err != nil {
|
|
16426
|
+
log.Fatalf("could not start the worker: %v", err)
|
|
16427
|
+
}
|
|
16428
|
+
registered.Store(true)
|
|
16429
|
+
|
|
16430
|
+
ctx, cancel := cmdutils.NewInterruptContext()
|
|
16431
|
+
defer cancel()
|
|
16432
|
+
<-ctx.Done()
|
|
16433
|
+
|
|
16434
|
+
if err := cleanup(); err != nil {
|
|
16435
|
+
log.Fatalf("worker shutdown failed: %v", err)
|
|
16436
|
+
}
|
|
16437
|
+
}
|
|
16438
|
+
`
|
|
16439
|
+
},
|
|
16440
|
+
{
|
|
16441
|
+
path: ".gitignore",
|
|
16442
|
+
contents: `/${workerName}
|
|
16443
|
+
*.exe
|
|
16444
|
+
.env
|
|
16445
|
+
.env.local
|
|
16446
|
+
`
|
|
16447
|
+
},
|
|
16448
|
+
{
|
|
16449
|
+
path: "README.md",
|
|
16450
|
+
contents: `# ${workerName}
|
|
16451
|
+
|
|
16452
|
+
A Hatchet workflow worker, ready to deploy on Kryd.
|
|
16453
|
+
|
|
16454
|
+
- Run \`go mod tidy\` once before your first push \u2014 no \`go.sum\` ships with the scaffold.
|
|
16455
|
+
- \`tasks.go\` holds your tasks. \`main.go\` starts the worker and serves the health endpoint Kryd's
|
|
16456
|
+
deploy gate needs \u2014 keep that listener, and keep \`main.go\` at the root, or your deploys will fail.
|
|
16457
|
+
- **\`HATCHET_CLIENT_TOKEN\` only exists inside a deployed container.** Kryd never prints it, so you
|
|
16458
|
+
cannot run this worker against your tenant from your laptop today.
|
|
16459
|
+
- Deploy it: \`kryd workflow add\` once, then \`kryd push\`.
|
|
16460
|
+
|
|
16461
|
+
Full docs: https://docs.kryd.eu/docs/workflows
|
|
16462
|
+
`
|
|
16463
|
+
}
|
|
16464
|
+
]
|
|
16465
|
+
};
|
|
16466
|
+
|
|
16467
|
+
// src/scaffold/index.ts
|
|
16468
|
+
var SCAFFOLD_LANGUAGES = ["typescript", "python", "go"];
|
|
16469
|
+
function isScaffoldLanguage(value) {
|
|
16470
|
+
return SCAFFOLD_LANGUAGES.includes(value);
|
|
16471
|
+
}
|
|
16472
|
+
var TEMPLATES = {
|
|
16473
|
+
typescript: TYPESCRIPT_TEMPLATE,
|
|
16474
|
+
python: PYTHON_TEMPLATE,
|
|
16475
|
+
go: GO_TEMPLATE
|
|
16476
|
+
};
|
|
16477
|
+
function declarationFile() {
|
|
16478
|
+
return {
|
|
16479
|
+
path: DECLARATION_FILE,
|
|
16480
|
+
contents: `${JSON.stringify({ kind: DECLARATION_KIND_WORKFLOW }, null, 2)}
|
|
16481
|
+
`
|
|
16482
|
+
};
|
|
16483
|
+
}
|
|
16484
|
+
function scaffoldFiles(language, workerName) {
|
|
16485
|
+
return [...TEMPLATES[language].files(workerName), declarationFile()];
|
|
16486
|
+
}
|
|
16487
|
+
function firstCommand(language) {
|
|
16488
|
+
return TEMPLATES[language].firstCommand;
|
|
16489
|
+
}
|
|
16490
|
+
function workerNameFrom(directoryName) {
|
|
16491
|
+
const slug = directoryName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
|
|
16492
|
+
return slug || "kryd-worker";
|
|
16493
|
+
}
|
|
16494
|
+
|
|
15944
16495
|
// src/progress.ts
|
|
15945
16496
|
var STEPS = DEPLOY_STATES.filter(
|
|
15946
16497
|
(s) => !TERMINAL_DEPLOY_STATES.includes(s)
|
|
@@ -16271,10 +16822,10 @@ async function promptHidden(question, io) {
|
|
|
16271
16822
|
terminal: true
|
|
16272
16823
|
});
|
|
16273
16824
|
try {
|
|
16274
|
-
return await new Promise((
|
|
16275
|
-
rl.once("SIGINT", () =>
|
|
16276
|
-
rl.once("close", () =>
|
|
16277
|
-
rl.question("", (answer) =>
|
|
16825
|
+
return await new Promise((resolve3) => {
|
|
16826
|
+
rl.once("SIGINT", () => resolve3(null));
|
|
16827
|
+
rl.once("close", () => resolve3(null));
|
|
16828
|
+
rl.question("", (answer) => resolve3(answer));
|
|
16278
16829
|
});
|
|
16279
16830
|
} finally {
|
|
16280
16831
|
rl.close();
|
|
@@ -16286,10 +16837,10 @@ async function promptHidden(question, io) {
|
|
|
16286
16837
|
async function promptLine(question, io) {
|
|
16287
16838
|
const rl = createInterface({ input: io.input, output: io.output });
|
|
16288
16839
|
try {
|
|
16289
|
-
return await new Promise((
|
|
16290
|
-
rl.once("SIGINT", () =>
|
|
16291
|
-
rl.once("close", () =>
|
|
16292
|
-
rl.question(question, (answer) =>
|
|
16840
|
+
return await new Promise((resolve3) => {
|
|
16841
|
+
rl.once("SIGINT", () => resolve3(null));
|
|
16842
|
+
rl.once("close", () => resolve3(null));
|
|
16843
|
+
rl.question(question, (answer) => resolve3(answer));
|
|
16293
16844
|
});
|
|
16294
16845
|
} finally {
|
|
16295
16846
|
rl.close();
|
|
@@ -16298,10 +16849,10 @@ async function promptLine(question, io) {
|
|
|
16298
16849
|
async function promptConfirm(question, io) {
|
|
16299
16850
|
const rl = createInterface({ input: io.input, output: io.output });
|
|
16300
16851
|
try {
|
|
16301
|
-
const answer = await new Promise((
|
|
16302
|
-
rl.once("SIGINT", () =>
|
|
16303
|
-
rl.once("close", () =>
|
|
16304
|
-
rl.question(`${question} [y/N] `, (a) =>
|
|
16852
|
+
const answer = await new Promise((resolve3) => {
|
|
16853
|
+
rl.once("SIGINT", () => resolve3(null));
|
|
16854
|
+
rl.once("close", () => resolve3(null));
|
|
16855
|
+
rl.question(`${question} [y/N] `, (a) => resolve3(a));
|
|
16305
16856
|
});
|
|
16306
16857
|
return answer !== null && /^y(es)?$/i.test(answer.trim());
|
|
16307
16858
|
} finally {
|
|
@@ -16311,6 +16862,7 @@ async function promptConfirm(question, io) {
|
|
|
16311
16862
|
|
|
16312
16863
|
// src/git.ts
|
|
16313
16864
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
16865
|
+
import { resolve } from "node:path";
|
|
16314
16866
|
function authenticatedRemoteUrl(cloneUrl, username, token) {
|
|
16315
16867
|
const prefix = "https://";
|
|
16316
16868
|
if (!cloneUrl.startsWith(prefix)) return cloneUrl;
|
|
@@ -16375,6 +16927,21 @@ function gitAvailable(cwd) {
|
|
|
16375
16927
|
if (res.status !== "ok" || res.value !== "true") return { status: "not-a-repo" };
|
|
16376
16928
|
return { status: "ok" };
|
|
16377
16929
|
}
|
|
16930
|
+
function initRepo(cwd) {
|
|
16931
|
+
const existing = gitAvailable(cwd);
|
|
16932
|
+
if (existing.status === "no-git") return existing;
|
|
16933
|
+
if (existing.status === "ok") {
|
|
16934
|
+
const top = probe(cwd, ["rev-parse", "--show-toplevel"]);
|
|
16935
|
+
if (top.status === "ok" && resolve(top.value) !== resolve(cwd)) {
|
|
16936
|
+
return { status: "nested", root: resolve(top.value) };
|
|
16937
|
+
}
|
|
16938
|
+
return { status: "already" };
|
|
16939
|
+
}
|
|
16940
|
+
const res = probe(cwd, ["init", "--quiet"]);
|
|
16941
|
+
if (res.status === "no-git") return res;
|
|
16942
|
+
if (res.status !== "ok") return { status: "failed" };
|
|
16943
|
+
return { status: "ok" };
|
|
16944
|
+
}
|
|
16378
16945
|
function currentBranch(cwd) {
|
|
16379
16946
|
return probe(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
16380
16947
|
}
|
|
@@ -16463,12 +17030,31 @@ async function runInit(opts) {
|
|
|
16463
17030
|
process.exitCode = 1;
|
|
16464
17031
|
return;
|
|
16465
17032
|
}
|
|
17033
|
+
const existingLink = directoryIsLinked(cwd);
|
|
17034
|
+
if (existingLink) {
|
|
17035
|
+
const remote = hasRemote(cwd, "kryd");
|
|
17036
|
+
const remoteConfigured = remote.status === "ok" && remote.value;
|
|
17037
|
+
if (remoteConfigured) {
|
|
17038
|
+
process.stderr.write(
|
|
17039
|
+
`This folder is already linked to ${existingLink.projectId} (.kryd/project.json).
|
|
17040
|
+
Running \`kryd init\` again would create a SECOND project and leave that one orphaned.
|
|
17041
|
+
To deploy it: \`kryd push\`. To start over: \`kryd project rm\` first, or delete .kryd/project.json to unlink this folder.
|
|
17042
|
+
`
|
|
17043
|
+
);
|
|
17044
|
+
process.exitCode = 1;
|
|
17045
|
+
return;
|
|
17046
|
+
}
|
|
17047
|
+
process.stdout.write(
|
|
17048
|
+
`This folder is linked to ${existingLink.projectId} but has no \`kryd\` remote \u2014 finishing that setup.
|
|
17049
|
+
`
|
|
17050
|
+
);
|
|
17051
|
+
}
|
|
16466
17052
|
const detected = inspectProject(cwd);
|
|
16467
17053
|
let framework;
|
|
16468
17054
|
if (opts.framework) {
|
|
16469
17055
|
if (!isFramework(opts.framework)) {
|
|
16470
17056
|
process.stderr.write(
|
|
16471
|
-
`Unknown framework "${opts.framework}" \u2014 expected react-router, nextjs, vite-spa, or
|
|
17057
|
+
`Unknown framework "${opts.framework}" \u2014 expected react-router, nextjs, vite-spa, node, or workflow.
|
|
16472
17058
|
`
|
|
16473
17059
|
);
|
|
16474
17060
|
process.exitCode = 1;
|
|
@@ -16478,7 +17064,7 @@ async function runInit(opts) {
|
|
|
16478
17064
|
} else {
|
|
16479
17065
|
if (!detected.framework) {
|
|
16480
17066
|
process.stderr.write(
|
|
16481
|
-
"Could not detect a supported framework (React Router, Next.js, a Vite SPA, or a Node service). Pass --framework <react-router|nextjs|vite-spa|node
|
|
17067
|
+
"Could not detect a supported framework (React Router, Next.js, a Vite SPA, or a Node service). Pass --framework <react-router|nextjs|vite-spa|node>, or run `kryd workflow init` here to scaffold a workflow worker.\n"
|
|
16482
17068
|
);
|
|
16483
17069
|
process.exitCode = 1;
|
|
16484
17070
|
return;
|
|
@@ -17554,6 +18140,120 @@ async function runWorkflowStatus(opts) {
|
|
|
17554
18140
|
reportError(err);
|
|
17555
18141
|
}
|
|
17556
18142
|
}
|
|
18143
|
+
async function runWorkflowInit(opts) {
|
|
18144
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
18145
|
+
if (!opts.language) {
|
|
18146
|
+
process.stderr.write(
|
|
18147
|
+
`Pass --language <${SCAFFOLD_LANGUAGES.join("|")}>.
|
|
18148
|
+
`
|
|
18149
|
+
);
|
|
18150
|
+
process.exitCode = 1;
|
|
18151
|
+
return;
|
|
18152
|
+
}
|
|
18153
|
+
if (!isScaffoldLanguage(opts.language)) {
|
|
18154
|
+
process.stderr.write(
|
|
18155
|
+
`Unknown language "${opts.language}" \u2014 expected ${SCAFFOLD_LANGUAGES.join(", ")}.
|
|
18156
|
+
`
|
|
18157
|
+
);
|
|
18158
|
+
process.exitCode = 1;
|
|
18159
|
+
return;
|
|
18160
|
+
}
|
|
18161
|
+
const language = opts.language;
|
|
18162
|
+
const target = opts.dir ? resolve2(cwd, opts.dir) : cwd;
|
|
18163
|
+
const workerName = workerNameFrom(opts.name ?? basename2(target));
|
|
18164
|
+
const files = scaffoldFiles(language, workerName);
|
|
18165
|
+
const clashes = files.map((f) => f.path).filter((p) => existsSync3(join3(target, p)));
|
|
18166
|
+
if (clashes.length > 0) {
|
|
18167
|
+
process.stderr.write(
|
|
18168
|
+
`Refusing to overwrite: ${clashes.join(", ")}.
|
|
18169
|
+
Nothing was written. Run this in an empty directory, or pass --dir <path>.
|
|
18170
|
+
`
|
|
18171
|
+
);
|
|
18172
|
+
process.exitCode = 1;
|
|
18173
|
+
return;
|
|
18174
|
+
}
|
|
18175
|
+
const written = [];
|
|
18176
|
+
try {
|
|
18177
|
+
for (const file2 of files) {
|
|
18178
|
+
const full = join3(target, file2.path);
|
|
18179
|
+
mkdirSync2(dirname2(full), { recursive: true });
|
|
18180
|
+
writeFileSync2(full, file2.contents);
|
|
18181
|
+
written.push(full);
|
|
18182
|
+
}
|
|
18183
|
+
} catch (err) {
|
|
18184
|
+
for (const path of written.reverse()) {
|
|
18185
|
+
try {
|
|
18186
|
+
rmSync2(path);
|
|
18187
|
+
} catch {
|
|
18188
|
+
}
|
|
18189
|
+
}
|
|
18190
|
+
process.stderr.write(
|
|
18191
|
+
`Could not write the scaffold (${err instanceof Error ? err.message : String(err)}).
|
|
18192
|
+
Nothing was left behind.
|
|
18193
|
+
`
|
|
18194
|
+
);
|
|
18195
|
+
process.exitCode = 1;
|
|
18196
|
+
return;
|
|
18197
|
+
}
|
|
18198
|
+
const where = opts.dir ? `${opts.dir}/` : "";
|
|
18199
|
+
process.stdout.write(
|
|
18200
|
+
`Scaffolded a ${language} workflow worker${opts.dir ? ` in ${opts.dir}` : ""}:
|
|
18201
|
+
` + files.map((f) => ` ${where}${f.path}
|
|
18202
|
+
`).join("") + "\n"
|
|
18203
|
+
);
|
|
18204
|
+
const repo = initRepo(target);
|
|
18205
|
+
if (repo.status === "nested") {
|
|
18206
|
+
process.stderr.write(
|
|
18207
|
+
`That directory is inside the git repository at ${repo.root}.
|
|
18208
|
+
A worker has to be its own repository: Kryd builds one app per repository, and git would
|
|
18209
|
+
write the deploy remote into the enclosing repo, so \`kryd push\` would push that tree instead.
|
|
18210
|
+
The files above were written \u2014 move them to a directory of their own, or run this outside that repository.
|
|
18211
|
+
`
|
|
18212
|
+
);
|
|
18213
|
+
process.exitCode = 1;
|
|
18214
|
+
return;
|
|
18215
|
+
}
|
|
18216
|
+
const next = [];
|
|
18217
|
+
if (opts.dir) next.push(`cd ${opts.dir}`);
|
|
18218
|
+
const first = firstCommand(language);
|
|
18219
|
+
if (first) next.push(first);
|
|
18220
|
+
next.push('git add . && git commit -m "scaffold"');
|
|
18221
|
+
if (opts.link === false) {
|
|
18222
|
+
next.push("kryd init", "kryd workflow add", "kryd push");
|
|
18223
|
+
process.stdout.write(
|
|
18224
|
+
`Next: ${next.map((c) => `\`${c}\``).join(", then ")}.
|
|
18225
|
+
HATCHET_CLIENT_TOKEN is injected into your container on deploy; it is never available locally.
|
|
18226
|
+
`
|
|
18227
|
+
);
|
|
18228
|
+
return;
|
|
18229
|
+
}
|
|
18230
|
+
if (repo.status === "failed") {
|
|
18231
|
+
process.stderr.write(
|
|
18232
|
+
"Could not run `git init` here, so the project was not linked \u2014 that path would print your\npush credential into this terminal. The files above are fine: fix git, run `git init`,\nthen `kryd init` in that directory.\n"
|
|
18233
|
+
);
|
|
18234
|
+
process.exitCode = 1;
|
|
18235
|
+
return;
|
|
18236
|
+
}
|
|
18237
|
+
process.exitCode = 0;
|
|
18238
|
+
await runInit({
|
|
18239
|
+
cwd: target,
|
|
18240
|
+
...opts.name ? { name: opts.name } : {},
|
|
18241
|
+
...opts.apiUrl ? { apiUrl: opts.apiUrl } : {}
|
|
18242
|
+
});
|
|
18243
|
+
if (process.exitCode) {
|
|
18244
|
+
process.stderr.write(
|
|
18245
|
+
"\nThe files above were written; only the link failed. Fix the problem above and run `kryd init` here.\n"
|
|
18246
|
+
);
|
|
18247
|
+
return;
|
|
18248
|
+
}
|
|
18249
|
+
next.push("kryd workflow add", "kryd push");
|
|
18250
|
+
process.stdout.write(
|
|
18251
|
+
`
|
|
18252
|
+
Next: ${next.map((c) => `\`${c}\``).join(", then ")}.
|
|
18253
|
+
HATCHET_CLIENT_TOKEN is injected into your container on deploy; it is never available locally.
|
|
18254
|
+
`
|
|
18255
|
+
);
|
|
18256
|
+
}
|
|
17557
18257
|
async function runStorageRemove(opts) {
|
|
17558
18258
|
const apiUrl = resolveApiUrl(opts.apiUrl);
|
|
17559
18259
|
const token = loadConfig().token;
|
|
@@ -17765,7 +18465,7 @@ async function runEnvPull(opts) {
|
|
|
17765
18465
|
const cwd = opts.cwd ?? process.cwd();
|
|
17766
18466
|
const root = findProjectLinkDir(cwd) ?? cwd;
|
|
17767
18467
|
const outRel = opts.out ?? ".env.kryd";
|
|
17768
|
-
const outPath =
|
|
18468
|
+
const outPath = resolve2(root, outRel);
|
|
17769
18469
|
if (opts.out !== void 0 && existsSync3(outPath) && !opts.force) {
|
|
17770
18470
|
process.stderr.write(
|
|
17771
18471
|
`Refusing to overwrite ${outRel} \u2014 pass --force to replace it (or omit --out to write .env.kryd).
|
|
@@ -18040,7 +18740,7 @@ Run \`kryd env list\` to see what is set. (If you meant the runtime variable of
|
|
|
18040
18740
|
reportError(err);
|
|
18041
18741
|
}
|
|
18042
18742
|
}
|
|
18043
|
-
var CLI_VERSION = true ? "0.
|
|
18743
|
+
var CLI_VERSION = true ? "0.8.0" : "0.0.0-dev";
|
|
18044
18744
|
var program = new Command();
|
|
18045
18745
|
program.name("kryd").description("Kryd CLI").version(CLI_VERSION);
|
|
18046
18746
|
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(
|
|
@@ -18051,7 +18751,7 @@ program.command("whoami").description("Show the current account").option("--api-
|
|
|
18051
18751
|
program.command("logout").description("Clear the stored token").action(() => runLogout());
|
|
18052
18752
|
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(
|
|
18053
18753
|
"--framework <framework>",
|
|
18054
|
-
"react-router | nextjs | vite-spa | node (auto-detected if omitted)"
|
|
18754
|
+
"react-router | nextjs | vite-spa | node (auto-detected if omitted; `workflow` comes from a committed kryd.json)"
|
|
18055
18755
|
).option(
|
|
18056
18756
|
"--tenant <slug>",
|
|
18057
18757
|
// KRYD-265 / KRYD-188: this used to say "claimed once on first init", which was false — signup
|
|
@@ -18140,6 +18840,12 @@ var workflow = program.command("workflow").description("Manage the project's dur
|
|
|
18140
18840
|
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
18841
|
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
18842
|
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 }));
|
|
18843
|
+
workflow.command("init").description(
|
|
18844
|
+
"Scaffold a workflow worker here (with the health listener Kryd's deploy gate needs) and link it"
|
|
18845
|
+
).requiredOption(
|
|
18846
|
+
"--language <language>",
|
|
18847
|
+
`worker language: ${SCAFFOLD_LANGUAGES.join(" | ")}`
|
|
18848
|
+
).option("--dir <path>", "write into this directory instead of the current one").option("--name <name>", "project and worker name (defaults to the directory name)").option("--no-link", "only write the files; do not create a Kryd project").option("--api-url <url>", "control-plane API base URL").action((opts) => runWorkflowInit(opts));
|
|
18143
18849
|
function invokedDirectly() {
|
|
18144
18850
|
const entry = process.argv[1];
|
|
18145
18851
|
if (!entry) return false;
|
|
@@ -18186,6 +18892,7 @@ export {
|
|
|
18186
18892
|
runStorageStatus,
|
|
18187
18893
|
runWhoami,
|
|
18188
18894
|
runWorkflowAdd,
|
|
18895
|
+
runWorkflowInit,
|
|
18189
18896
|
runWorkflowRemove,
|
|
18190
18897
|
runWorkflowStatus,
|
|
18191
18898
|
splitKeyValue
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kryd/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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/shared-types": "0.0.0",
|
|
52
53
|
"@kryd/config-eslint": "0.0.0",
|
|
53
|
-
"@kryd/config-ts": "0.0.0"
|
|
54
|
-
"@kryd/shared-types": "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",
|