loopable-cli 0.1.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/.output/nitro.json +17 -0
- package/.output/public/apple-touch-icon.png +0 -0
- package/.output/public/assets/_connectorId-BWjYn5tm.js +1 -0
- package/.output/public/assets/_loopId-Ds4RkyEc.js +1 -0
- package/.output/public/assets/_taskId-BOzICXDf.js +1 -0
- package/.output/public/assets/alert-Bn9-Zm0l.js +1 -0
- package/.output/public/assets/arrow-left-BF9juV9T.js +1 -0
- package/.output/public/assets/arrow-right-CzqvnOY1.js +1 -0
- package/.output/public/assets/button-HCY6DCY-.js +1 -0
- package/.output/public/assets/connection-status-CYZB8kLy.js +1 -0
- package/.output/public/assets/connector-icon-BZ_MUJV_.js +1 -0
- package/.output/public/assets/connectors-BvzttfSu.js +1 -0
- package/.output/public/assets/connectors-DdJNwRN_.js +1 -0
- package/.output/public/assets/external-link-D46X-bXa.js +1 -0
- package/.output/public/assets/gaps-D-I6xiDc.js +1 -0
- package/.output/public/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/.output/public/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/.output/public/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/.output/public/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/.output/public/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/.output/public/assets/inbox-BUUu9xxE.js +1 -0
- package/.output/public/assets/index-ztzex0FN.js +10 -0
- package/.output/public/assets/loader-CcBl2u8m.js +1 -0
- package/.output/public/assets/loop-form-dB3CLcZb.js +1 -0
- package/.output/public/assets/loops-DRunQrxL.js +1 -0
- package/.output/public/assets/loops-oSdqMd29.js +1 -0
- package/.output/public/assets/manifests-DNMqf7Vi.js +7 -0
- package/.output/public/assets/new-uyFxVjuB.js +1 -0
- package/.output/public/assets/preload-helper-ORbuV86A.js +3 -0
- package/.output/public/assets/runners-DSU3r5Ww.js +1 -0
- package/.output/public/assets/setting-fields-ixkpdBb-.js +3 -0
- package/.output/public/assets/styles-DZL5V1o7.css +2 -0
- package/.output/public/assets/switch-d5ummmHY.js +1 -0
- package/.output/public/assets/task-list-BpOStt9v.js +1 -0
- package/.output/public/assets/tasks-B8MsKgic.js +1 -0
- package/.output/public/assets/trash-2-D6u6NpzO.js +1 -0
- package/.output/public/favicon.svg +9 -0
- package/.output/server/_chunks/ssr-renderer.mjs +26 -0
- package/.output/server/_connectorId-8nb5yOzG.mjs +421 -0
- package/.output/server/_libs/@base-ui/react+[...].mjs +12841 -0
- package/.output/server/_libs/@tanstack/react-router+[...].mjs +14349 -0
- package/.output/server/_libs/class-variance-authority+clsx.mjs +69 -0
- package/.output/server/_libs/dijkstrajs.mjs +118 -0
- package/.output/server/_libs/drizzle-orm.mjs +4349 -0
- package/.output/server/_libs/h3+rou3+srvx.mjs +1361 -0
- package/.output/server/_libs/lucide-react.mjs +911 -0
- package/.output/server/_libs/next-themes.mjs +53 -0
- package/.output/server/_libs/pngjs.mjs +1680 -0
- package/.output/server/_libs/qrcode.mjs +2583 -0
- package/.output/server/_libs/sonner.mjs +955 -0
- package/.output/server/_libs/tailwind-merge.mjs +3380 -0
- package/.output/server/_libs/tanstack__history.mjs +341 -0
- package/.output/server/_libs/ufo.mjs +64 -0
- package/.output/server/_loopId-BsUL1SeL.mjs +272 -0
- package/.output/server/_runtime.mjs +35 -0
- package/.output/server/_ssr/access-zIl15o8y.mjs +42 -0
- package/.output/server/_ssr/agents-Btql102J.mjs +657 -0
- package/.output/server/_ssr/agents-DAOZPgO2.mjs +76 -0
- package/.output/server/_ssr/alert-NEiOCTOl.mjs +37 -0
- package/.output/server/_ssr/callback-CUINvl7M.mjs +62 -0
- package/.output/server/_ssr/client-CKkOfh-s.mjs +335 -0
- package/.output/server/_ssr/connection-status-Bi5peUAk.mjs +31 -0
- package/.output/server/_ssr/connections-qnw2GVlP.mjs +2542 -0
- package/.output/server/_ssr/connector-icon-DZXQLLFo.mjs +37 -0
- package/.output/server/_ssr/connectors-BHNwuIMj.mjs +106 -0
- package/.output/server/_ssr/connectors-C28g0i4c.mjs +83 -0
- package/.output/server/_ssr/createServerRpc-DPDrnFdH.mjs +12 -0
- package/.output/server/_ssr/empty-plugin-adapters-BJ4m9L8g.mjs +5 -0
- package/.output/server/_ssr/gaps-RAFSXLZf.mjs +53 -0
- package/.output/server/_ssr/inbox-k5zHergU.mjs +35 -0
- package/.output/server/_ssr/loop-form-DlObSewZ.mjs +412 -0
- package/.output/server/_ssr/loops-APFcP0y6.mjs +311 -0
- package/.output/server/_ssr/loops-xt2Bn7_q.mjs +222 -0
- package/.output/server/_ssr/new-DfKyd2Wa.mjs +169 -0
- package/.output/server/_ssr/router-3WmpE5b_.mjs +1553 -0
- package/.output/server/_ssr/runners-By-mwc3F.mjs +32 -0
- package/.output/server/_ssr/runners-DI_NxpHC.mjs +116 -0
- package/.output/server/_ssr/runners-DtXs4Q8a.mjs +106 -0
- package/.output/server/_ssr/secrets-Hqti-niS.mjs +199 -0
- package/.output/server/_ssr/setting-fields-8vKFj4df.mjs +92 -0
- package/.output/server/_ssr/ssr.mjs +6605 -0
- package/.output/server/_ssr/start-Bdc2GJUI.mjs +51 -0
- package/.output/server/_ssr/switch-ReNglgH6.mjs +18 -0
- package/.output/server/_ssr/task-list-CUITIqkb.mjs +186 -0
- package/.output/server/_ssr/tasks-SAjNovX6.mjs +110 -0
- package/.output/server/_ssr/tasks-vcoodp_D.mjs +533 -0
- package/.output/server/_tanstack-start-manifest_v-C0cP5F0M.mjs +125 -0
- package/.output/server/_taskId-CXS3lod0.mjs +249 -0
- package/.output/server/index.mjs +541 -0
- package/README.md +81 -0
- package/dist/cli.mjs +165 -0
- package/dist/dispatcher.mjs +9871 -0
- package/dist/runner.mjs +657 -0
- package/drizzle/0000_cold_demogoblin.sql +29 -0
- package/drizzle/0001_fancy_blonde_phantom.sql +13 -0
- package/drizzle/0002_spooky_smiling_tiger.sql +16 -0
- package/drizzle/0003_small_golden_guardian.sql +25 -0
- package/drizzle/0004_wonderful_thunderbolts.sql +39 -0
- package/drizzle/0005_named_workflows.sql +7 -0
- package/drizzle/0006_signals.sql +18 -0
- package/drizzle/0007_review_comments.sql +1 -0
- package/drizzle/0008_held_signals.sql +1 -0
- package/drizzle/0009_work_item_ref.sql +58 -0
- package/drizzle/0010_signal_payload.sql +2 -0
- package/drizzle/0011_rule_poll_cursor.sql +1 -0
- package/drizzle/0012_loops.sql +63 -0
- package/drizzle/0013_loop_owns_action_and_prompt.sql +154 -0
- package/drizzle/0014_loop_poll_every.sql +4 -0
- package/drizzle/0015_machines.sql +10 -0
- package/drizzle/0016_task_machine.sql +1 -0
- package/drizzle/0017_loop_machine.sql +1 -0
- package/drizzle/0018_runners.sql +1 -0
- package/drizzle/0019_task_runner.sql +1 -0
- package/drizzle/0020_loop_runner.sql +1 -0
- package/drizzle/0021_drop_runner_kind.sql +1 -0
- package/drizzle/0022_drop_loop_runner.sql +1 -0
- package/drizzle/0023_shared_host.sql +6 -0
- package/drizzle/meta/0000_snapshot.json +206 -0
- package/drizzle/meta/0001_snapshot.json +284 -0
- package/drizzle/meta/0002_snapshot.json +390 -0
- package/drizzle/meta/0003_snapshot.json +566 -0
- package/drizzle/meta/0004_snapshot.json +633 -0
- package/drizzle/meta/0005_snapshot.json +626 -0
- package/drizzle/meta/0006_snapshot.json +752 -0
- package/drizzle/meta/0007_snapshot.json +759 -0
- package/drizzle/meta/0008_snapshot.json +766 -0
- package/drizzle/meta/0009_snapshot.json +752 -0
- package/drizzle/meta/0010_snapshot.json +766 -0
- package/drizzle/meta/0011_snapshot.json +773 -0
- package/drizzle/meta/0012_snapshot.json +773 -0
- package/drizzle/meta/0013_snapshot.json +817 -0
- package/drizzle/meta/_journal.json +174 -0
- package/package.json +88 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { n as createServerFn } from "./ssr.mjs";
|
|
2
|
+
import { i as loopableOrigin } from "./callback-CUINvl7M.mjs";
|
|
3
|
+
import { c as listRunners, n as forgetRunner, r as getJoinToken, u as rotateJoinToken } from "./runners-DtXs4Q8a.mjs";
|
|
4
|
+
import { t as createServerRpc } from "./createServerRpc-DPDrnFdH.mjs";
|
|
5
|
+
//#region node_modules/.nitro/vite/services/ssr/assets/runners-By-mwc3F.js
|
|
6
|
+
var getRunnersPage_createServerFn_handler = createServerRpc({
|
|
7
|
+
id: "45dbbb612763589268984d236edcd17f5868ecf716df9625bfa46e7fec691f4e",
|
|
8
|
+
name: "getRunnersPage",
|
|
9
|
+
filename: "src/server/functions/runners.ts"
|
|
10
|
+
}, (opts) => getRunnersPage.__executeServer(opts));
|
|
11
|
+
var getRunnersPage = createServerFn({ method: "GET" }).handler(getRunnersPage_createServerFn_handler, async () => ({
|
|
12
|
+
runners: await listRunners(),
|
|
13
|
+
joinToken: await getJoinToken(),
|
|
14
|
+
origin: loopableOrigin()
|
|
15
|
+
}));
|
|
16
|
+
var rotateRunnerJoinToken_createServerFn_handler = createServerRpc({
|
|
17
|
+
id: "b4f7a66e1bb69cc14ab84d8f5dc7f85119c9a9b1d065a6b4e51dd0ba84c3f60c",
|
|
18
|
+
name: "rotateRunnerJoinToken",
|
|
19
|
+
filename: "src/server/functions/runners.ts"
|
|
20
|
+
}, (opts) => rotateRunnerJoinToken.__executeServer(opts));
|
|
21
|
+
var rotateRunnerJoinToken = createServerFn({ method: "POST" }).handler(rotateRunnerJoinToken_createServerFn_handler, async () => await rotateJoinToken());
|
|
22
|
+
var removeRunner_createServerFn_handler = createServerRpc({
|
|
23
|
+
id: "611491b32d9583ccaae637ddf9fb2f842ff75994f50513ecd8273e305e889eb8",
|
|
24
|
+
name: "removeRunner",
|
|
25
|
+
filename: "src/server/functions/runners.ts"
|
|
26
|
+
}, (opts) => removeRunner.__executeServer(opts));
|
|
27
|
+
var removeRunner = createServerFn({ method: "POST" }).inputValidator((data) => data).handler(removeRunner_createServerFn_handler, async ({ data }) => {
|
|
28
|
+
await forgetRunner(data.id);
|
|
29
|
+
return await listRunners();
|
|
30
|
+
});
|
|
31
|
+
//#endregion
|
|
32
|
+
export { getRunnersPage_createServerFn_handler, removeRunner_createServerFn_handler, rotateRunnerJoinToken_createServerFn_handler };
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { U as require_jsx_runtime } from "../_libs/@base-ui/react+[...].mjs";
|
|
2
|
+
import { m as useRouter } from "../_libs/@tanstack/react-router+[...].mjs";
|
|
3
|
+
import { s as Server } from "../_libs/lucide-react.mjs";
|
|
4
|
+
import { n as toast } from "../_libs/sonner.mjs";
|
|
5
|
+
import { C as Button$1, I as Badge, L as Card, R as CardContent, f as useLiveRefresh, i as rotateRunnerJoinToken, n as Route$7, r as removeRunner, z as CardHeader } from "./router-3WmpE5b_.mjs";
|
|
6
|
+
import { n as AlertDescription, t as Alert } from "./alert-NEiOCTOl.mjs";
|
|
7
|
+
//#region node_modules/.nitro/vite/services/ssr/assets/runners-DI_NxpHC.js
|
|
8
|
+
var import_jsx_runtime = require_jsx_runtime();
|
|
9
|
+
function inventoryLines(runner) {
|
|
10
|
+
if (runner.inventory.length === 0) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
|
|
11
|
+
className: "mt-2 text-sm text-muted-foreground",
|
|
12
|
+
children: "No coding agents found yet."
|
|
13
|
+
});
|
|
14
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", {
|
|
15
|
+
className: "mt-2 text-sm text-muted-foreground",
|
|
16
|
+
children: runner.inventory.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { children: [entry.agentId, entry.signedIn ? " · signed in" : entry.installed ? " · not signed in" : " · missing"] }, entry.agentId))
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
function RunnersPage() {
|
|
20
|
+
const { runners, joinToken, origin } = Route$7.useLoaderData();
|
|
21
|
+
const router = useRouter();
|
|
22
|
+
useLiveRefresh();
|
|
23
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
24
|
+
className: "flex flex-col gap-8",
|
|
25
|
+
children: [
|
|
26
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("header", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h1", {
|
|
27
|
+
className: "text-2xl font-semibold tracking-tight",
|
|
28
|
+
children: "Runners"
|
|
29
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
|
|
30
|
+
className: "mt-1 text-sm text-muted-foreground",
|
|
31
|
+
children: "A runner is a process that runs agents. Join one on every host that should do work."
|
|
32
|
+
})] }),
|
|
33
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Card, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardHeader, {
|
|
34
|
+
className: "text-base font-medium",
|
|
35
|
+
children: "Join a runner"
|
|
36
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(CardContent, {
|
|
37
|
+
className: "flex flex-col gap-3 text-sm",
|
|
38
|
+
children: [
|
|
39
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", {
|
|
40
|
+
className: "text-muted-foreground",
|
|
41
|
+
children: [
|
|
42
|
+
"Install the CLI once (",
|
|
43
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", {
|
|
44
|
+
className: "text-xs",
|
|
45
|
+
children: "npm install -g loopable-cli"
|
|
46
|
+
}),
|
|
47
|
+
"), sign in a coding agent on that host, then start a runner with this token."
|
|
48
|
+
]
|
|
49
|
+
}),
|
|
50
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("pre", {
|
|
51
|
+
className: "overflow-x-auto rounded-md bg-muted p-3 text-xs",
|
|
52
|
+
children: `loopable runner --url ${origin} --token ${joinToken}`
|
|
53
|
+
}),
|
|
54
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$1, {
|
|
55
|
+
variant: "outline",
|
|
56
|
+
size: "sm",
|
|
57
|
+
onClick: async () => {
|
|
58
|
+
try {
|
|
59
|
+
await rotateRunnerJoinToken();
|
|
60
|
+
await router.invalidate();
|
|
61
|
+
toast.success("Join token rotated. Existing runners keep working.");
|
|
62
|
+
} catch (error) {
|
|
63
|
+
toast.error(error instanceof Error ? error.message : String(error));
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
children: "Rotate join token"
|
|
67
|
+
}) })
|
|
68
|
+
]
|
|
69
|
+
})] }),
|
|
70
|
+
runners.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Alert, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertDescription, { children: "No runner has joined yet. Loops will wait until one is online." }) }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
71
|
+
className: "flex flex-col gap-3",
|
|
72
|
+
children: runners.map((runner) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Card, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(CardContent, {
|
|
73
|
+
className: "flex items-start gap-4 pt-6",
|
|
74
|
+
children: [
|
|
75
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Server, { className: "mt-0.5 size-5 shrink-0 text-muted-foreground" }),
|
|
76
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
77
|
+
className: "min-w-0 flex-1",
|
|
78
|
+
children: [
|
|
79
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
80
|
+
className: "flex flex-wrap items-center gap-2",
|
|
81
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
|
|
82
|
+
className: "font-medium",
|
|
83
|
+
children: runner.name
|
|
84
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Badge, {
|
|
85
|
+
variant: runner.status === "online" ? "default" : "outline",
|
|
86
|
+
children: runner.status
|
|
87
|
+
})]
|
|
88
|
+
}),
|
|
89
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
|
|
90
|
+
className: "mt-1 text-xs text-muted-foreground",
|
|
91
|
+
children: runner.hostname
|
|
92
|
+
}),
|
|
93
|
+
inventoryLines(runner)
|
|
94
|
+
]
|
|
95
|
+
}),
|
|
96
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$1, {
|
|
97
|
+
variant: "ghost",
|
|
98
|
+
size: "sm",
|
|
99
|
+
onClick: async () => {
|
|
100
|
+
try {
|
|
101
|
+
await removeRunner({ data: { id: runner.id } });
|
|
102
|
+
await router.invalidate();
|
|
103
|
+
} catch (error) {
|
|
104
|
+
toast.error(error instanceof Error ? error.message : String(error));
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
children: "Forget"
|
|
108
|
+
})
|
|
109
|
+
]
|
|
110
|
+
}) }, runner.id))
|
|
111
|
+
})
|
|
112
|
+
]
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
//#endregion
|
|
116
|
+
export { RunnersPage as component };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { f as eq } from "../_libs/drizzle-orm.mjs";
|
|
2
|
+
import { f as runners, g as tasks, s as db } from "./client-CKkOfh-s.mjs";
|
|
3
|
+
import { a as runnerTokenKey, o as secretStore, r as joinTokenKey } from "./secrets-Hqti-niS.mjs";
|
|
4
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
5
|
+
import { hostname } from "node:os";
|
|
6
|
+
//#region node_modules/.nitro/vite/services/ssr/assets/runners-DtXs4Q8a.js
|
|
7
|
+
var ONLINE_MS = 45e3;
|
|
8
|
+
function isRunnerOnline(row, now = Date.now()) {
|
|
9
|
+
if (row.status !== "online") return false;
|
|
10
|
+
if (!row.lastSeenAt) return false;
|
|
11
|
+
return now - row.lastSeenAt.getTime() < ONLINE_MS;
|
|
12
|
+
}
|
|
13
|
+
function toView(row, now = Date.now()) {
|
|
14
|
+
const online = isRunnerOnline(row, now);
|
|
15
|
+
return {
|
|
16
|
+
id: row.id,
|
|
17
|
+
name: row.name,
|
|
18
|
+
hostname: row.hostname,
|
|
19
|
+
status: online ? "online" : "offline",
|
|
20
|
+
inventory: row.inventory,
|
|
21
|
+
lastSeenAt: row.lastSeenAt?.toISOString() ?? null
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
async function listRunners() {
|
|
25
|
+
await db().delete(runners).where(eq(runners.id, "host")).run();
|
|
26
|
+
const now = Date.now();
|
|
27
|
+
return (await db().select().from(runners).all()).map((row) => toView(row, now));
|
|
28
|
+
}
|
|
29
|
+
async function getRunner(id) {
|
|
30
|
+
return await db().select().from(runners).where(eq(runners.id, id)).get();
|
|
31
|
+
}
|
|
32
|
+
async function onlineRunnerCount() {
|
|
33
|
+
return (await listRunners()).filter((row) => row.status === "online").length;
|
|
34
|
+
}
|
|
35
|
+
/** Agent ids signed in on an online runner. */
|
|
36
|
+
async function availableAgentIds(requiresHost = false) {
|
|
37
|
+
const local = hostname();
|
|
38
|
+
return [...new Set((await listRunners()).filter((row) => row.status === "online" && (!requiresHost || row.hostname === local)).flatMap((row) => row.inventory.filter((entry) => entry.installed && entry.signedIn).map((entry) => entry.agentId)))];
|
|
39
|
+
}
|
|
40
|
+
async function getJoinToken() {
|
|
41
|
+
const existing = await secretStore().get(joinTokenKey());
|
|
42
|
+
if (existing) return existing;
|
|
43
|
+
const token = randomBytes(24).toString("base64url");
|
|
44
|
+
await secretStore().set(joinTokenKey(), token);
|
|
45
|
+
return token;
|
|
46
|
+
}
|
|
47
|
+
async function rotateJoinToken() {
|
|
48
|
+
const token = randomBytes(24).toString("base64url");
|
|
49
|
+
await secretStore().set(joinTokenKey(), token);
|
|
50
|
+
return token;
|
|
51
|
+
}
|
|
52
|
+
async function heartbeatRunner(runnerId, inventory) {
|
|
53
|
+
if (!await getRunner(runnerId)) throw new Error("Unknown runner.");
|
|
54
|
+
await db().update(runners).set({
|
|
55
|
+
status: "online",
|
|
56
|
+
inventory,
|
|
57
|
+
lastSeenAt: /* @__PURE__ */ new Date()
|
|
58
|
+
}).where(eq(runners.id, runnerId)).run();
|
|
59
|
+
return toView(await getRunner(runnerId));
|
|
60
|
+
}
|
|
61
|
+
async function joinRunner(input) {
|
|
62
|
+
const expected = await getJoinToken();
|
|
63
|
+
if (!input.joinToken || input.joinToken !== expected) throw new Error("That join token is not valid.");
|
|
64
|
+
const existing = (await db().select().from(runners).all()).find((row) => row.hostname === input.hostname);
|
|
65
|
+
const id = existing?.id ?? randomUUID();
|
|
66
|
+
const name = existing?.name ?? input.hostname;
|
|
67
|
+
const now = /* @__PURE__ */ new Date();
|
|
68
|
+
if (existing) await db().update(runners).set({
|
|
69
|
+
status: "online",
|
|
70
|
+
inventory: input.inventory,
|
|
71
|
+
lastSeenAt: now,
|
|
72
|
+
hostname: input.hostname
|
|
73
|
+
}).where(eq(runners.id, id)).run();
|
|
74
|
+
else await db().insert(runners).values({
|
|
75
|
+
id,
|
|
76
|
+
name,
|
|
77
|
+
hostname: input.hostname,
|
|
78
|
+
status: "online",
|
|
79
|
+
inventory: input.inventory,
|
|
80
|
+
lastSeenAt: now
|
|
81
|
+
}).run();
|
|
82
|
+
const token = randomBytes(24).toString("base64url");
|
|
83
|
+
await secretStore().set(runnerTokenKey(id), token);
|
|
84
|
+
return {
|
|
85
|
+
runnerId: id,
|
|
86
|
+
runnerToken: token,
|
|
87
|
+
name
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
async function runnerIdForToken(token) {
|
|
91
|
+
if (!token) return null;
|
|
92
|
+
for (const row of await db().select().from(runners).all()) {
|
|
93
|
+
const stored = await secretStore().get(runnerTokenKey(row.id)) ?? await secretStore().get(`runners.machine.${row.id}`);
|
|
94
|
+
if (stored && stored === token) return row.id;
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
async function forgetRunner(id) {
|
|
99
|
+
if (!await getRunner(id)) throw new Error("Unknown runner.");
|
|
100
|
+
if ((await db().select().from(tasks).all()).some((task) => task.runnerId === id && task.state === "awaiting_agent")) throw new Error("This runner still has a run in flight.");
|
|
101
|
+
await db().delete(runners).where(eq(runners.id, id)).run();
|
|
102
|
+
await secretStore().delete(runnerTokenKey(id));
|
|
103
|
+
await secretStore().delete(`runners.machine.${id}`);
|
|
104
|
+
}
|
|
105
|
+
//#endregion
|
|
106
|
+
export { heartbeatRunner as a, listRunners as c, runnerIdForToken as d, getRunner as i, onlineRunnerCount as l, forgetRunner as n, isRunnerOnline as o, getJoinToken as r, joinRunner as s, availableAgentIds as t, rotateJoinToken as u };
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { f as eq } from "../_libs/drizzle-orm.mjs";
|
|
2
|
+
import { m as secrets, o as dataDir, p as secretNamespace, s as db } from "./client-CKkOfh-s.mjs";
|
|
3
|
+
import { chmodSync, existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { execFileSync } from "node:child_process";
|
|
6
|
+
//#region node_modules/.nitro/vite/services/ssr/assets/secrets-Hqti-niS.js
|
|
7
|
+
/**
|
|
8
|
+
* Hosted Loopable shares a network database between the App Worker and the
|
|
9
|
+
* Dispatcher. A local laptop still uses a file, and needs none of this.
|
|
10
|
+
*/
|
|
11
|
+
function isHosted() {
|
|
12
|
+
const url = process.env.LOOPABLE_DATABASE_URL ?? "";
|
|
13
|
+
return url.startsWith("libsql:") || url.startsWith("https:") || url.startsWith("wss:");
|
|
14
|
+
}
|
|
15
|
+
function account() {
|
|
16
|
+
return process.env.USER ?? "loopable";
|
|
17
|
+
}
|
|
18
|
+
function service(key) {
|
|
19
|
+
return `${secretNamespace()}.${key}`;
|
|
20
|
+
}
|
|
21
|
+
var keychainStore = {
|
|
22
|
+
async get(key) {
|
|
23
|
+
try {
|
|
24
|
+
return execFileSync("security", [
|
|
25
|
+
"find-generic-password",
|
|
26
|
+
"-a",
|
|
27
|
+
account(),
|
|
28
|
+
"-s",
|
|
29
|
+
service(key),
|
|
30
|
+
"-w"
|
|
31
|
+
], {
|
|
32
|
+
encoding: "utf8",
|
|
33
|
+
stdio: [
|
|
34
|
+
"ignore",
|
|
35
|
+
"pipe",
|
|
36
|
+
"ignore"
|
|
37
|
+
]
|
|
38
|
+
}).trim() || null;
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
async set(key, value) {
|
|
44
|
+
try {
|
|
45
|
+
execFileSync("security", [
|
|
46
|
+
"delete-generic-password",
|
|
47
|
+
"-a",
|
|
48
|
+
account(),
|
|
49
|
+
"-s",
|
|
50
|
+
service(key)
|
|
51
|
+
], { stdio: "ignore" });
|
|
52
|
+
} catch {}
|
|
53
|
+
execFileSync("security", [
|
|
54
|
+
"add-generic-password",
|
|
55
|
+
"-a",
|
|
56
|
+
account(),
|
|
57
|
+
"-s",
|
|
58
|
+
service(key),
|
|
59
|
+
"-w",
|
|
60
|
+
value
|
|
61
|
+
], { stdio: "ignore" });
|
|
62
|
+
},
|
|
63
|
+
async delete(key) {
|
|
64
|
+
try {
|
|
65
|
+
execFileSync("security", [
|
|
66
|
+
"delete-generic-password",
|
|
67
|
+
"-a",
|
|
68
|
+
account(),
|
|
69
|
+
"-s",
|
|
70
|
+
service(key)
|
|
71
|
+
], { stdio: "ignore" });
|
|
72
|
+
} catch {}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
function secretFile(key) {
|
|
76
|
+
return join(dataDir(), `secret-${key.replace(/[^a-zA-Z0-9._-]/g, "_")}.json`);
|
|
77
|
+
}
|
|
78
|
+
var fileStore = {
|
|
79
|
+
async get(key) {
|
|
80
|
+
const path = secretFile(key);
|
|
81
|
+
if (!existsSync(path)) return null;
|
|
82
|
+
try {
|
|
83
|
+
return readFileSync(path, "utf8");
|
|
84
|
+
} catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
async set(key, value) {
|
|
89
|
+
const path = secretFile(key);
|
|
90
|
+
writeFileSync(path, value, {
|
|
91
|
+
encoding: "utf8",
|
|
92
|
+
mode: 384
|
|
93
|
+
});
|
|
94
|
+
chmodSync(path, 384);
|
|
95
|
+
},
|
|
96
|
+
async delete(key) {
|
|
97
|
+
const path = secretFile(key);
|
|
98
|
+
if (existsSync(path)) rmSync(path);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
function masterKey() {
|
|
102
|
+
const key = process.env.LOOPABLE_MASTER_KEY?.trim();
|
|
103
|
+
if (!key) throw new Error("Set LOOPABLE_MASTER_KEY so App and Dispatcher can share credentials.");
|
|
104
|
+
return key;
|
|
105
|
+
}
|
|
106
|
+
async function aesKey() {
|
|
107
|
+
const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(masterKey()));
|
|
108
|
+
return crypto.subtle.importKey("raw", hash, "AES-GCM", false, ["encrypt", "decrypt"]);
|
|
109
|
+
}
|
|
110
|
+
async function encrypt(plain) {
|
|
111
|
+
const key = await aesKey();
|
|
112
|
+
const iv = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(12));
|
|
113
|
+
const sealed = await crypto.subtle.encrypt({
|
|
114
|
+
name: "AES-GCM",
|
|
115
|
+
iv
|
|
116
|
+
}, key, new TextEncoder().encode(plain));
|
|
117
|
+
const packed = new Uint8Array(iv.length + sealed.byteLength);
|
|
118
|
+
packed.set(iv);
|
|
119
|
+
packed.set(new Uint8Array(sealed), iv.length);
|
|
120
|
+
return bytesToBase64(packed);
|
|
121
|
+
}
|
|
122
|
+
async function decrypt(blob) {
|
|
123
|
+
const key = await aesKey();
|
|
124
|
+
const packed = base64ToBytes(blob);
|
|
125
|
+
const iv = packed.slice(0, 12);
|
|
126
|
+
const data = packed.slice(12);
|
|
127
|
+
const opened = await crypto.subtle.decrypt({
|
|
128
|
+
name: "AES-GCM",
|
|
129
|
+
iv
|
|
130
|
+
}, key, data);
|
|
131
|
+
return new TextDecoder().decode(opened);
|
|
132
|
+
}
|
|
133
|
+
function bytesToBase64(bytes) {
|
|
134
|
+
let binary = "";
|
|
135
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
136
|
+
return btoa(binary);
|
|
137
|
+
}
|
|
138
|
+
function base64ToBytes(blob) {
|
|
139
|
+
const binary = atob(blob);
|
|
140
|
+
const bytes = new Uint8Array(binary.length);
|
|
141
|
+
for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
|
|
142
|
+
return bytes;
|
|
143
|
+
}
|
|
144
|
+
var tableStore = {
|
|
145
|
+
async get(key) {
|
|
146
|
+
const row = await db().select().from(secrets).where(eq(secrets.key, key)).get();
|
|
147
|
+
if (!row) return null;
|
|
148
|
+
return decrypt(row.value);
|
|
149
|
+
},
|
|
150
|
+
async set(key, value) {
|
|
151
|
+
const stored = await encrypt(value);
|
|
152
|
+
const now = /* @__PURE__ */ new Date();
|
|
153
|
+
if (await db().select().from(secrets).where(eq(secrets.key, key)).get()) {
|
|
154
|
+
await db().update(secrets).set({
|
|
155
|
+
value: stored,
|
|
156
|
+
updatedAt: now
|
|
157
|
+
}).where(eq(secrets.key, key)).run();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
await db().insert(secrets).values({
|
|
161
|
+
key,
|
|
162
|
+
value: stored,
|
|
163
|
+
updatedAt: now
|
|
164
|
+
}).run();
|
|
165
|
+
},
|
|
166
|
+
async delete(key) {
|
|
167
|
+
await db().delete(secrets).where(eq(secrets.key, key)).run();
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
function secretStore() {
|
|
171
|
+
if (isHosted() || process.env.LOOPABLE_MASTER_KEY) return tableStore;
|
|
172
|
+
return process.platform === "darwin" && process.env.LOOPABLE_KEYCHAIN !== "0" ? keychainStore : fileStore;
|
|
173
|
+
}
|
|
174
|
+
function connectionSecretKey(connectionId) {
|
|
175
|
+
return `connection.${connectionId}`;
|
|
176
|
+
}
|
|
177
|
+
async function readCredential(connectionId) {
|
|
178
|
+
const raw = await secretStore().get(connectionSecretKey(connectionId));
|
|
179
|
+
if (!raw) return null;
|
|
180
|
+
try {
|
|
181
|
+
return JSON.parse(raw);
|
|
182
|
+
} catch {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async function writeCredential(connectionId, credential) {
|
|
187
|
+
await secretStore().set(connectionSecretKey(connectionId), JSON.stringify(credential));
|
|
188
|
+
}
|
|
189
|
+
async function deleteCredential(connectionId) {
|
|
190
|
+
await secretStore().delete(connectionSecretKey(connectionId));
|
|
191
|
+
}
|
|
192
|
+
function joinTokenKey() {
|
|
193
|
+
return "runners.join";
|
|
194
|
+
}
|
|
195
|
+
function runnerTokenKey(runnerId) {
|
|
196
|
+
return `runners.token.${runnerId}`;
|
|
197
|
+
}
|
|
198
|
+
//#endregion
|
|
199
|
+
export { runnerTokenKey as a, readCredential as i, isHosted as n, secretStore as o, joinTokenKey as r, writeCredential as s, deleteCredential as t };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import "../_runtime.mjs";
|
|
2
|
+
import { U as require_jsx_runtime, W as require_react } from "../_libs/@base-ui/react+[...].mjs";
|
|
3
|
+
import { S as Input$1, V as cn, _ as SelectItem, b as Label, g as SelectContent, h as Select$1, v as SelectTrigger, y as SelectValue } from "./router-3WmpE5b_.mjs";
|
|
4
|
+
import { t as Switch$1 } from "./switch-ReNglgH6.mjs";
|
|
5
|
+
require_react();
|
|
6
|
+
var import_jsx_runtime = require_jsx_runtime();
|
|
7
|
+
function Textarea({ className, ...props }) {
|
|
8
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("textarea", {
|
|
9
|
+
"data-slot": "textarea",
|
|
10
|
+
className: cn("flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40", className),
|
|
11
|
+
...props
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* One renderer for anything a connector declares as a field, so connection
|
|
16
|
+
* settings and loop conditions can never drift apart visually.
|
|
17
|
+
*/
|
|
18
|
+
function initialFieldValues(fields, stored) {
|
|
19
|
+
const values = {};
|
|
20
|
+
for (const field of fields) {
|
|
21
|
+
const current = stored[field.key];
|
|
22
|
+
switch (field.kind) {
|
|
23
|
+
case "boolean":
|
|
24
|
+
values[field.key] = typeof current === "boolean" ? current : field.default;
|
|
25
|
+
break;
|
|
26
|
+
case "string_list":
|
|
27
|
+
values[field.key] = Array.isArray(current) ? current : [];
|
|
28
|
+
break;
|
|
29
|
+
case "select":
|
|
30
|
+
values[field.key] = typeof current === "string" ? current : field.default;
|
|
31
|
+
break;
|
|
32
|
+
default: values[field.key] = typeof current === "string" ? current : "";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return values;
|
|
36
|
+
}
|
|
37
|
+
function SettingFieldInputs({ fields, values, onChange, idPrefix = "" }) {
|
|
38
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: fields.map((field) => {
|
|
39
|
+
const id = `${idPrefix}${field.key}`;
|
|
40
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
41
|
+
className: "flex flex-col gap-2",
|
|
42
|
+
children: field.kind === "boolean" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
43
|
+
className: "flex items-center justify-between gap-4",
|
|
44
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label, {
|
|
45
|
+
htmlFor: id,
|
|
46
|
+
children: field.label
|
|
47
|
+
}), field.help ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
|
|
48
|
+
className: "mt-1 text-xs text-muted-foreground",
|
|
49
|
+
children: field.help
|
|
50
|
+
}) : null] }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Switch$1, {
|
|
51
|
+
id,
|
|
52
|
+
checked: Boolean(values[field.key]),
|
|
53
|
+
onCheckedChange: (checked) => onChange(field.key, checked)
|
|
54
|
+
})]
|
|
55
|
+
}) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
56
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label, {
|
|
57
|
+
htmlFor: id,
|
|
58
|
+
children: field.label
|
|
59
|
+
}),
|
|
60
|
+
field.help ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
|
|
61
|
+
className: "text-xs text-muted-foreground",
|
|
62
|
+
children: field.help
|
|
63
|
+
}) : null,
|
|
64
|
+
field.kind === "string_list" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Textarea, {
|
|
65
|
+
id,
|
|
66
|
+
rows: 4,
|
|
67
|
+
placeholder: field.placeholder,
|
|
68
|
+
value: (values[field.key] ?? []).join("\n"),
|
|
69
|
+
onChange: (event) => onChange(field.key, event.target.value.split("\n").map((line) => line.trim()).filter(Boolean))
|
|
70
|
+
}) : field.kind === "select" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Select$1, {
|
|
71
|
+
value: values[field.key],
|
|
72
|
+
onValueChange: (value) => onChange(field.key, value),
|
|
73
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectTrigger, {
|
|
74
|
+
id,
|
|
75
|
+
className: "w-full",
|
|
76
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectValue, { children: (value) => field.options.find((option) => option.value === value)?.label ?? value })
|
|
77
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectContent, { children: field.options.map((option) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectItem, {
|
|
78
|
+
value: option.value,
|
|
79
|
+
children: option.label
|
|
80
|
+
}, option.value)) })]
|
|
81
|
+
}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input$1, {
|
|
82
|
+
id,
|
|
83
|
+
placeholder: field.placeholder,
|
|
84
|
+
value: values[field.key],
|
|
85
|
+
onChange: (event) => onChange(field.key, event.target.value)
|
|
86
|
+
})
|
|
87
|
+
] })
|
|
88
|
+
}, field.key);
|
|
89
|
+
}) });
|
|
90
|
+
}
|
|
91
|
+
//#endregion
|
|
92
|
+
export { Textarea as n, initialFieldValues as r, SettingFieldInputs as t };
|