@kryd/cli 0.7.0 → 0.8.1
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 +1003 -98
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -14855,6 +14855,10 @@ function validateBuildEnvVarValue(value) {
|
|
|
14855
14855
|
var BUILD_ENV_PUBLIC_WARNING = "\u26A0 Build-time values are compiled into your app's public bundle \u2014 anyone who loads your site can read them. Never put a secret here.";
|
|
14856
14856
|
var BUILD_ENV_REBUILD_NOTE = "This takes effect at your next BUILD, not your next deploy \u2014 the value is compiled into the image, so redeploying the existing image keeps the old value. Push a commit (`kryd push`) to rebuild.";
|
|
14857
14857
|
|
|
14858
|
+
// ../../packages/shared-types/dist/workflow-run.js
|
|
14859
|
+
var MAX_WEBHOOK_NAME = 32;
|
|
14860
|
+
var WEBHOOK_NAME_RE = new RegExp(`^[a-z0-9](?:[a-z0-9-]{0,${MAX_WEBHOOK_NAME - 2}}[a-z0-9])?$`);
|
|
14861
|
+
|
|
14858
14862
|
// ../../packages/shared-types/dist/index.js
|
|
14859
14863
|
var DEPLOY_STATES = [
|
|
14860
14864
|
"queued",
|
|
@@ -14895,8 +14899,14 @@ function isTerminalResourceStatus(status) {
|
|
|
14895
14899
|
}
|
|
14896
14900
|
|
|
14897
14901
|
// src/index.ts
|
|
14898
|
-
import {
|
|
14899
|
-
|
|
14902
|
+
import {
|
|
14903
|
+
existsSync as existsSync3,
|
|
14904
|
+
mkdirSync as mkdirSync2,
|
|
14905
|
+
realpathSync,
|
|
14906
|
+
rmSync as rmSync2,
|
|
14907
|
+
writeFileSync as writeFileSync2
|
|
14908
|
+
} from "node:fs";
|
|
14909
|
+
import { basename as basename2, dirname as dirname2, isAbsolute, join as join3, relative, resolve as resolve2 } from "node:path";
|
|
14900
14910
|
import { pathToFileURL } from "node:url";
|
|
14901
14911
|
|
|
14902
14912
|
// src/config.ts
|
|
@@ -14950,8 +14960,13 @@ function clearToken() {
|
|
|
14950
14960
|
delete config2.token;
|
|
14951
14961
|
writeConfig(config2);
|
|
14952
14962
|
}
|
|
14963
|
+
var lastApiUrl = null;
|
|
14953
14964
|
function resolveApiUrl(flag) {
|
|
14954
|
-
|
|
14965
|
+
lastApiUrl = flag ?? process.env.KRYD_API_URL ?? loadConfig().apiUrl ?? DEFAULT_API_URL;
|
|
14966
|
+
return lastApiUrl;
|
|
14967
|
+
}
|
|
14968
|
+
function currentApiUrl() {
|
|
14969
|
+
return lastApiUrl ?? resolveApiUrl();
|
|
14955
14970
|
}
|
|
14956
14971
|
var DEFAULT_DASHBOARD_URL = "https://app.kryd.eu";
|
|
14957
14972
|
function resolveDashboardUrl(flag) {
|
|
@@ -15018,6 +15033,11 @@ function ensureIgnored(cwd, entry, aliases = []) {
|
|
|
15018
15033
|
function ensureKrydIgnored(cwd) {
|
|
15019
15034
|
ensureIgnored(cwd, `${PROJECT_LINK_DIR}/`, [PROJECT_LINK_DIR]);
|
|
15020
15035
|
}
|
|
15036
|
+
function directoryIsLinked(cwd) {
|
|
15037
|
+
const path = join(cwd, PROJECT_LINK_DIR, PROJECT_LINK_FILE);
|
|
15038
|
+
if (!existsSync(path)) return null;
|
|
15039
|
+
return loadProjectLink(cwd);
|
|
15040
|
+
}
|
|
15021
15041
|
function writeSecretFile(path, contents) {
|
|
15022
15042
|
mkdirSync(dirname(path), { recursive: true });
|
|
15023
15043
|
writeFileSync(path, contents, { mode: 384 });
|
|
@@ -15035,9 +15055,33 @@ function clearProjectLinkIfMatches(projectId, cwd = process.cwd()) {
|
|
|
15035
15055
|
return false;
|
|
15036
15056
|
}
|
|
15037
15057
|
}
|
|
15058
|
+
function resolvedLink() {
|
|
15059
|
+
return lastLinkResolution;
|
|
15060
|
+
}
|
|
15061
|
+
var lastLinkResolution = null;
|
|
15038
15062
|
function resolveProjectId(explicit, cwd = process.cwd()) {
|
|
15063
|
+
lastLinkResolution = null;
|
|
15039
15064
|
if (explicit) return explicit;
|
|
15040
|
-
|
|
15065
|
+
const root = findProjectLinkDir(cwd);
|
|
15066
|
+
if (!root) return null;
|
|
15067
|
+
const link = loadProjectLink(cwd);
|
|
15068
|
+
if (!link) return null;
|
|
15069
|
+
lastLinkResolution = { root, link };
|
|
15070
|
+
return link.projectId;
|
|
15071
|
+
}
|
|
15072
|
+
function rememberLinkAccount(root, account) {
|
|
15073
|
+
if (!account.accountId) return false;
|
|
15074
|
+
const link = directoryIsLinked(root);
|
|
15075
|
+
if (!link) return false;
|
|
15076
|
+
const slug = account.accountSlug ?? link.accountSlug;
|
|
15077
|
+
if (link.accountId === account.accountId && link.accountSlug === slug) return false;
|
|
15078
|
+
saveProjectLink(root, {
|
|
15079
|
+
...link,
|
|
15080
|
+
accountId: account.accountId,
|
|
15081
|
+
// `exactOptionalPropertyTypes`: an absent key and an explicit `undefined` are different types.
|
|
15082
|
+
...slug ? { accountSlug: slug } : {}
|
|
15083
|
+
});
|
|
15084
|
+
return true;
|
|
15041
15085
|
}
|
|
15042
15086
|
|
|
15043
15087
|
// src/browser-login.ts
|
|
@@ -15058,7 +15102,7 @@ function browserLogin(dashboardUrl, opts = {}) {
|
|
|
15058
15102
|
const state = randomUUID();
|
|
15059
15103
|
const timeoutMs = opts.timeoutMs ?? 12e4;
|
|
15060
15104
|
const shouldOpen = opts.open ?? true;
|
|
15061
|
-
return new Promise((
|
|
15105
|
+
return new Promise((resolve3, reject) => {
|
|
15062
15106
|
let settled = false;
|
|
15063
15107
|
const settle = (fn) => {
|
|
15064
15108
|
if (settled) return;
|
|
@@ -15090,7 +15134,7 @@ function browserLogin(dashboardUrl, opts = {}) {
|
|
|
15090
15134
|
res.end(SUCCESS_HTML, () => {
|
|
15091
15135
|
settle(() => {
|
|
15092
15136
|
shutdown();
|
|
15093
|
-
|
|
15137
|
+
resolve3({ token, ...apiUrl ? { apiUrl } : {} });
|
|
15094
15138
|
});
|
|
15095
15139
|
});
|
|
15096
15140
|
});
|
|
@@ -15125,7 +15169,15 @@ Waiting for approval\u2026
|
|
|
15125
15169
|
// src/client.ts
|
|
15126
15170
|
var ApiError = class extends Error {
|
|
15127
15171
|
envelope;
|
|
15128
|
-
/**
|
|
15172
|
+
/**
|
|
15173
|
+
* The HTTP status of the failed response, when it came from one (used to detect a 404 = gone).
|
|
15174
|
+
*
|
|
15175
|
+
* ⚠️ Still optional because an `ApiError` is also thrown for a well-formed response the CLI
|
|
15176
|
+
* cannot use ("the API returned an unexpected …") and for a stream's own error frame — neither
|
|
15177
|
+
* has a status. Every throw that DOES hold a `Response` now passes it (KRYD-481): the 404
|
|
15178
|
+
* diagnosis keys on the status, and a third of them used to omit it, which made the same
|
|
15179
|
+
* failure explainable or unexplainable depending on which verb the customer typed.
|
|
15180
|
+
*/
|
|
15129
15181
|
status;
|
|
15130
15182
|
constructor(message, envelope, status) {
|
|
15131
15183
|
super(message);
|
|
@@ -15150,7 +15202,8 @@ async function login(apiUrl, creds) {
|
|
|
15150
15202
|
if (!res.ok) {
|
|
15151
15203
|
throw new ApiError(
|
|
15152
15204
|
`Login failed (${res.status})`,
|
|
15153
|
-
await parseEnvelope(res)
|
|
15205
|
+
await parseEnvelope(res),
|
|
15206
|
+
res.status
|
|
15154
15207
|
);
|
|
15155
15208
|
}
|
|
15156
15209
|
const token = res.headers.get("set-auth-token");
|
|
@@ -15166,7 +15219,8 @@ async function whoami(apiUrl, token) {
|
|
|
15166
15219
|
if (!res.ok) {
|
|
15167
15220
|
throw new ApiError(
|
|
15168
15221
|
`Request failed (${res.status})`,
|
|
15169
|
-
await parseEnvelope(res)
|
|
15222
|
+
await parseEnvelope(res),
|
|
15223
|
+
res.status
|
|
15170
15224
|
);
|
|
15171
15225
|
}
|
|
15172
15226
|
return await res.json();
|
|
@@ -15183,7 +15237,8 @@ async function linkProject(apiUrl, token, input) {
|
|
|
15183
15237
|
if (!res.ok) {
|
|
15184
15238
|
throw new ApiError(
|
|
15185
15239
|
`Repo linking failed (${res.status})`,
|
|
15186
|
-
await parseEnvelope(res)
|
|
15240
|
+
await parseEnvelope(res),
|
|
15241
|
+
res.status
|
|
15187
15242
|
);
|
|
15188
15243
|
}
|
|
15189
15244
|
const parsed = await res.json().catch(() => void 0);
|
|
@@ -15211,7 +15266,8 @@ async function attachDatabase(apiUrl, token, input) {
|
|
|
15211
15266
|
if (!res.ok) {
|
|
15212
15267
|
throw new ApiError(
|
|
15213
15268
|
`Database attach failed (${res.status})`,
|
|
15214
|
-
await parseEnvelope(res)
|
|
15269
|
+
await parseEnvelope(res),
|
|
15270
|
+
res.status
|
|
15215
15271
|
);
|
|
15216
15272
|
}
|
|
15217
15273
|
const parsed = await res.json().catch(() => void 0);
|
|
@@ -15236,7 +15292,8 @@ async function createStorage(apiUrl, token, input) {
|
|
|
15236
15292
|
if (!res.ok) {
|
|
15237
15293
|
throw new ApiError(
|
|
15238
15294
|
`Storage create failed (${res.status})`,
|
|
15239
|
-
await parseEnvelope(res)
|
|
15295
|
+
await parseEnvelope(res),
|
|
15296
|
+
res.status
|
|
15240
15297
|
);
|
|
15241
15298
|
}
|
|
15242
15299
|
const parsed = await res.json().catch(() => void 0);
|
|
@@ -15438,7 +15495,8 @@ async function detachDatabase(apiUrl, token, projectId) {
|
|
|
15438
15495
|
if (!res.ok) {
|
|
15439
15496
|
throw new ApiError(
|
|
15440
15497
|
`Database detach failed (${res.status})`,
|
|
15441
|
-
await parseEnvelope(res)
|
|
15498
|
+
await parseEnvelope(res),
|
|
15499
|
+
res.status
|
|
15442
15500
|
);
|
|
15443
15501
|
}
|
|
15444
15502
|
}
|
|
@@ -15448,7 +15506,7 @@ async function getProject(apiUrl, token, projectId) {
|
|
|
15448
15506
|
});
|
|
15449
15507
|
if (res.status === 404) return null;
|
|
15450
15508
|
if (!res.ok) {
|
|
15451
|
-
throw new ApiError(`Reading the project failed (${res.status})`, await parseEnvelope(res));
|
|
15509
|
+
throw new ApiError(`Reading the project failed (${res.status})`, await parseEnvelope(res), res.status);
|
|
15452
15510
|
}
|
|
15453
15511
|
return await res.json();
|
|
15454
15512
|
}
|
|
@@ -15457,7 +15515,7 @@ async function listProjects(apiUrl, token) {
|
|
|
15457
15515
|
headers: { authorization: `Bearer ${token}` }
|
|
15458
15516
|
});
|
|
15459
15517
|
if (!res.ok) {
|
|
15460
|
-
throw new ApiError(`Listing projects failed (${res.status})`, await parseEnvelope(res));
|
|
15518
|
+
throw new ApiError(`Listing projects failed (${res.status})`, await parseEnvelope(res), res.status);
|
|
15461
15519
|
}
|
|
15462
15520
|
return (await res.json()).items;
|
|
15463
15521
|
}
|
|
@@ -15467,7 +15525,7 @@ async function deleteProject(apiUrl, token, projectId) {
|
|
|
15467
15525
|
headers: { authorization: `Bearer ${token}` }
|
|
15468
15526
|
});
|
|
15469
15527
|
if (!res.ok) {
|
|
15470
|
-
throw new ApiError(`Project delete failed (${res.status})`, await parseEnvelope(res));
|
|
15528
|
+
throw new ApiError(`Project delete failed (${res.status})`, await parseEnvelope(res), res.status);
|
|
15471
15529
|
}
|
|
15472
15530
|
return await res.json();
|
|
15473
15531
|
}
|
|
@@ -15502,7 +15560,8 @@ async function detachStorage(apiUrl, token, projectId) {
|
|
|
15502
15560
|
if (!res.ok) {
|
|
15503
15561
|
throw new ApiError(
|
|
15504
15562
|
`Storage detach failed (${res.status})`,
|
|
15505
|
-
await parseEnvelope(res)
|
|
15563
|
+
await parseEnvelope(res),
|
|
15564
|
+
res.status
|
|
15506
15565
|
);
|
|
15507
15566
|
}
|
|
15508
15567
|
}
|
|
@@ -15668,7 +15727,7 @@ async function triggerDeploy(apiUrl, token, projectId) {
|
|
|
15668
15727
|
body: JSON.stringify({ projectId })
|
|
15669
15728
|
});
|
|
15670
15729
|
if (!res.ok) {
|
|
15671
|
-
throw new ApiError(`Deploy trigger failed (${res.status})`, await parseEnvelope(res));
|
|
15730
|
+
throw new ApiError(`Deploy trigger failed (${res.status})`, await parseEnvelope(res), res.status);
|
|
15672
15731
|
}
|
|
15673
15732
|
return (await res.json()).deploymentId;
|
|
15674
15733
|
}
|
|
@@ -15685,7 +15744,7 @@ async function rollbackDeploy(apiUrl, token, projectId, deploymentId) {
|
|
|
15685
15744
|
})
|
|
15686
15745
|
});
|
|
15687
15746
|
if (!res.ok) {
|
|
15688
|
-
throw new ApiError(`Rollback failed (${res.status})`, await parseEnvelope(res));
|
|
15747
|
+
throw new ApiError(`Rollback failed (${res.status})`, await parseEnvelope(res), res.status);
|
|
15689
15748
|
}
|
|
15690
15749
|
return await res.json();
|
|
15691
15750
|
}
|
|
@@ -15699,7 +15758,7 @@ async function redeployProject(apiUrl, token, projectId) {
|
|
|
15699
15758
|
body: JSON.stringify({ projectId })
|
|
15700
15759
|
});
|
|
15701
15760
|
if (!res.ok) {
|
|
15702
|
-
throw new ApiError(`Redeploy failed (${res.status})`, await parseEnvelope(res));
|
|
15761
|
+
throw new ApiError(`Redeploy failed (${res.status})`, await parseEnvelope(res), res.status);
|
|
15703
15762
|
}
|
|
15704
15763
|
return await res.json();
|
|
15705
15764
|
}
|
|
@@ -15708,7 +15767,7 @@ async function listDeployments(apiUrl, token, projectId) {
|
|
|
15708
15767
|
if (projectId) url2.searchParams.set("projectId", projectId);
|
|
15709
15768
|
const res = await fetch(url2, { headers: { authorization: `Bearer ${token}` } });
|
|
15710
15769
|
if (!res.ok) {
|
|
15711
|
-
throw new ApiError(`Listing deploys failed (${res.status})`, await parseEnvelope(res));
|
|
15770
|
+
throw new ApiError(`Listing deploys failed (${res.status})`, await parseEnvelope(res), res.status);
|
|
15712
15771
|
}
|
|
15713
15772
|
return (await res.json()).items;
|
|
15714
15773
|
}
|
|
@@ -15764,14 +15823,14 @@ async function* readSseFrames(body) {
|
|
|
15764
15823
|
}
|
|
15765
15824
|
function abortableSleep(ms, signal) {
|
|
15766
15825
|
if (signal?.aborted) return Promise.resolve();
|
|
15767
|
-
return new Promise((
|
|
15826
|
+
return new Promise((resolve3) => {
|
|
15768
15827
|
const onAbort = () => {
|
|
15769
15828
|
clearTimeout(timer);
|
|
15770
|
-
|
|
15829
|
+
resolve3();
|
|
15771
15830
|
};
|
|
15772
15831
|
const timer = setTimeout(() => {
|
|
15773
15832
|
signal?.removeEventListener("abort", onAbort);
|
|
15774
|
-
|
|
15833
|
+
resolve3();
|
|
15775
15834
|
}, ms);
|
|
15776
15835
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
15777
15836
|
});
|
|
@@ -15787,7 +15846,7 @@ async function streamDeploy(apiUrl, token, deploymentId, handlers, opts = {}) {
|
|
|
15787
15846
|
headers: { authorization: `Bearer ${token}`, accept: "text/event-stream" }
|
|
15788
15847
|
});
|
|
15789
15848
|
if (!res.ok) {
|
|
15790
|
-
throw new ApiError(`Log stream failed (${res.status})`, await parseEnvelope(res));
|
|
15849
|
+
throw new ApiError(`Log stream failed (${res.status})`, await parseEnvelope(res), res.status);
|
|
15791
15850
|
}
|
|
15792
15851
|
if (!res.body) throw new Error("The log stream returned no body.");
|
|
15793
15852
|
for await (const data of readSseFrames(res.body)) {
|
|
@@ -15831,7 +15890,8 @@ async function streamRuntime(apiUrl, token, id, handlers, opts = {}) {
|
|
|
15831
15890
|
if (!res.ok) {
|
|
15832
15891
|
throw new ApiError(
|
|
15833
15892
|
`Runtime log stream failed (${res.status})`,
|
|
15834
|
-
await parseEnvelope(res)
|
|
15893
|
+
await parseEnvelope(res),
|
|
15894
|
+
res.status
|
|
15835
15895
|
);
|
|
15836
15896
|
}
|
|
15837
15897
|
if (!res.body) throw new Error("The runtime log stream returned no body.");
|
|
@@ -15860,11 +15920,59 @@ async function fetchDeployLog(apiUrl, token, deploymentId, opts = {}) {
|
|
|
15860
15920
|
if (!res.ok) {
|
|
15861
15921
|
throw new ApiError(
|
|
15862
15922
|
`Fetching the stored log failed (${res.status})`,
|
|
15863
|
-
await parseEnvelope(res)
|
|
15923
|
+
await parseEnvelope(res),
|
|
15924
|
+
res.status
|
|
15864
15925
|
);
|
|
15865
15926
|
}
|
|
15866
15927
|
return (await res.json()).log;
|
|
15867
15928
|
}
|
|
15929
|
+
async function fetchPushCredential(apiUrl, token, projectId) {
|
|
15930
|
+
const res = await fetch(
|
|
15931
|
+
`${apiUrl}/repos/${encodeURIComponent(projectId)}/push-credential`,
|
|
15932
|
+
{ headers: { authorization: `Bearer ${token}` } }
|
|
15933
|
+
);
|
|
15934
|
+
if (!res.ok) {
|
|
15935
|
+
throw new ApiError(
|
|
15936
|
+
`Could not fetch the push credential (${res.status})`,
|
|
15937
|
+
await parseEnvelope(res),
|
|
15938
|
+
res.status
|
|
15939
|
+
);
|
|
15940
|
+
}
|
|
15941
|
+
const parsed = await res.json().catch(() => void 0);
|
|
15942
|
+
if (!parsed || typeof parsed.username !== "string" || typeof parsed.password !== "string" || parsed.password.length === 0) {
|
|
15943
|
+
throw new ApiError("The API returned an unexpected push-credential response.");
|
|
15944
|
+
}
|
|
15945
|
+
return { username: parsed.username, password: parsed.password };
|
|
15946
|
+
}
|
|
15947
|
+
|
|
15948
|
+
// src/git-credential.ts
|
|
15949
|
+
async function runGitCredential(operation, opts = {}) {
|
|
15950
|
+
if (operation !== "get") return;
|
|
15951
|
+
const input = parseCredentialInput(opts.stdin ?? "");
|
|
15952
|
+
if (input.protocol && input.protocol !== "https") return;
|
|
15953
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
15954
|
+
const token = loadConfig().token;
|
|
15955
|
+
if (!token) return;
|
|
15956
|
+
const projectId = resolveProjectId(void 0, cwd);
|
|
15957
|
+
if (!projectId) return;
|
|
15958
|
+
try {
|
|
15959
|
+
const cred = await fetchPushCredential(resolveApiUrl(opts.apiUrl), token, projectId);
|
|
15960
|
+
process.stdout.write(`username=${cred.username}
|
|
15961
|
+
password=${cred.password}
|
|
15962
|
+
`);
|
|
15963
|
+
} catch {
|
|
15964
|
+
}
|
|
15965
|
+
}
|
|
15966
|
+
function parseCredentialInput(raw) {
|
|
15967
|
+
const out = {};
|
|
15968
|
+
for (const line of raw.split("\n")) {
|
|
15969
|
+
if (line === "") continue;
|
|
15970
|
+
const eq = line.indexOf("=");
|
|
15971
|
+
if (eq <= 0) continue;
|
|
15972
|
+
out[line.slice(0, eq)] = line.slice(eq + 1);
|
|
15973
|
+
}
|
|
15974
|
+
return out;
|
|
15975
|
+
}
|
|
15868
15976
|
|
|
15869
15977
|
// src/framework.ts
|
|
15870
15978
|
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
@@ -15873,11 +15981,14 @@ var FRAMEWORKS = [
|
|
|
15873
15981
|
"react-router",
|
|
15874
15982
|
"nextjs",
|
|
15875
15983
|
"vite-spa",
|
|
15876
|
-
"node"
|
|
15984
|
+
"node",
|
|
15985
|
+
"workflow"
|
|
15877
15986
|
];
|
|
15878
15987
|
function isFramework(value) {
|
|
15879
15988
|
return FRAMEWORKS.includes(value);
|
|
15880
15989
|
}
|
|
15990
|
+
var DECLARATION_FILE = "kryd.json";
|
|
15991
|
+
var DECLARATION_KIND_WORKFLOW = "workflow";
|
|
15881
15992
|
var VITE_META_FRAMEWORKS = [
|
|
15882
15993
|
"@sveltejs/kit",
|
|
15883
15994
|
"astro",
|
|
@@ -15919,7 +16030,22 @@ function readPackageJson(cwd) {
|
|
|
15919
16030
|
return null;
|
|
15920
16031
|
}
|
|
15921
16032
|
}
|
|
16033
|
+
function declaredKind(cwd) {
|
|
16034
|
+
try {
|
|
16035
|
+
const parsed = JSON.parse(
|
|
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;
|
|
16043
|
+
} catch {
|
|
16044
|
+
return null;
|
|
16045
|
+
}
|
|
16046
|
+
}
|
|
15922
16047
|
function frameworkFrom(pkg, cwd) {
|
|
16048
|
+
if (declaredKind(cwd) === DECLARATION_KIND_WORKFLOW) return "workflow";
|
|
15923
16049
|
const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
|
|
15924
16050
|
const has = (name) => name in deps;
|
|
15925
16051
|
if (has("next")) return "nextjs";
|
|
@@ -15941,6 +16067,528 @@ function inspectProject(cwd) {
|
|
|
15941
16067
|
return { framework: frameworkFrom(pkg, cwd), name: nameFrom(pkg, cwd) };
|
|
15942
16068
|
}
|
|
15943
16069
|
|
|
16070
|
+
// src/scaffold/typescript.ts
|
|
16071
|
+
var TYPESCRIPT_TEMPLATE = {
|
|
16072
|
+
files: (workerName) => [
|
|
16073
|
+
{
|
|
16074
|
+
path: "package.json",
|
|
16075
|
+
contents: `${JSON.stringify(
|
|
16076
|
+
{
|
|
16077
|
+
name: workerName,
|
|
16078
|
+
private: true,
|
|
16079
|
+
version: "0.1.0",
|
|
16080
|
+
scripts: {
|
|
16081
|
+
build: "tsc",
|
|
16082
|
+
start: "node dist/worker.js"
|
|
16083
|
+
},
|
|
16084
|
+
engines: { node: ">=20" },
|
|
16085
|
+
dependencies: {
|
|
16086
|
+
"@hatchet-dev/typescript-sdk": "^1.33.0"
|
|
16087
|
+
},
|
|
16088
|
+
devDependencies: {
|
|
16089
|
+
"@types/node": "^22.10.2",
|
|
16090
|
+
typescript: "^5.7.2"
|
|
16091
|
+
}
|
|
16092
|
+
},
|
|
16093
|
+
null,
|
|
16094
|
+
2
|
|
16095
|
+
)}
|
|
16096
|
+
`
|
|
16097
|
+
},
|
|
16098
|
+
{
|
|
16099
|
+
path: "tsconfig.json",
|
|
16100
|
+
contents: `${JSON.stringify(
|
|
16101
|
+
{
|
|
16102
|
+
compilerOptions: {
|
|
16103
|
+
// CommonJS because the SDK is: it ships no "exports" map and no "type": "module".
|
|
16104
|
+
module: "commonjs",
|
|
16105
|
+
moduleResolution: "node",
|
|
16106
|
+
target: "es2022",
|
|
16107
|
+
lib: ["es2022"],
|
|
16108
|
+
outDir: "dist",
|
|
16109
|
+
rootDir: "src",
|
|
16110
|
+
strict: true,
|
|
16111
|
+
esModuleInterop: true,
|
|
16112
|
+
skipLibCheck: true
|
|
16113
|
+
},
|
|
16114
|
+
include: ["src"]
|
|
16115
|
+
},
|
|
16116
|
+
null,
|
|
16117
|
+
2
|
|
16118
|
+
)}
|
|
16119
|
+
`
|
|
16120
|
+
},
|
|
16121
|
+
{
|
|
16122
|
+
path: "src/hatchet.ts",
|
|
16123
|
+
contents: `import { HatchetClient } from "@hatchet-dev/typescript-sdk/v1";
|
|
16124
|
+
|
|
16125
|
+
/**
|
|
16126
|
+
* Reads HATCHET_CLIENT_TOKEN from the environment and needs nothing else: the token carries the
|
|
16127
|
+
* engine's addresses. Kryd injects it on every deploy once you have run \`kryd workflow add\`.
|
|
16128
|
+
*
|
|
16129
|
+
* It lives in its own file so tasks and the worker can share one client without importing each
|
|
16130
|
+
* other in a circle.
|
|
16131
|
+
*/
|
|
16132
|
+
export const hatchet = HatchetClient.init();
|
|
16133
|
+
`
|
|
16134
|
+
},
|
|
16135
|
+
{
|
|
16136
|
+
path: "src/tasks.ts",
|
|
16137
|
+
contents: `import { hatchet } from "./hatchet";
|
|
16138
|
+
|
|
16139
|
+
/**
|
|
16140
|
+
* One task, to prove the wiring end to end. Add your own beside it and list them in
|
|
16141
|
+
* \`src/worker.ts\` \u2014 everything about how a task runs (retries, timeouts, concurrency, crons,
|
|
16142
|
+
* DAGs) belongs to the engine's SDK, not to Kryd.
|
|
16143
|
+
*/
|
|
16144
|
+
export const greet = hatchet.task({
|
|
16145
|
+
name: "greet",
|
|
16146
|
+
fn: (input: { name?: string }) => ({
|
|
16147
|
+
greeting: \`Hello, \${input.name ?? "world"}\`,
|
|
16148
|
+
}),
|
|
16149
|
+
});
|
|
16150
|
+
`
|
|
16151
|
+
},
|
|
16152
|
+
{
|
|
16153
|
+
path: "src/worker.ts",
|
|
16154
|
+
contents: `import { createServer } from "node:http";
|
|
16155
|
+
|
|
16156
|
+
const WORKER_NAME = ${JSON.stringify(workerName)};
|
|
16157
|
+
|
|
16158
|
+
/*
|
|
16159
|
+
* The health listener, and the one piece of this file that is about Kryd rather than about workflows.
|
|
16160
|
+
*
|
|
16161
|
+
* A worker holds an outbound gRPC session and listens on no port of its own, but Kryd decides a
|
|
16162
|
+
* deploy is live by asking GET / for a 2xx. Without this the deploy is torn down with
|
|
16163
|
+
* "connection refused (nothing listening yet)".
|
|
16164
|
+
*
|
|
16165
|
+
* \u{1F6A8} It answers 503 until the engine has registered the worker, on purpose. Answering 200 straight
|
|
16166
|
+
* away would mark the deploy live a moment before a failed registration crashes the process \u2014 which
|
|
16167
|
+
* is exactly what happened while this was being measured.
|
|
16168
|
+
*/
|
|
16169
|
+
let registered = false;
|
|
16170
|
+
|
|
16171
|
+
createServer((_req, res) => {
|
|
16172
|
+
res.writeHead(registered ? 200 : 503, { "content-type": "application/json" });
|
|
16173
|
+
res.end(
|
|
16174
|
+
JSON.stringify({
|
|
16175
|
+
worker: WORKER_NAME,
|
|
16176
|
+
status: registered ? "registered" : "starting",
|
|
16177
|
+
}),
|
|
16178
|
+
);
|
|
16179
|
+
}).listen(Number(process.env.PORT ?? 8080), "0.0.0.0");
|
|
16180
|
+
|
|
16181
|
+
async function main(): Promise<void> {
|
|
16182
|
+
// Imported here rather than at the top of the file, and that is deliberate: creating the client
|
|
16183
|
+
// reads and decodes HATCHET_CLIENT_TOKEN, and a token it cannot parse makes it throw during
|
|
16184
|
+
// module evaluation \u2014 before the listener above ever binds. The deploy would then fail with
|
|
16185
|
+
// "connection refused (nothing listening yet)", which is the one message this whole scaffold
|
|
16186
|
+
// exists to stop you seeing. Binding first means a bad token shows up as a 503 plus a readable
|
|
16187
|
+
// error in your deploy log.
|
|
16188
|
+
const { hatchet } = await import("./hatchet");
|
|
16189
|
+
const { greet } = await import("./tasks");
|
|
16190
|
+
|
|
16191
|
+
const worker = await hatchet.worker(WORKER_NAME, { workflows: [greet] });
|
|
16192
|
+
|
|
16193
|
+
// start() resolves when the worker stops, so it is not awaited here; waitUntilReady() flips the
|
|
16194
|
+
// health flag once the engine has the worker. A failed registration rejects \`running\` and the
|
|
16195
|
+
// process exits non-zero, which fails the deploy loudly instead of going live and crash-looping.
|
|
16196
|
+
const running = worker.start();
|
|
16197
|
+
void worker.waitUntilReady().then(
|
|
16198
|
+
() => {
|
|
16199
|
+
registered = true;
|
|
16200
|
+
},
|
|
16201
|
+
() => {
|
|
16202
|
+
// Stays 503. The deploy gate reports it; there is nothing useful to do here.
|
|
16203
|
+
},
|
|
16204
|
+
);
|
|
16205
|
+
await running;
|
|
16206
|
+
}
|
|
16207
|
+
|
|
16208
|
+
main().catch((err: unknown) => {
|
|
16209
|
+
console.error("[worker] stopped:", err);
|
|
16210
|
+
process.exit(1);
|
|
16211
|
+
});
|
|
16212
|
+
`
|
|
16213
|
+
},
|
|
16214
|
+
{
|
|
16215
|
+
path: ".gitignore",
|
|
16216
|
+
contents: `node_modules/
|
|
16217
|
+
dist/
|
|
16218
|
+
.env
|
|
16219
|
+
.env.local
|
|
16220
|
+
`
|
|
16221
|
+
},
|
|
16222
|
+
{
|
|
16223
|
+
path: "README.md",
|
|
16224
|
+
contents: `# ${workerName}
|
|
16225
|
+
|
|
16226
|
+
A Hatchet workflow worker, ready to deploy on Kryd.
|
|
16227
|
+
|
|
16228
|
+
- \`src/tasks.ts\` holds your tasks. \`src/worker.ts\` starts the worker and serves the health
|
|
16229
|
+
endpoint Kryd's deploy gate needs \u2014 keep that listener, or your deploys will be torn down.
|
|
16230
|
+
- **\`HATCHET_CLIENT_TOKEN\` only exists inside a deployed container.** Kryd never prints it, so you
|
|
16231
|
+
cannot run this worker against your tenant from your laptop today.
|
|
16232
|
+
- Deploy it: \`kryd workflow add\` once, then \`kryd push\`.
|
|
16233
|
+
|
|
16234
|
+
Full docs: https://docs.kryd.eu/docs/workflows
|
|
16235
|
+
`
|
|
16236
|
+
}
|
|
16237
|
+
]
|
|
16238
|
+
};
|
|
16239
|
+
|
|
16240
|
+
// src/scaffold/python.ts
|
|
16241
|
+
var PYTHON_TEMPLATE = {
|
|
16242
|
+
files: (workerName) => [
|
|
16243
|
+
{
|
|
16244
|
+
path: "requirements.txt",
|
|
16245
|
+
contents: `hatchet-sdk>=1.40.1
|
|
16246
|
+
`
|
|
16247
|
+
},
|
|
16248
|
+
{
|
|
16249
|
+
path: "tasks.py",
|
|
16250
|
+
contents: `from hatchet_client import hatchet
|
|
16251
|
+
from hatchet_sdk import Context, EmptyModel
|
|
16252
|
+
|
|
16253
|
+
|
|
16254
|
+
# One task, to prove the wiring end to end. Add your own beside it and list them in main.py \u2014
|
|
16255
|
+
# everything about how a task runs (retries, timeouts, concurrency, crons, DAGs) belongs to the
|
|
16256
|
+
# engine's SDK, not to Kryd.
|
|
16257
|
+
@hatchet.task(name="greet")
|
|
16258
|
+
def greet(input: EmptyModel, ctx: Context) -> dict[str, str]:
|
|
16259
|
+
return {"greeting": "Hello, world"}
|
|
16260
|
+
`
|
|
16261
|
+
},
|
|
16262
|
+
{
|
|
16263
|
+
path: "hatchet_client.py",
|
|
16264
|
+
contents: `from hatchet_sdk import Hatchet
|
|
16265
|
+
|
|
16266
|
+
# Reads HATCHET_CLIENT_TOKEN from the environment and needs nothing else: the token carries the
|
|
16267
|
+
# engine's addresses. Kryd injects it on every deploy once you have run \`kryd workflow add\`.
|
|
16268
|
+
#
|
|
16269
|
+
# It lives in its own module so tasks and the worker can share one client without importing each
|
|
16270
|
+
# other in a circle.
|
|
16271
|
+
hatchet = Hatchet()
|
|
16272
|
+
`
|
|
16273
|
+
},
|
|
16274
|
+
{
|
|
16275
|
+
path: "main.py",
|
|
16276
|
+
contents: `"""Worker entry point.
|
|
16277
|
+
|
|
16278
|
+
\u{1F6A8} This file must stay at the project root and keep this name: Kryd's builder derives the start
|
|
16279
|
+
command by finding main.py here. Move it into a package and the deploy has no start command at all.
|
|
16280
|
+
"""
|
|
16281
|
+
|
|
16282
|
+
import json
|
|
16283
|
+
import os
|
|
16284
|
+
import threading
|
|
16285
|
+
import time
|
|
16286
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
16287
|
+
|
|
16288
|
+
WORKER_NAME = ${JSON.stringify(workerName)}
|
|
16289
|
+
|
|
16290
|
+
# The health listener, and the one piece of this file that is about Kryd rather than about workflows.
|
|
16291
|
+
#
|
|
16292
|
+
# A worker holds an outbound gRPC session and listens on no port of its own, but Kryd decides a
|
|
16293
|
+
# deploy is live by asking GET / for a 2xx. Without this the deploy is torn down with
|
|
16294
|
+
# "connection refused (nothing listening yet)".
|
|
16295
|
+
#
|
|
16296
|
+
# \u{1F6A8} It answers 503 until the engine has registered the worker, on purpose. Answering 200 straight
|
|
16297
|
+
# away would mark the deploy live a moment before a failed registration crashes the process.
|
|
16298
|
+
_registered = threading.Event()
|
|
16299
|
+
|
|
16300
|
+
|
|
16301
|
+
class _Health(BaseHTTPRequestHandler):
|
|
16302
|
+
def do_GET(self) -> None: # noqa: N802 - BaseHTTPRequestHandler's spelling
|
|
16303
|
+
ready = _registered.is_set()
|
|
16304
|
+
body = json.dumps(
|
|
16305
|
+
{"worker": WORKER_NAME, "status": "registered" if ready else "starting"}
|
|
16306
|
+
).encode()
|
|
16307
|
+
self.send_response(200 if ready else 503)
|
|
16308
|
+
self.send_header("content-type", "application/json")
|
|
16309
|
+
self.send_header("content-length", str(len(body)))
|
|
16310
|
+
self.end_headers()
|
|
16311
|
+
self.wfile.write(body)
|
|
16312
|
+
|
|
16313
|
+
def log_message(self, *args: object) -> None:
|
|
16314
|
+
# The default handler writes every request to stderr, which would drown the worker's own
|
|
16315
|
+
# logs: the deploy gate polls this endpoint every two seconds.
|
|
16316
|
+
pass
|
|
16317
|
+
|
|
16318
|
+
|
|
16319
|
+
def _serve_health() -> None:
|
|
16320
|
+
port = int(os.environ.get("PORT", "8080"))
|
|
16321
|
+
HTTPServer(("0.0.0.0", port), _Health).serve_forever()
|
|
16322
|
+
|
|
16323
|
+
|
|
16324
|
+
def _watch_registration(worker: object) -> None:
|
|
16325
|
+
# worker.start() blocks, so readiness is observed from a thread. The status enum is compared by
|
|
16326
|
+
# name rather than imported: it lives at a private-ish path in the SDK and this keeps the
|
|
16327
|
+
# scaffold working across SDK reshuffles.
|
|
16328
|
+
#
|
|
16329
|
+
# \u{1F6A8} If the SDK ever renames or drops .status, this loop would spin forever, / would answer 503
|
|
16330
|
+
# forever, and the deploy would be failed by the health gate after three minutes with nothing in
|
|
16331
|
+
# the log to explain it. Say so once instead: a deploy that fails is fine, a deploy that fails
|
|
16332
|
+
# silently is not.
|
|
16333
|
+
if not hasattr(worker, "status"):
|
|
16334
|
+
print(
|
|
16335
|
+
"[worker] this SDK build has no 'status' attribute, so readiness cannot be observed; "
|
|
16336
|
+
"the health endpoint will stay 503 and Kryd will fail the deploy.",
|
|
16337
|
+
flush=True,
|
|
16338
|
+
)
|
|
16339
|
+
return
|
|
16340
|
+
|
|
16341
|
+
while not _registered.is_set():
|
|
16342
|
+
if getattr(getattr(worker, "status", None), "name", "") == "HEALTHY":
|
|
16343
|
+
_registered.set()
|
|
16344
|
+
return
|
|
16345
|
+
time.sleep(0.2)
|
|
16346
|
+
|
|
16347
|
+
|
|
16348
|
+
def main() -> None:
|
|
16349
|
+
threading.Thread(target=_serve_health, daemon=True).start()
|
|
16350
|
+
|
|
16351
|
+
# Imported here rather than at the top of the file, and that is deliberate: creating the client
|
|
16352
|
+
# reads and decodes HATCHET_CLIENT_TOKEN, and a token it cannot parse raises during import \u2014
|
|
16353
|
+
# before the listener above ever binds. The deploy would then fail with "connection refused
|
|
16354
|
+
# (nothing listening yet)", which is the one message this whole scaffold exists to stop you
|
|
16355
|
+
# seeing. Binding first means a bad token shows up as a 503 plus a readable error in your log.
|
|
16356
|
+
from hatchet_client import hatchet
|
|
16357
|
+
from tasks import greet
|
|
16358
|
+
|
|
16359
|
+
worker = hatchet.worker(WORKER_NAME, workflows=[greet])
|
|
16360
|
+
threading.Thread(target=_watch_registration, args=(worker,), daemon=True).start()
|
|
16361
|
+
worker.start()
|
|
16362
|
+
|
|
16363
|
+
|
|
16364
|
+
if __name__ == "__main__":
|
|
16365
|
+
main()
|
|
16366
|
+
`
|
|
16367
|
+
},
|
|
16368
|
+
{
|
|
16369
|
+
path: ".gitignore",
|
|
16370
|
+
contents: `__pycache__/
|
|
16371
|
+
*.py[cod]
|
|
16372
|
+
.venv/
|
|
16373
|
+
venv/
|
|
16374
|
+
.env
|
|
16375
|
+
.env.local
|
|
16376
|
+
`
|
|
16377
|
+
},
|
|
16378
|
+
{
|
|
16379
|
+
path: "README.md",
|
|
16380
|
+
contents: `# ${workerName}
|
|
16381
|
+
|
|
16382
|
+
A Hatchet workflow worker, ready to deploy on Kryd.
|
|
16383
|
+
|
|
16384
|
+
- \`tasks.py\` holds your tasks. \`main.py\` starts the worker and serves the health endpoint Kryd's
|
|
16385
|
+
deploy gate needs \u2014 keep that listener, and keep \`main.py\` at the root under that name, or your
|
|
16386
|
+
deploys will fail.
|
|
16387
|
+
- **\`HATCHET_CLIENT_TOKEN\` only exists inside a deployed container.** Kryd never prints it, so you
|
|
16388
|
+
cannot run this worker against your tenant from your laptop today.
|
|
16389
|
+
- Deploy it: \`kryd workflow add\` once, then \`kryd push\`.
|
|
16390
|
+
|
|
16391
|
+
Full docs: https://docs.kryd.eu/docs/workflows
|
|
16392
|
+
`
|
|
16393
|
+
}
|
|
16394
|
+
]
|
|
16395
|
+
};
|
|
16396
|
+
|
|
16397
|
+
// src/scaffold/go.ts
|
|
16398
|
+
var GO_TEMPLATE = {
|
|
16399
|
+
firstCommand: "go mod tidy",
|
|
16400
|
+
files: (workerName) => [
|
|
16401
|
+
{
|
|
16402
|
+
path: "go.mod",
|
|
16403
|
+
contents: `module kryd.local/${workerName}
|
|
16404
|
+
|
|
16405
|
+
go 1.26
|
|
16406
|
+
|
|
16407
|
+
toolchain go1.26.0
|
|
16408
|
+
|
|
16409
|
+
require github.com/hatchet-dev/hatchet v0.106.10
|
|
16410
|
+
`
|
|
16411
|
+
},
|
|
16412
|
+
{
|
|
16413
|
+
path: "tasks.go",
|
|
16414
|
+
contents: `package main
|
|
16415
|
+
|
|
16416
|
+
import (
|
|
16417
|
+
hatchet "github.com/hatchet-dev/hatchet/sdks/go"
|
|
16418
|
+
)
|
|
16419
|
+
|
|
16420
|
+
// GreetInput is what a run is triggered with.
|
|
16421
|
+
type GreetInput struct {
|
|
16422
|
+
Name string \`json:"name"\`
|
|
16423
|
+
}
|
|
16424
|
+
|
|
16425
|
+
// GreetOutput is what the task returns.
|
|
16426
|
+
type GreetOutput struct {
|
|
16427
|
+
Greeting string \`json:"greeting"\`
|
|
16428
|
+
}
|
|
16429
|
+
|
|
16430
|
+
// Greet is one task, to prove the wiring end to end. Add your own beside it and register them in
|
|
16431
|
+
// main.go \u2014 everything about how a task runs (retries, timeouts, concurrency, crons, DAGs) belongs
|
|
16432
|
+
// to the engine's SDK, not to Kryd.
|
|
16433
|
+
func Greet(c *hatchet.Client) *hatchet.StandaloneTask {
|
|
16434
|
+
return c.NewStandaloneTask("greet", func(ctx hatchet.Context, input GreetInput) (GreetOutput, error) {
|
|
16435
|
+
name := input.Name
|
|
16436
|
+
if name == "" {
|
|
16437
|
+
name = "world"
|
|
16438
|
+
}
|
|
16439
|
+
return GreetOutput{Greeting: "Hello, " + name}, nil
|
|
16440
|
+
})
|
|
16441
|
+
}
|
|
16442
|
+
`
|
|
16443
|
+
},
|
|
16444
|
+
{
|
|
16445
|
+
path: "main.go",
|
|
16446
|
+
contents: `// Worker entry point.
|
|
16447
|
+
//
|
|
16448
|
+
// \u{1F6A8} This file must stay at the project root: Kryd's builder compiles the root package when one
|
|
16449
|
+
// exists, and otherwise picks the first cmd/* directory alphabetically \u2014 which is how a trigger
|
|
16450
|
+
// script ends up deployed instead of the worker.
|
|
16451
|
+
package main
|
|
16452
|
+
|
|
16453
|
+
import (
|
|
16454
|
+
"encoding/json"
|
|
16455
|
+
"log"
|
|
16456
|
+
"net/http"
|
|
16457
|
+
"os"
|
|
16458
|
+
"sync/atomic"
|
|
16459
|
+
|
|
16460
|
+
"github.com/hatchet-dev/hatchet/pkg/cmdutils"
|
|
16461
|
+
hatchet "github.com/hatchet-dev/hatchet/sdks/go"
|
|
16462
|
+
)
|
|
16463
|
+
|
|
16464
|
+
const workerName = ${JSON.stringify(workerName)}
|
|
16465
|
+
|
|
16466
|
+
// The health listener, and the one piece of this file that is about Kryd rather than about
|
|
16467
|
+
// workflows.
|
|
16468
|
+
//
|
|
16469
|
+
// A worker holds an outbound gRPC session and listens on no port of its own, but Kryd decides a
|
|
16470
|
+
// deploy is live by asking GET / for a 2xx. Without this the deploy is torn down with
|
|
16471
|
+
// "connection refused (nothing listening yet)".
|
|
16472
|
+
//
|
|
16473
|
+
// \u{1F6A8} It answers 503 until the engine has registered the worker, on purpose. Answering 200 straight
|
|
16474
|
+
// away would mark the deploy live a moment before a failed registration exits the process.
|
|
16475
|
+
var registered atomic.Bool
|
|
16476
|
+
|
|
16477
|
+
func health(w http.ResponseWriter, _ *http.Request) {
|
|
16478
|
+
ready := registered.Load()
|
|
16479
|
+
status := "starting"
|
|
16480
|
+
if ready {
|
|
16481
|
+
status = "registered"
|
|
16482
|
+
}
|
|
16483
|
+
w.Header().Set("content-type", "application/json")
|
|
16484
|
+
if ready {
|
|
16485
|
+
w.WriteHeader(http.StatusOK)
|
|
16486
|
+
} else {
|
|
16487
|
+
w.WriteHeader(http.StatusServiceUnavailable)
|
|
16488
|
+
}
|
|
16489
|
+
_ = json.NewEncoder(w).Encode(map[string]string{"worker": workerName, "status": status})
|
|
16490
|
+
}
|
|
16491
|
+
|
|
16492
|
+
func main() {
|
|
16493
|
+
port := os.Getenv("PORT")
|
|
16494
|
+
if port == "" {
|
|
16495
|
+
port = "8080"
|
|
16496
|
+
}
|
|
16497
|
+
|
|
16498
|
+
mux := http.NewServeMux()
|
|
16499
|
+
mux.HandleFunc("/", health)
|
|
16500
|
+
// Served before the worker connects, so the gate gets a 503 rather than a refused connection
|
|
16501
|
+
// while registration is still in flight.
|
|
16502
|
+
go func() {
|
|
16503
|
+
if err := http.ListenAndServe(":"+port, mux); err != nil {
|
|
16504
|
+
log.Fatalf("health listener stopped: %v", err)
|
|
16505
|
+
}
|
|
16506
|
+
}()
|
|
16507
|
+
|
|
16508
|
+
// Reads HATCHET_CLIENT_TOKEN from the environment and needs nothing else: the token carries the
|
|
16509
|
+
// engine's addresses. Kryd injects it on every deploy once you have run \`kryd workflow add\`.
|
|
16510
|
+
client, err := hatchet.NewClient()
|
|
16511
|
+
if err != nil {
|
|
16512
|
+
log.Fatalf("could not create the Hatchet client: %v", err)
|
|
16513
|
+
}
|
|
16514
|
+
|
|
16515
|
+
worker, err := client.NewWorker(workerName, hatchet.WithWorkflows(Greet(client)))
|
|
16516
|
+
if err != nil {
|
|
16517
|
+
log.Fatalf("could not create the worker: %v", err)
|
|
16518
|
+
}
|
|
16519
|
+
|
|
16520
|
+
// Start returns once the engine has registered the worker, so the flag flips exactly then.
|
|
16521
|
+
cleanup, err := worker.Start()
|
|
16522
|
+
if err != nil {
|
|
16523
|
+
log.Fatalf("could not start the worker: %v", err)
|
|
16524
|
+
}
|
|
16525
|
+
registered.Store(true)
|
|
16526
|
+
|
|
16527
|
+
ctx, cancel := cmdutils.NewInterruptContext()
|
|
16528
|
+
defer cancel()
|
|
16529
|
+
<-ctx.Done()
|
|
16530
|
+
|
|
16531
|
+
if err := cleanup(); err != nil {
|
|
16532
|
+
log.Fatalf("worker shutdown failed: %v", err)
|
|
16533
|
+
}
|
|
16534
|
+
}
|
|
16535
|
+
`
|
|
16536
|
+
},
|
|
16537
|
+
{
|
|
16538
|
+
path: ".gitignore",
|
|
16539
|
+
contents: `/${workerName}
|
|
16540
|
+
*.exe
|
|
16541
|
+
.env
|
|
16542
|
+
.env.local
|
|
16543
|
+
`
|
|
16544
|
+
},
|
|
16545
|
+
{
|
|
16546
|
+
path: "README.md",
|
|
16547
|
+
contents: `# ${workerName}
|
|
16548
|
+
|
|
16549
|
+
A Hatchet workflow worker, ready to deploy on Kryd.
|
|
16550
|
+
|
|
16551
|
+
- Run \`go mod tidy\` once before your first push \u2014 no \`go.sum\` ships with the scaffold.
|
|
16552
|
+
- \`tasks.go\` holds your tasks. \`main.go\` starts the worker and serves the health endpoint Kryd's
|
|
16553
|
+
deploy gate needs \u2014 keep that listener, and keep \`main.go\` at the root, or your deploys will fail.
|
|
16554
|
+
- **\`HATCHET_CLIENT_TOKEN\` only exists inside a deployed container.** Kryd never prints it, so you
|
|
16555
|
+
cannot run this worker against your tenant from your laptop today.
|
|
16556
|
+
- Deploy it: \`kryd workflow add\` once, then \`kryd push\`.
|
|
16557
|
+
|
|
16558
|
+
Full docs: https://docs.kryd.eu/docs/workflows
|
|
16559
|
+
`
|
|
16560
|
+
}
|
|
16561
|
+
]
|
|
16562
|
+
};
|
|
16563
|
+
|
|
16564
|
+
// src/scaffold/index.ts
|
|
16565
|
+
var SCAFFOLD_LANGUAGES = ["typescript", "python", "go"];
|
|
16566
|
+
function isScaffoldLanguage(value) {
|
|
16567
|
+
return SCAFFOLD_LANGUAGES.includes(value);
|
|
16568
|
+
}
|
|
16569
|
+
var TEMPLATES = {
|
|
16570
|
+
typescript: TYPESCRIPT_TEMPLATE,
|
|
16571
|
+
python: PYTHON_TEMPLATE,
|
|
16572
|
+
go: GO_TEMPLATE
|
|
16573
|
+
};
|
|
16574
|
+
function declarationFile() {
|
|
16575
|
+
return {
|
|
16576
|
+
path: DECLARATION_FILE,
|
|
16577
|
+
contents: `${JSON.stringify({ kind: DECLARATION_KIND_WORKFLOW }, null, 2)}
|
|
16578
|
+
`
|
|
16579
|
+
};
|
|
16580
|
+
}
|
|
16581
|
+
function scaffoldFiles(language, workerName) {
|
|
16582
|
+
return [...TEMPLATES[language].files(workerName), declarationFile()];
|
|
16583
|
+
}
|
|
16584
|
+
function firstCommand(language) {
|
|
16585
|
+
return TEMPLATES[language].firstCommand;
|
|
16586
|
+
}
|
|
16587
|
+
function workerNameFrom(directoryName) {
|
|
16588
|
+
const slug = directoryName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
|
|
16589
|
+
return slug || "kryd-worker";
|
|
16590
|
+
}
|
|
16591
|
+
|
|
15944
16592
|
// src/progress.ts
|
|
15945
16593
|
var STEPS = DEPLOY_STATES.filter(
|
|
15946
16594
|
(s) => !TERMINAL_DEPLOY_STATES.includes(s)
|
|
@@ -16271,10 +16919,10 @@ async function promptHidden(question, io) {
|
|
|
16271
16919
|
terminal: true
|
|
16272
16920
|
});
|
|
16273
16921
|
try {
|
|
16274
|
-
return await new Promise((
|
|
16275
|
-
rl.once("SIGINT", () =>
|
|
16276
|
-
rl.once("close", () =>
|
|
16277
|
-
rl.question("", (answer) =>
|
|
16922
|
+
return await new Promise((resolve3) => {
|
|
16923
|
+
rl.once("SIGINT", () => resolve3(null));
|
|
16924
|
+
rl.once("close", () => resolve3(null));
|
|
16925
|
+
rl.question("", (answer) => resolve3(answer));
|
|
16278
16926
|
});
|
|
16279
16927
|
} finally {
|
|
16280
16928
|
rl.close();
|
|
@@ -16286,10 +16934,10 @@ async function promptHidden(question, io) {
|
|
|
16286
16934
|
async function promptLine(question, io) {
|
|
16287
16935
|
const rl = createInterface({ input: io.input, output: io.output });
|
|
16288
16936
|
try {
|
|
16289
|
-
return await new Promise((
|
|
16290
|
-
rl.once("SIGINT", () =>
|
|
16291
|
-
rl.once("close", () =>
|
|
16292
|
-
rl.question(question, (answer) =>
|
|
16937
|
+
return await new Promise((resolve3) => {
|
|
16938
|
+
rl.once("SIGINT", () => resolve3(null));
|
|
16939
|
+
rl.once("close", () => resolve3(null));
|
|
16940
|
+
rl.question(question, (answer) => resolve3(answer));
|
|
16293
16941
|
});
|
|
16294
16942
|
} finally {
|
|
16295
16943
|
rl.close();
|
|
@@ -16298,10 +16946,10 @@ async function promptLine(question, io) {
|
|
|
16298
16946
|
async function promptConfirm(question, io) {
|
|
16299
16947
|
const rl = createInterface({ input: io.input, output: io.output });
|
|
16300
16948
|
try {
|
|
16301
|
-
const answer = await new Promise((
|
|
16302
|
-
rl.once("SIGINT", () =>
|
|
16303
|
-
rl.once("close", () =>
|
|
16304
|
-
rl.question(`${question} [y/N] `, (a) =>
|
|
16949
|
+
const answer = await new Promise((resolve3) => {
|
|
16950
|
+
rl.once("SIGINT", () => resolve3(null));
|
|
16951
|
+
rl.once("close", () => resolve3(null));
|
|
16952
|
+
rl.question(`${question} [y/N] `, (a) => resolve3(a));
|
|
16305
16953
|
});
|
|
16306
16954
|
return answer !== null && /^y(es)?$/i.test(answer.trim());
|
|
16307
16955
|
} finally {
|
|
@@ -16311,12 +16959,7 @@ async function promptConfirm(question, io) {
|
|
|
16311
16959
|
|
|
16312
16960
|
// src/git.ts
|
|
16313
16961
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
16314
|
-
|
|
16315
|
-
const prefix = "https://";
|
|
16316
|
-
if (!cloneUrl.startsWith(prefix)) return cloneUrl;
|
|
16317
|
-
const creds = `${encodeURIComponent(username)}:${encodeURIComponent(token)}@`;
|
|
16318
|
-
return prefix + creds + cloneUrl.slice(prefix.length);
|
|
16319
|
-
}
|
|
16962
|
+
import { resolve } from "node:path";
|
|
16320
16963
|
function configureGitRemote(cwd, remote, url2) {
|
|
16321
16964
|
const run = (args) => execFileSync("git", args, { cwd, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
|
|
16322
16965
|
try {
|
|
@@ -16375,6 +17018,21 @@ function gitAvailable(cwd) {
|
|
|
16375
17018
|
if (res.status !== "ok" || res.value !== "true") return { status: "not-a-repo" };
|
|
16376
17019
|
return { status: "ok" };
|
|
16377
17020
|
}
|
|
17021
|
+
function initRepo(cwd) {
|
|
17022
|
+
const existing = gitAvailable(cwd);
|
|
17023
|
+
if (existing.status === "no-git") return existing;
|
|
17024
|
+
if (existing.status === "ok") {
|
|
17025
|
+
const top = probe(cwd, ["rev-parse", "--show-toplevel"]);
|
|
17026
|
+
if (top.status === "ok" && resolve(top.value) !== resolve(cwd)) {
|
|
17027
|
+
return { status: "nested", root: resolve(top.value) };
|
|
17028
|
+
}
|
|
17029
|
+
return { status: "already" };
|
|
17030
|
+
}
|
|
17031
|
+
const res = probe(cwd, ["init", "--quiet"]);
|
|
17032
|
+
if (res.status === "no-git") return res;
|
|
17033
|
+
if (res.status !== "ok") return { status: "failed" };
|
|
17034
|
+
return { status: "ok" };
|
|
17035
|
+
}
|
|
16378
17036
|
function currentBranch(cwd) {
|
|
16379
17037
|
return probe(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
16380
17038
|
}
|
|
@@ -16394,9 +17052,32 @@ function pushBranch(cwd, remote, branch, extraArgs) {
|
|
|
16394
17052
|
if (res.status === 0) return { status: "ok" };
|
|
16395
17053
|
return { status: "failed", code: res.status ?? 1 };
|
|
16396
17054
|
}
|
|
17055
|
+
function configureCredentialHelper(cwd, cloneUrl) {
|
|
17056
|
+
const origin = originOf(cloneUrl);
|
|
17057
|
+
if (!origin) return "unavailable";
|
|
17058
|
+
const run = (args) => {
|
|
17059
|
+
try {
|
|
17060
|
+
execFileSync("git", args, { cwd, stdio: ["ignore", "ignore", "ignore"] });
|
|
17061
|
+
return true;
|
|
17062
|
+
} catch {
|
|
17063
|
+
return false;
|
|
17064
|
+
}
|
|
17065
|
+
};
|
|
17066
|
+
const ok = run(["config", `credential.${origin}.helper`, ""]) && run(["config", "--add", `credential.${origin}.helper`, "!kryd git-credential"]) && run(["config", `credential.${origin}.useHttpPath`, "true"]);
|
|
17067
|
+
return ok ? "set" : "unavailable";
|
|
17068
|
+
}
|
|
17069
|
+
function originOf(url2) {
|
|
17070
|
+
try {
|
|
17071
|
+
const parsed = new URL(url2);
|
|
17072
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return null;
|
|
17073
|
+
return `${parsed.protocol}//${parsed.host}`;
|
|
17074
|
+
} catch {
|
|
17075
|
+
return null;
|
|
17076
|
+
}
|
|
17077
|
+
}
|
|
16397
17078
|
|
|
16398
17079
|
// src/index.ts
|
|
16399
|
-
function reportError(err) {
|
|
17080
|
+
async function reportError(err) {
|
|
16400
17081
|
if (err instanceof ApiError && err.envelope) {
|
|
16401
17082
|
process.stderr.write(
|
|
16402
17083
|
`${err.envelope.error.code}: ${err.envelope.error.message}
|
|
@@ -16407,6 +17088,63 @@ function reportError(err) {
|
|
|
16407
17088
|
`);
|
|
16408
17089
|
}
|
|
16409
17090
|
process.exitCode = 1;
|
|
17091
|
+
if (err instanceof ApiError) {
|
|
17092
|
+
process.stderr.write(await explainMissingProject(err));
|
|
17093
|
+
}
|
|
17094
|
+
}
|
|
17095
|
+
async function projectIsVisible(token, projectId) {
|
|
17096
|
+
try {
|
|
17097
|
+
return await getProject(currentApiUrl(), token, projectId) !== null;
|
|
17098
|
+
} catch {
|
|
17099
|
+
return null;
|
|
17100
|
+
}
|
|
17101
|
+
}
|
|
17102
|
+
function accountLabel(slug, id) {
|
|
17103
|
+
return slug ?? id;
|
|
17104
|
+
}
|
|
17105
|
+
async function explainMissingProject(err) {
|
|
17106
|
+
if (err.status !== 404) return "";
|
|
17107
|
+
const resolution = resolvedLink();
|
|
17108
|
+
if (!resolution) return "";
|
|
17109
|
+
const token = loadConfig().token;
|
|
17110
|
+
if (!token) return "";
|
|
17111
|
+
if (await projectIsVisible(token, resolution.link.projectId) !== false) return "";
|
|
17112
|
+
let session;
|
|
17113
|
+
try {
|
|
17114
|
+
session = await whoami(currentApiUrl(), token);
|
|
17115
|
+
} catch {
|
|
17116
|
+
return "";
|
|
17117
|
+
}
|
|
17118
|
+
if (!session.accountId) return "";
|
|
17119
|
+
const signedInAs = accountLabel(session.accountSlug, session.accountId);
|
|
17120
|
+
const linkedAccount = resolution.link.accountId;
|
|
17121
|
+
if (!linkedAccount) {
|
|
17122
|
+
return `This folder is linked to project ${resolution.link.projectId}, and you are signed in as ${signedInAs}. That project is not on this account \u2014 if it belongs to another one, switch with \`kryd login\` and run this again.
|
|
17123
|
+
`;
|
|
17124
|
+
}
|
|
17125
|
+
if (linkedAccount === session.accountId) {
|
|
17126
|
+
return `This folder is linked to project ${resolution.link.projectId} on ${signedInAs}, which is the account you are signed in as \u2014 so that project no longer exists.
|
|
17127
|
+
`;
|
|
17128
|
+
}
|
|
17129
|
+
return `This folder is linked to a project of account ${accountLabel(resolution.link.accountSlug, linkedAccount)}, but you are signed in as ${signedInAs}. Switch with \`kryd login\` and run this again.
|
|
17130
|
+
`;
|
|
17131
|
+
}
|
|
17132
|
+
async function backfillLinkAccount() {
|
|
17133
|
+
if (process.exitCode) return;
|
|
17134
|
+
const resolution = resolvedLink();
|
|
17135
|
+
if (!resolution) return;
|
|
17136
|
+
if (resolution.link.accountId && resolution.link.accountSlug) return;
|
|
17137
|
+
const token = loadConfig().token;
|
|
17138
|
+
if (!token) return;
|
|
17139
|
+
if (await projectIsVisible(token, resolution.link.projectId) !== true) return;
|
|
17140
|
+
try {
|
|
17141
|
+
const session = await whoami(currentApiUrl(), token);
|
|
17142
|
+
rememberLinkAccount(resolution.root, {
|
|
17143
|
+
accountId: session.accountId,
|
|
17144
|
+
accountSlug: session.accountSlug
|
|
17145
|
+
});
|
|
17146
|
+
} catch {
|
|
17147
|
+
}
|
|
16410
17148
|
}
|
|
16411
17149
|
function reportNotLinked(example) {
|
|
16412
17150
|
process.stderr.write(
|
|
@@ -16447,7 +17185,7 @@ async function runWhoami(opts = {}) {
|
|
|
16447
17185
|
`
|
|
16448
17186
|
);
|
|
16449
17187
|
} catch (err) {
|
|
16450
|
-
reportError(err);
|
|
17188
|
+
await reportError(err);
|
|
16451
17189
|
}
|
|
16452
17190
|
}
|
|
16453
17191
|
function runLogout() {
|
|
@@ -16463,12 +17201,31 @@ async function runInit(opts) {
|
|
|
16463
17201
|
process.exitCode = 1;
|
|
16464
17202
|
return;
|
|
16465
17203
|
}
|
|
17204
|
+
const existingLink = directoryIsLinked(cwd);
|
|
17205
|
+
if (existingLink) {
|
|
17206
|
+
const remote = hasRemote(cwd, "kryd");
|
|
17207
|
+
const remoteConfigured = remote.status === "ok" && remote.value;
|
|
17208
|
+
if (remoteConfigured) {
|
|
17209
|
+
process.stderr.write(
|
|
17210
|
+
`This folder is already linked to ${existingLink.projectId} (.kryd/project.json).
|
|
17211
|
+
Running \`kryd init\` again would create a SECOND project and leave that one orphaned.
|
|
17212
|
+
To deploy it: \`kryd push\`. To start over: \`kryd project rm\` first, or delete .kryd/project.json to unlink this folder.
|
|
17213
|
+
`
|
|
17214
|
+
);
|
|
17215
|
+
process.exitCode = 1;
|
|
17216
|
+
return;
|
|
17217
|
+
}
|
|
17218
|
+
process.stdout.write(
|
|
17219
|
+
`This folder is linked to ${existingLink.projectId} but has no \`kryd\` remote \u2014 finishing that setup.
|
|
17220
|
+
`
|
|
17221
|
+
);
|
|
17222
|
+
}
|
|
16466
17223
|
const detected = inspectProject(cwd);
|
|
16467
17224
|
let framework;
|
|
16468
17225
|
if (opts.framework) {
|
|
16469
17226
|
if (!isFramework(opts.framework)) {
|
|
16470
17227
|
process.stderr.write(
|
|
16471
|
-
`Unknown framework "${opts.framework}" \u2014 expected react-router, nextjs, vite-spa, or
|
|
17228
|
+
`Unknown framework "${opts.framework}" \u2014 expected react-router, nextjs, vite-spa, node, or workflow.
|
|
16472
17229
|
`
|
|
16473
17230
|
);
|
|
16474
17231
|
process.exitCode = 1;
|
|
@@ -16478,7 +17235,7 @@ async function runInit(opts) {
|
|
|
16478
17235
|
} else {
|
|
16479
17236
|
if (!detected.framework) {
|
|
16480
17237
|
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
|
|
17238
|
+
"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
17239
|
);
|
|
16483
17240
|
process.exitCode = 1;
|
|
16484
17241
|
return;
|
|
@@ -16487,7 +17244,7 @@ async function runInit(opts) {
|
|
|
16487
17244
|
}
|
|
16488
17245
|
const name = opts.name ?? detected.name;
|
|
16489
17246
|
try {
|
|
16490
|
-
const { project: project2, repo
|
|
17247
|
+
const { project: project2, repo } = await linkProject(apiUrl, token, {
|
|
16491
17248
|
name,
|
|
16492
17249
|
framework,
|
|
16493
17250
|
// First `kryd init` for the account claims the tenant slug (the vanity routing key in
|
|
@@ -16501,6 +17258,14 @@ async function runInit(opts) {
|
|
|
16501
17258
|
let linkNote = "";
|
|
16502
17259
|
try {
|
|
16503
17260
|
saveProjectLink(cwd, { projectId: project2.id });
|
|
17261
|
+
try {
|
|
17262
|
+
const session = await whoami(apiUrl, token);
|
|
17263
|
+
rememberLinkAccount(cwd, {
|
|
17264
|
+
accountId: session.accountId,
|
|
17265
|
+
accountSlug: session.accountSlug
|
|
17266
|
+
});
|
|
17267
|
+
} catch {
|
|
17268
|
+
}
|
|
16504
17269
|
linkNote = "Linked this folder \u2192 .kryd/project.json \u2014 `kryd deploy` / `kryd logs` now work here with no id.\n";
|
|
16505
17270
|
try {
|
|
16506
17271
|
ensureKrydIgnored(cwd);
|
|
@@ -16517,44 +17282,49 @@ async function runInit(opts) {
|
|
|
16517
17282
|
);
|
|
16518
17283
|
}
|
|
16519
17284
|
const branch = repo.defaultBranch;
|
|
16520
|
-
const
|
|
16521
|
-
const
|
|
16522
|
-
const remote = configureGitRemote(cwd, "kryd", authedUrl);
|
|
17285
|
+
const remote = configureGitRemote(cwd, "kryd", repo.cloneUrl);
|
|
17286
|
+
const helper = configureCredentialHelper(cwd, repo.cloneUrl);
|
|
16523
17287
|
let nextStep;
|
|
16524
17288
|
switch (remote.status) {
|
|
16525
17289
|
case "added":
|
|
16526
17290
|
case "updated":
|
|
16527
|
-
nextStep = `${remote.status === "added" ? "Added" : "Updated"} git remote "${remote.remote}"
|
|
16528
|
-
` + (
|
|
17291
|
+
nextStep = `${remote.status === "added" ? "Added" : "Updated"} git remote "${remote.remote}".
|
|
17292
|
+
` + (helper === "set" ? "" : `\u26A0\uFE0F Could not register the credential helper, so git will ask for a password on push.
|
|
17293
|
+
Run \`kryd login\` and re-run \`kryd init\` here to fix it.
|
|
17294
|
+
`) + (remote.tracking === "set" ? `Tracking set: \`git push\` and \`kryd push\` both deploy this branch.
|
|
16529
17295
|
` : remote.tracking === "kept-existing" ? `This branch already tracks another remote, so it was left alone \u2014 deploy with \`kryd push\`.
|
|
16530
17296
|
` : "") + `Next: kryd push # push to deploy (same as \`git push ${remote.remote} ${branch}\`)
|
|
16531
17297
|
`;
|
|
16532
17298
|
break;
|
|
16533
17299
|
case "not-a-repo":
|
|
16534
|
-
nextStep = `No git repo here yet. To deploy
|
|
17300
|
+
nextStep = `No git repo here yet. To deploy:
|
|
16535
17301
|
git init && git add -A && git commit -m "init"
|
|
16536
|
-
git remote add kryd ${
|
|
17302
|
+
git remote add kryd ${repo.cloneUrl}
|
|
17303
|
+
kryd init # again, to register the credential helper
|
|
16537
17304
|
kryd push
|
|
16538
17305
|
`;
|
|
16539
17306
|
break;
|
|
16540
17307
|
case "unavailable":
|
|
16541
17308
|
nextStep = gitAvailable(cwd).status === "no-git" ? `git is not installed, so the deploy remote could not be configured.
|
|
16542
17309
|
Install git (https://git-scm.com/downloads), then re-run \`kryd init\` here.
|
|
16543
|
-
` : `Add the remote, then push to deploy
|
|
16544
|
-
git remote add kryd ${
|
|
17310
|
+
` : `Add the remote, then push to deploy:
|
|
17311
|
+
git remote add kryd ${repo.cloneUrl}
|
|
16545
17312
|
kryd push
|
|
16546
17313
|
`;
|
|
16547
17314
|
break;
|
|
16548
17315
|
}
|
|
17316
|
+
const routing = project2.framework === "workflow" ? `No public URL: a workflow worker dials out to the engine and serves nothing.
|
|
17317
|
+
Scheduled and triggered work runs on your production deploy; preview branches build but do not start a worker.
|
|
17318
|
+
` : `Production URL: https://${project2.subdomain}.${PRODUCTION_TLD}
|
|
17319
|
+
Preview branches deploy to https://<branch>-<hash>-${project2.subdomain}.${PREVIEW_TLD}
|
|
17320
|
+
`;
|
|
16549
17321
|
process.stdout.write(
|
|
16550
17322
|
`Linked "${name}" (${project2.framework}) \u2192 ${repo.htmlUrl}
|
|
16551
|
-
|
|
16552
|
-
Preview branches deploy to https://<branch>-<hash>-${project2.subdomain}.${PREVIEW_TLD}
|
|
16553
|
-
${linkNote}
|
|
17323
|
+
` + routing + `${linkNote}
|
|
16554
17324
|
${nextStep}`
|
|
16555
17325
|
);
|
|
16556
17326
|
} catch (err) {
|
|
16557
|
-
reportError(err);
|
|
17327
|
+
await reportError(err);
|
|
16558
17328
|
}
|
|
16559
17329
|
}
|
|
16560
17330
|
function formatStatus(event) {
|
|
@@ -16670,7 +17440,7 @@ async function runLogs(opts) {
|
|
|
16670
17440
|
}
|
|
16671
17441
|
await followDeploy(apiUrl, token, deploymentId);
|
|
16672
17442
|
} catch (err) {
|
|
16673
|
-
reportError(err);
|
|
17443
|
+
await reportError(err);
|
|
16674
17444
|
}
|
|
16675
17445
|
}
|
|
16676
17446
|
async function runRuntimeLogs(opts) {
|
|
@@ -16717,7 +17487,7 @@ async function runRuntimeLogs(opts) {
|
|
|
16717
17487
|
}
|
|
16718
17488
|
);
|
|
16719
17489
|
} catch (err) {
|
|
16720
|
-
reportError(err);
|
|
17490
|
+
await reportError(err);
|
|
16721
17491
|
} finally {
|
|
16722
17492
|
process.off("SIGINT", onSigint);
|
|
16723
17493
|
}
|
|
@@ -16741,7 +17511,7 @@ async function runDeploy(opts) {
|
|
|
16741
17511
|
`);
|
|
16742
17512
|
await followDeploy(apiUrl, token, deploymentId, { render: renderFor(opts.logs) });
|
|
16743
17513
|
} catch (err) {
|
|
16744
|
-
reportError(err);
|
|
17514
|
+
await reportError(err);
|
|
16745
17515
|
}
|
|
16746
17516
|
}
|
|
16747
17517
|
function renderFor(logs) {
|
|
@@ -16879,7 +17649,7 @@ async function runPush(opts) {
|
|
|
16879
17649
|
estimateFrom: recent
|
|
16880
17650
|
});
|
|
16881
17651
|
} catch (err) {
|
|
16882
|
-
reportError(err);
|
|
17652
|
+
await reportError(err);
|
|
16883
17653
|
}
|
|
16884
17654
|
}
|
|
16885
17655
|
async function runRollback(opts) {
|
|
@@ -16909,7 +17679,7 @@ async function runRollback(opts) {
|
|
|
16909
17679
|
);
|
|
16910
17680
|
await followDeploy(apiUrl, token, deploymentId, { render: renderFor(opts.logs) });
|
|
16911
17681
|
} catch (err) {
|
|
16912
|
-
reportError(err);
|
|
17682
|
+
await reportError(err);
|
|
16913
17683
|
}
|
|
16914
17684
|
}
|
|
16915
17685
|
async function runRedeploy(opts) {
|
|
@@ -16938,7 +17708,7 @@ ${note}
|
|
|
16938
17708
|
);
|
|
16939
17709
|
await followDeploy(apiUrl, token, deploymentId, { render: renderFor(opts.logs) });
|
|
16940
17710
|
} catch (err) {
|
|
16941
|
-
reportError(err);
|
|
17711
|
+
await reportError(err);
|
|
16942
17712
|
}
|
|
16943
17713
|
}
|
|
16944
17714
|
async function runDbAdd(opts) {
|
|
@@ -17003,7 +17773,7 @@ The connection string will be injected as DATABASE_URL on your next deploy.
|
|
|
17003
17773
|
retryCmd: `kryd db status ${project2}`
|
|
17004
17774
|
});
|
|
17005
17775
|
} catch (err) {
|
|
17006
|
-
reportError(err);
|
|
17776
|
+
await reportError(err);
|
|
17007
17777
|
}
|
|
17008
17778
|
}
|
|
17009
17779
|
function reportSettleResult(result, opts) {
|
|
@@ -17050,7 +17820,7 @@ Its S3 credentials will be injected on your next deploy.
|
|
|
17050
17820
|
retryCmd: `kryd storage status ${project2}`
|
|
17051
17821
|
});
|
|
17052
17822
|
} catch (err) {
|
|
17053
|
-
reportError(err);
|
|
17823
|
+
await reportError(err);
|
|
17054
17824
|
}
|
|
17055
17825
|
}
|
|
17056
17826
|
function reportDetachResult(result, opts) {
|
|
@@ -17093,7 +17863,7 @@ async function resolveProjectIdByName(apiUrl, token, name) {
|
|
|
17093
17863
|
try {
|
|
17094
17864
|
projects = await listProjects(apiUrl, token);
|
|
17095
17865
|
} catch (err) {
|
|
17096
|
-
reportError(err);
|
|
17866
|
+
await reportError(err);
|
|
17097
17867
|
return null;
|
|
17098
17868
|
}
|
|
17099
17869
|
const matches = projects.filter((p) => p.name === name);
|
|
@@ -17154,7 +17924,7 @@ async function runProjectRemove(opts) {
|
|
|
17154
17924
|
}
|
|
17155
17925
|
resources = project2.status === "delete_failed" ? ["whatever the previous attempt did not manage to remove"] : await describeProjectResources(apiUrl, token, projectId);
|
|
17156
17926
|
} catch (err) {
|
|
17157
|
-
reportError(err);
|
|
17927
|
+
await reportError(err);
|
|
17158
17928
|
return;
|
|
17159
17929
|
}
|
|
17160
17930
|
if (!opts.yes) {
|
|
@@ -17191,7 +17961,7 @@ async function runProjectRemove(opts) {
|
|
|
17191
17961
|
);
|
|
17192
17962
|
await followProjectDeletion(apiUrl, token, projectId, project2.name, opts);
|
|
17193
17963
|
} catch (err) {
|
|
17194
|
-
reportError(err);
|
|
17964
|
+
await reportError(err);
|
|
17195
17965
|
}
|
|
17196
17966
|
}
|
|
17197
17967
|
async function followProjectDeletion(apiUrl, token, projectId, name, opts) {
|
|
@@ -17267,7 +18037,7 @@ async function runDbRemove(opts) {
|
|
|
17267
18037
|
retryCmd: `kryd db remove ${project2}`
|
|
17268
18038
|
});
|
|
17269
18039
|
} catch (err) {
|
|
17270
|
-
reportError(err);
|
|
18040
|
+
await reportError(err);
|
|
17271
18041
|
}
|
|
17272
18042
|
}
|
|
17273
18043
|
async function runAiAdd(opts) {
|
|
@@ -17292,7 +18062,7 @@ async function runAiAdd(opts) {
|
|
|
17292
18062
|
`
|
|
17293
18063
|
);
|
|
17294
18064
|
} catch (err) {
|
|
17295
|
-
reportError(err);
|
|
18065
|
+
await reportError(err);
|
|
17296
18066
|
}
|
|
17297
18067
|
}
|
|
17298
18068
|
async function runAiRemove(opts) {
|
|
@@ -17312,7 +18082,7 @@ async function runAiRemove(opts) {
|
|
|
17312
18082
|
try {
|
|
17313
18083
|
current = await getAiStatus(apiUrl, token, project2);
|
|
17314
18084
|
} catch (err) {
|
|
17315
|
-
reportError(err);
|
|
18085
|
+
await reportError(err);
|
|
17316
18086
|
return;
|
|
17317
18087
|
}
|
|
17318
18088
|
if (!current.enabled) {
|
|
@@ -17337,7 +18107,7 @@ AI_GATEWAY_URL and AI_GATEWAY_TOKEN stop being injected from your next deploy (k
|
|
|
17337
18107
|
`
|
|
17338
18108
|
);
|
|
17339
18109
|
} catch (err) {
|
|
17340
|
-
reportError(err);
|
|
18110
|
+
await reportError(err);
|
|
17341
18111
|
}
|
|
17342
18112
|
}
|
|
17343
18113
|
async function reportResourceStatus(opts) {
|
|
@@ -17354,12 +18124,12 @@ Remove it before attaching another.
|
|
|
17354
18124
|
process.stdout.write(`${opts.project}: ${opts.noun} ${result.status}.
|
|
17355
18125
|
`);
|
|
17356
18126
|
} catch (err) {
|
|
17357
|
-
if (err instanceof ApiError && err.status === 404) {
|
|
18127
|
+
if (err instanceof ApiError && err.status === 404 && await projectIsVisible(opts.token, opts.project) === true) {
|
|
17358
18128
|
process.stdout.write(`${opts.project}: no ${opts.noun} attached.
|
|
17359
18129
|
`);
|
|
17360
18130
|
return;
|
|
17361
18131
|
}
|
|
17362
|
-
reportError(err);
|
|
18132
|
+
await reportError(err);
|
|
17363
18133
|
}
|
|
17364
18134
|
}
|
|
17365
18135
|
async function runDbStatus(opts) {
|
|
@@ -17378,7 +18148,8 @@ async function runDbStatus(opts) {
|
|
|
17378
18148
|
await reportResourceStatus({
|
|
17379
18149
|
read: () => getDatabaseStatus(apiUrl, token, project2),
|
|
17380
18150
|
noun: "database",
|
|
17381
|
-
project: project2
|
|
18151
|
+
project: project2,
|
|
18152
|
+
token
|
|
17382
18153
|
});
|
|
17383
18154
|
}
|
|
17384
18155
|
async function runStorageStatus(opts) {
|
|
@@ -17397,7 +18168,8 @@ async function runStorageStatus(opts) {
|
|
|
17397
18168
|
await reportResourceStatus({
|
|
17398
18169
|
read: () => getStorageStatus(apiUrl, token, project2),
|
|
17399
18170
|
noun: "object storage",
|
|
17400
|
-
project: project2
|
|
18171
|
+
project: project2,
|
|
18172
|
+
token
|
|
17401
18173
|
});
|
|
17402
18174
|
}
|
|
17403
18175
|
async function runAiStatus(opts) {
|
|
@@ -17426,7 +18198,7 @@ async function runAiStatus(opts) {
|
|
|
17426
18198
|
`
|
|
17427
18199
|
);
|
|
17428
18200
|
} catch (err) {
|
|
17429
|
-
reportError(err);
|
|
18201
|
+
await reportError(err);
|
|
17430
18202
|
}
|
|
17431
18203
|
}
|
|
17432
18204
|
function isoDay(iso) {
|
|
@@ -17466,7 +18238,7 @@ HATCHET_CLIENT_TOKEN will be injected on your next deploy (kryd deploy)` + (stat
|
|
|
17466
18238
|
` : ".\n")
|
|
17467
18239
|
);
|
|
17468
18240
|
} catch (err) {
|
|
17469
|
-
reportError(err);
|
|
18241
|
+
await reportError(err);
|
|
17470
18242
|
}
|
|
17471
18243
|
}
|
|
17472
18244
|
async function runWorkflowRemove(opts) {
|
|
@@ -17486,7 +18258,7 @@ async function runWorkflowRemove(opts) {
|
|
|
17486
18258
|
try {
|
|
17487
18259
|
current = await getWorkflowStatus(apiUrl, token, project2);
|
|
17488
18260
|
} catch (err) {
|
|
17489
|
-
reportError(err);
|
|
18261
|
+
await reportError(err);
|
|
17490
18262
|
return;
|
|
17491
18263
|
}
|
|
17492
18264
|
if (!current.enabled) {
|
|
@@ -17511,7 +18283,7 @@ Workflow history, crons and schedules stay until the project is deleted.
|
|
|
17511
18283
|
`
|
|
17512
18284
|
);
|
|
17513
18285
|
} catch (err) {
|
|
17514
|
-
reportError(err);
|
|
18286
|
+
await reportError(err);
|
|
17515
18287
|
}
|
|
17516
18288
|
}
|
|
17517
18289
|
async function runWorkflowStatus(opts) {
|
|
@@ -17551,8 +18323,122 @@ async function runWorkflowStatus(opts) {
|
|
|
17551
18323
|
);
|
|
17552
18324
|
}
|
|
17553
18325
|
} catch (err) {
|
|
17554
|
-
reportError(err);
|
|
18326
|
+
await reportError(err);
|
|
18327
|
+
}
|
|
18328
|
+
}
|
|
18329
|
+
async function runWorkflowInit(opts) {
|
|
18330
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
18331
|
+
if (!opts.language) {
|
|
18332
|
+
process.stderr.write(
|
|
18333
|
+
`Pass --language <${SCAFFOLD_LANGUAGES.join("|")}>.
|
|
18334
|
+
`
|
|
18335
|
+
);
|
|
18336
|
+
process.exitCode = 1;
|
|
18337
|
+
return;
|
|
18338
|
+
}
|
|
18339
|
+
if (!isScaffoldLanguage(opts.language)) {
|
|
18340
|
+
process.stderr.write(
|
|
18341
|
+
`Unknown language "${opts.language}" \u2014 expected ${SCAFFOLD_LANGUAGES.join(", ")}.
|
|
18342
|
+
`
|
|
18343
|
+
);
|
|
18344
|
+
process.exitCode = 1;
|
|
18345
|
+
return;
|
|
18346
|
+
}
|
|
18347
|
+
const language = opts.language;
|
|
18348
|
+
const target = opts.dir ? resolve2(cwd, opts.dir) : cwd;
|
|
18349
|
+
const workerName = workerNameFrom(opts.name ?? basename2(target));
|
|
18350
|
+
const files = scaffoldFiles(language, workerName);
|
|
18351
|
+
const clashes = files.map((f) => f.path).filter((p) => existsSync3(join3(target, p)));
|
|
18352
|
+
if (clashes.length > 0) {
|
|
18353
|
+
process.stderr.write(
|
|
18354
|
+
`Refusing to overwrite: ${clashes.join(", ")}.
|
|
18355
|
+
Nothing was written. Run this in an empty directory, or pass --dir <path>.
|
|
18356
|
+
`
|
|
18357
|
+
);
|
|
18358
|
+
process.exitCode = 1;
|
|
18359
|
+
return;
|
|
18360
|
+
}
|
|
18361
|
+
const written = [];
|
|
18362
|
+
try {
|
|
18363
|
+
for (const file2 of files) {
|
|
18364
|
+
const full = join3(target, file2.path);
|
|
18365
|
+
mkdirSync2(dirname2(full), { recursive: true });
|
|
18366
|
+
writeFileSync2(full, file2.contents);
|
|
18367
|
+
written.push(full);
|
|
18368
|
+
}
|
|
18369
|
+
} catch (err) {
|
|
18370
|
+
for (const path of written.reverse()) {
|
|
18371
|
+
try {
|
|
18372
|
+
rmSync2(path);
|
|
18373
|
+
} catch {
|
|
18374
|
+
}
|
|
18375
|
+
}
|
|
18376
|
+
process.stderr.write(
|
|
18377
|
+
`Could not write the scaffold (${err instanceof Error ? err.message : String(err)}).
|
|
18378
|
+
Nothing was left behind.
|
|
18379
|
+
`
|
|
18380
|
+
);
|
|
18381
|
+
process.exitCode = 1;
|
|
18382
|
+
return;
|
|
18383
|
+
}
|
|
18384
|
+
const where = opts.dir ? `${opts.dir}/` : "";
|
|
18385
|
+
process.stdout.write(
|
|
18386
|
+
`Scaffolded a ${language} workflow worker${opts.dir ? ` in ${opts.dir}` : ""}:
|
|
18387
|
+
` + files.map((f) => ` ${where}${f.path}
|
|
18388
|
+
`).join("") + "\n"
|
|
18389
|
+
);
|
|
18390
|
+
const repo = initRepo(target);
|
|
18391
|
+
if (repo.status === "nested") {
|
|
18392
|
+
process.stderr.write(
|
|
18393
|
+
`That directory is inside the git repository at ${repo.root}.
|
|
18394
|
+
A worker has to be its own repository: Kryd builds one app per repository, and git would
|
|
18395
|
+
write the deploy remote into the enclosing repo, so \`kryd push\` would push that tree instead.
|
|
18396
|
+
The files above were written \u2014 move them to a directory of their own, or run this outside that repository.
|
|
18397
|
+
`
|
|
18398
|
+
);
|
|
18399
|
+
process.exitCode = 1;
|
|
18400
|
+
return;
|
|
18401
|
+
}
|
|
18402
|
+
const next = [];
|
|
18403
|
+
if (opts.dir) next.push(`cd ${opts.dir}`);
|
|
18404
|
+
const first = firstCommand(language);
|
|
18405
|
+
if (first) next.push(first);
|
|
18406
|
+
next.push('git add . && git commit -m "scaffold"');
|
|
18407
|
+
if (opts.link === false) {
|
|
18408
|
+
next.push("kryd init", "kryd workflow add", "kryd push");
|
|
18409
|
+
process.stdout.write(
|
|
18410
|
+
`Next: ${next.map((c) => `\`${c}\``).join(", then ")}.
|
|
18411
|
+
HATCHET_CLIENT_TOKEN is injected into your container on deploy; it is never available locally.
|
|
18412
|
+
`
|
|
18413
|
+
);
|
|
18414
|
+
return;
|
|
18415
|
+
}
|
|
18416
|
+
if (repo.status === "failed") {
|
|
18417
|
+
process.stderr.write(
|
|
18418
|
+
"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"
|
|
18419
|
+
);
|
|
18420
|
+
process.exitCode = 1;
|
|
18421
|
+
return;
|
|
18422
|
+
}
|
|
18423
|
+
process.exitCode = 0;
|
|
18424
|
+
await runInit({
|
|
18425
|
+
cwd: target,
|
|
18426
|
+
...opts.name ? { name: opts.name } : {},
|
|
18427
|
+
...opts.apiUrl ? { apiUrl: opts.apiUrl } : {}
|
|
18428
|
+
});
|
|
18429
|
+
if (process.exitCode) {
|
|
18430
|
+
process.stderr.write(
|
|
18431
|
+
"\nThe files above were written; only the link failed. Fix the problem above and run `kryd init` here.\n"
|
|
18432
|
+
);
|
|
18433
|
+
return;
|
|
17555
18434
|
}
|
|
18435
|
+
next.push("kryd workflow add", "kryd push");
|
|
18436
|
+
process.stdout.write(
|
|
18437
|
+
`
|
|
18438
|
+
Next: ${next.map((c) => `\`${c}\``).join(", then ")}.
|
|
18439
|
+
HATCHET_CLIENT_TOKEN is injected into your container on deploy; it is never available locally.
|
|
18440
|
+
`
|
|
18441
|
+
);
|
|
17556
18442
|
}
|
|
17557
18443
|
async function runStorageRemove(opts) {
|
|
17558
18444
|
const apiUrl = resolveApiUrl(opts.apiUrl);
|
|
@@ -17590,7 +18476,7 @@ async function runStorageRemove(opts) {
|
|
|
17590
18476
|
retryCmd: `kryd storage remove ${project2}`
|
|
17591
18477
|
});
|
|
17592
18478
|
} catch (err) {
|
|
17593
|
-
reportError(err);
|
|
18479
|
+
await reportError(err);
|
|
17594
18480
|
}
|
|
17595
18481
|
}
|
|
17596
18482
|
function splitKeyValue(spec) {
|
|
@@ -17629,7 +18515,7 @@ async function runProjectList(opts) {
|
|
|
17629
18515
|
try {
|
|
17630
18516
|
projects = await listProjects(apiUrl, token);
|
|
17631
18517
|
} catch (err) {
|
|
17632
|
-
reportError(err);
|
|
18518
|
+
await reportError(err);
|
|
17633
18519
|
return;
|
|
17634
18520
|
}
|
|
17635
18521
|
if (projects.length === 0) {
|
|
@@ -17737,7 +18623,7 @@ Set one with \`kryd env set KEY --stdin\`, or attach a database, storage or the
|
|
|
17737
18623
|
out += "\nValues are never shown. This is the stored set \u2014 runtime changes reach your container at its next deploy (run `kryd redeploy` to apply them now)" + (buildVars.length > 0 ? ", and build-time changes at its next build.\n" : ".\n");
|
|
17738
18624
|
process.stdout.write(out);
|
|
17739
18625
|
} catch (err) {
|
|
17740
|
-
reportError(err);
|
|
18626
|
+
await reportError(err);
|
|
17741
18627
|
}
|
|
17742
18628
|
}
|
|
17743
18629
|
function toDotenvLine(key, value) {
|
|
@@ -17765,7 +18651,7 @@ async function runEnvPull(opts) {
|
|
|
17765
18651
|
const cwd = opts.cwd ?? process.cwd();
|
|
17766
18652
|
const root = findProjectLinkDir(cwd) ?? cwd;
|
|
17767
18653
|
const outRel = opts.out ?? ".env.kryd";
|
|
17768
|
-
const outPath =
|
|
18654
|
+
const outPath = resolve2(root, outRel);
|
|
17769
18655
|
if (opts.out !== void 0 && existsSync3(outPath) && !opts.force) {
|
|
17770
18656
|
process.stderr.write(
|
|
17771
18657
|
`Refusing to overwrite ${outRel} \u2014 pass --force to replace it (or omit --out to write .env.kryd).
|
|
@@ -17806,7 +18692,7 @@ ${body}
|
|
|
17806
18692
|
"These are your own values, read back from Kryd. Managed values (DATABASE_URL, storage, the AI gateway) are never pulled.\n"
|
|
17807
18693
|
);
|
|
17808
18694
|
} catch (err) {
|
|
17809
|
-
reportError(err);
|
|
18695
|
+
await reportError(err);
|
|
17810
18696
|
}
|
|
17811
18697
|
}
|
|
17812
18698
|
async function resolveEnvValue(inline, opts) {
|
|
@@ -17939,7 +18825,7 @@ async function runEnvSet(opts) {
|
|
|
17939
18825
|
${NEXT_BUILD_NOTE}` : NEXT_DEPLOY_NOTE)
|
|
17940
18826
|
);
|
|
17941
18827
|
} catch (err) {
|
|
17942
|
-
reportError(err);
|
|
18828
|
+
await reportError(err);
|
|
17943
18829
|
}
|
|
17944
18830
|
}
|
|
17945
18831
|
async function runEnvRemove(opts) {
|
|
@@ -17999,7 +18885,7 @@ Run \`kryd env list\` to see what is set. (If you meant the runtime variable of
|
|
|
17999
18885
|
return;
|
|
18000
18886
|
}
|
|
18001
18887
|
} catch (err) {
|
|
18002
|
-
reportError(err);
|
|
18888
|
+
await reportError(err);
|
|
18003
18889
|
return;
|
|
18004
18890
|
}
|
|
18005
18891
|
}
|
|
@@ -18037,21 +18923,32 @@ Run \`kryd env list\` to see what is set. (If you meant the runtime variable of
|
|
|
18037
18923
|
`)
|
|
18038
18924
|
);
|
|
18039
18925
|
} catch (err) {
|
|
18040
|
-
reportError(err);
|
|
18926
|
+
await reportError(err);
|
|
18041
18927
|
}
|
|
18042
18928
|
}
|
|
18043
|
-
var CLI_VERSION = true ? "0.
|
|
18929
|
+
var CLI_VERSION = true ? "0.8.1" : "0.0.0-dev";
|
|
18044
18930
|
var program = new Command();
|
|
18045
18931
|
program.name("kryd").description("Kryd CLI").version(CLI_VERSION);
|
|
18932
|
+
program.hook("postAction", async () => {
|
|
18933
|
+
await backfillLinkAccount();
|
|
18934
|
+
});
|
|
18046
18935
|
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(
|
|
18047
18936
|
"--token <token>",
|
|
18048
18937
|
"supply a token instead of the browser flow (prefer $KRYD_TOKEN \u2014 flags are visible in `ps`)"
|
|
18049
18938
|
).option("--api-url <url>", "control-plane API base URL").option("--dashboard-url <url>", "dashboard base URL for the browser approve page (default app.kryd.eu)").action((opts) => runLogin(opts));
|
|
18050
18939
|
program.command("whoami").description("Show the current account").option("--api-url <url>", "control-plane API base URL").action((opts) => runWhoami(opts));
|
|
18051
18940
|
program.command("logout").description("Clear the stored token").action(() => runLogout());
|
|
18941
|
+
program.command("git-credential <operation>", { hidden: true }).description("git credential helper (invoked by git, not by you)").option("--api-url <url>", "control-plane API base URL").action(async (operation, opts) => {
|
|
18942
|
+
const chunks = [];
|
|
18943
|
+
for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
|
|
18944
|
+
await runGitCredential(operation, {
|
|
18945
|
+
stdin: Buffer.concat(chunks).toString("utf8"),
|
|
18946
|
+
...opts.apiUrl ? { apiUrl: opts.apiUrl } : {}
|
|
18947
|
+
});
|
|
18948
|
+
});
|
|
18052
18949
|
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
18950
|
"--framework <framework>",
|
|
18054
|
-
"react-router | nextjs | vite-spa | node (auto-detected if omitted)"
|
|
18951
|
+
"react-router | nextjs | vite-spa | node (auto-detected if omitted; `workflow` comes from a committed kryd.json)"
|
|
18055
18952
|
).option(
|
|
18056
18953
|
"--tenant <slug>",
|
|
18057
18954
|
// KRYD-265 / KRYD-188: this used to say "claimed once on first init", which was false — signup
|
|
@@ -18140,6 +19037,12 @@ var workflow = program.command("workflow").description("Manage the project's dur
|
|
|
18140
19037
|
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
19038
|
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
19039
|
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
|
+
workflow.command("init").description(
|
|
19041
|
+
"Scaffold a workflow worker here (with the health listener Kryd's deploy gate needs) and link it"
|
|
19042
|
+
).requiredOption(
|
|
19043
|
+
"--language <language>",
|
|
19044
|
+
`worker language: ${SCAFFOLD_LANGUAGES.join(" | ")}`
|
|
19045
|
+
).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
19046
|
function invokedDirectly() {
|
|
18144
19047
|
const entry = process.argv[1];
|
|
18145
19048
|
if (!entry) return false;
|
|
@@ -18159,6 +19062,7 @@ if (invokedDirectly()) {
|
|
|
18159
19062
|
});
|
|
18160
19063
|
}
|
|
18161
19064
|
export {
|
|
19065
|
+
backfillLinkAccount,
|
|
18162
19066
|
program,
|
|
18163
19067
|
runAiAdd,
|
|
18164
19068
|
runAiRemove,
|
|
@@ -18186,6 +19090,7 @@ export {
|
|
|
18186
19090
|
runStorageStatus,
|
|
18187
19091
|
runWhoami,
|
|
18188
19092
|
runWorkflowAdd,
|
|
19093
|
+
runWorkflowInit,
|
|
18189
19094
|
runWorkflowRemove,
|
|
18190
19095
|
runWorkflowStatus,
|
|
18191
19096
|
splitKeyValue
|