clawdlets 0.2.4 → 0.3.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.
Files changed (113) hide show
  1. package/dist/main.mjs +556 -1004
  2. package/node_modules/@clawdlets/core/dist/lib/context.d.ts +21 -0
  3. package/node_modules/@clawdlets/core/dist/lib/context.d.ts.map +1 -0
  4. package/node_modules/@clawdlets/core/dist/lib/context.js +19 -0
  5. package/node_modules/@clawdlets/core/dist/lib/context.js.map +1 -0
  6. package/node_modules/@clawdlets/core/dist/lib/host-resolve.d.ts +6 -0
  7. package/node_modules/@clawdlets/core/dist/lib/host-resolve.d.ts.map +1 -0
  8. package/node_modules/@clawdlets/core/dist/lib/host-resolve.js +20 -0
  9. package/node_modules/@clawdlets/core/dist/lib/host-resolve.js.map +1 -0
  10. package/node_modules/@clawdlets/core/dist/repo-layout.d.ts +1 -0
  11. package/node_modules/@clawdlets/core/dist/repo-layout.d.ts.map +1 -1
  12. package/node_modules/@clawdlets/core/dist/repo-layout.js +2 -0
  13. package/node_modules/@clawdlets/core/dist/repo-layout.js.map +1 -1
  14. package/node_modules/@clawdlets/core/package.json +1 -3
  15. package/package.json +3 -6
  16. package/node_modules/@clawdlets/clf-queue/dist/client.d.ts +0 -21
  17. package/node_modules/@clawdlets/clf-queue/dist/client.d.ts.map +0 -1
  18. package/node_modules/@clawdlets/clf-queue/dist/client.js +0 -132
  19. package/node_modules/@clawdlets/clf-queue/dist/client.js.map +0 -1
  20. package/node_modules/@clawdlets/clf-queue/dist/index.d.ts +0 -9
  21. package/node_modules/@clawdlets/clf-queue/dist/index.d.ts.map +0 -1
  22. package/node_modules/@clawdlets/clf-queue/dist/index.js +0 -5
  23. package/node_modules/@clawdlets/clf-queue/dist/index.js.map +0 -1
  24. package/node_modules/@clawdlets/clf-queue/dist/jobs.d.ts +0 -32
  25. package/node_modules/@clawdlets/clf-queue/dist/jobs.d.ts.map +0 -1
  26. package/node_modules/@clawdlets/clf-queue/dist/jobs.js +0 -24
  27. package/node_modules/@clawdlets/clf-queue/dist/jobs.js.map +0 -1
  28. package/node_modules/@clawdlets/clf-queue/dist/protocol.d.ts +0 -118
  29. package/node_modules/@clawdlets/clf-queue/dist/protocol.d.ts.map +0 -1
  30. package/node_modules/@clawdlets/clf-queue/dist/protocol.js +0 -46
  31. package/node_modules/@clawdlets/clf-queue/dist/protocol.js.map +0 -1
  32. package/node_modules/@clawdlets/clf-queue/dist/queue/bootstrap-tokens.d.ts +0 -3
  33. package/node_modules/@clawdlets/clf-queue/dist/queue/bootstrap-tokens.d.ts.map +0 -1
  34. package/node_modules/@clawdlets/clf-queue/dist/queue/bootstrap-tokens.js +0 -112
  35. package/node_modules/@clawdlets/clf-queue/dist/queue/bootstrap-tokens.js.map +0 -1
  36. package/node_modules/@clawdlets/clf-queue/dist/queue/jobs.d.ts +0 -3
  37. package/node_modules/@clawdlets/clf-queue/dist/queue/jobs.d.ts.map +0 -1
  38. package/node_modules/@clawdlets/clf-queue/dist/queue/jobs.js +0 -313
  39. package/node_modules/@clawdlets/clf-queue/dist/queue/jobs.js.map +0 -1
  40. package/node_modules/@clawdlets/clf-queue/dist/queue/migrate.d.ts +0 -2
  41. package/node_modules/@clawdlets/clf-queue/dist/queue/migrate.d.ts.map +0 -1
  42. package/node_modules/@clawdlets/clf-queue/dist/queue/migrate.js +0 -74
  43. package/node_modules/@clawdlets/clf-queue/dist/queue/migrate.js.map +0 -1
  44. package/node_modules/@clawdlets/clf-queue/dist/queue/open.d.ts +0 -3
  45. package/node_modules/@clawdlets/clf-queue/dist/queue/open.d.ts.map +0 -1
  46. package/node_modules/@clawdlets/clf-queue/dist/queue/open.js +0 -27
  47. package/node_modules/@clawdlets/clf-queue/dist/queue/open.js.map +0 -1
  48. package/node_modules/@clawdlets/clf-queue/dist/queue/types.d.ts +0 -113
  49. package/node_modules/@clawdlets/clf-queue/dist/queue/types.d.ts.map +0 -1
  50. package/node_modules/@clawdlets/clf-queue/dist/queue/types.js +0 -2
  51. package/node_modules/@clawdlets/clf-queue/dist/queue/types.js.map +0 -1
  52. package/node_modules/@clawdlets/clf-queue/dist/queue/util.d.ts +0 -10
  53. package/node_modules/@clawdlets/clf-queue/dist/queue/util.d.ts.map +0 -1
  54. package/node_modules/@clawdlets/clf-queue/dist/queue/util.js +0 -30
  55. package/node_modules/@clawdlets/clf-queue/dist/queue/util.js.map +0 -1
  56. package/node_modules/@clawdlets/clf-queue/dist/queue.d.ts +0 -3
  57. package/node_modules/@clawdlets/clf-queue/dist/queue.d.ts.map +0 -1
  58. package/node_modules/@clawdlets/clf-queue/dist/queue.js +0 -2
  59. package/node_modules/@clawdlets/clf-queue/dist/queue.js.map +0 -1
  60. package/node_modules/@clawdlets/clf-queue/package.json +0 -36
  61. package/node_modules/@clawdlets/core/dist/lib/cattle-state.d.ts +0 -25
  62. package/node_modules/@clawdlets/core/dist/lib/cattle-state.d.ts.map +0 -1
  63. package/node_modules/@clawdlets/core/dist/lib/cattle-state.js +0 -136
  64. package/node_modules/@clawdlets/core/dist/lib/cattle-state.js.map +0 -1
  65. package/node_modules/better-sqlite3/LICENSE +0 -21
  66. package/node_modules/better-sqlite3/README.md +0 -99
  67. package/node_modules/better-sqlite3/binding.gyp +0 -38
  68. package/node_modules/better-sqlite3/deps/common.gypi +0 -68
  69. package/node_modules/better-sqlite3/deps/copy.js +0 -31
  70. package/node_modules/better-sqlite3/deps/defines.gypi +0 -41
  71. package/node_modules/better-sqlite3/deps/download.sh +0 -122
  72. package/node_modules/better-sqlite3/deps/patches/1208.patch +0 -15
  73. package/node_modules/better-sqlite3/deps/sqlite3/sqlite3.c +0 -265969
  74. package/node_modules/better-sqlite3/deps/sqlite3/sqlite3.h +0 -13968
  75. package/node_modules/better-sqlite3/deps/sqlite3/sqlite3ext.h +0 -730
  76. package/node_modules/better-sqlite3/deps/sqlite3.gyp +0 -80
  77. package/node_modules/better-sqlite3/deps/test_extension.c +0 -21
  78. package/node_modules/better-sqlite3/lib/database.js +0 -90
  79. package/node_modules/better-sqlite3/lib/index.js +0 -3
  80. package/node_modules/better-sqlite3/lib/methods/aggregate.js +0 -43
  81. package/node_modules/better-sqlite3/lib/methods/backup.js +0 -67
  82. package/node_modules/better-sqlite3/lib/methods/function.js +0 -31
  83. package/node_modules/better-sqlite3/lib/methods/inspect.js +0 -7
  84. package/node_modules/better-sqlite3/lib/methods/pragma.js +0 -12
  85. package/node_modules/better-sqlite3/lib/methods/serialize.js +0 -16
  86. package/node_modules/better-sqlite3/lib/methods/table.js +0 -189
  87. package/node_modules/better-sqlite3/lib/methods/transaction.js +0 -78
  88. package/node_modules/better-sqlite3/lib/methods/wrappers.js +0 -54
  89. package/node_modules/better-sqlite3/lib/sqlite-error.js +0 -20
  90. package/node_modules/better-sqlite3/lib/util.js +0 -12
  91. package/node_modules/better-sqlite3/package.json +0 -59
  92. package/node_modules/better-sqlite3/src/addon.cpp +0 -47
  93. package/node_modules/better-sqlite3/src/better_sqlite3.cpp +0 -74
  94. package/node_modules/better-sqlite3/src/objects/backup.cpp +0 -120
  95. package/node_modules/better-sqlite3/src/objects/backup.hpp +0 -36
  96. package/node_modules/better-sqlite3/src/objects/database.cpp +0 -417
  97. package/node_modules/better-sqlite3/src/objects/database.hpp +0 -103
  98. package/node_modules/better-sqlite3/src/objects/statement-iterator.cpp +0 -113
  99. package/node_modules/better-sqlite3/src/objects/statement-iterator.hpp +0 -50
  100. package/node_modules/better-sqlite3/src/objects/statement.cpp +0 -383
  101. package/node_modules/better-sqlite3/src/objects/statement.hpp +0 -58
  102. package/node_modules/better-sqlite3/src/util/bind-map.cpp +0 -73
  103. package/node_modules/better-sqlite3/src/util/binder.cpp +0 -193
  104. package/node_modules/better-sqlite3/src/util/constants.cpp +0 -172
  105. package/node_modules/better-sqlite3/src/util/custom-aggregate.cpp +0 -121
  106. package/node_modules/better-sqlite3/src/util/custom-function.cpp +0 -59
  107. package/node_modules/better-sqlite3/src/util/custom-table.cpp +0 -409
  108. package/node_modules/better-sqlite3/src/util/data-converter.cpp +0 -17
  109. package/node_modules/better-sqlite3/src/util/data.cpp +0 -194
  110. package/node_modules/better-sqlite3/src/util/helpers.cpp +0 -109
  111. package/node_modules/better-sqlite3/src/util/macros.cpp +0 -70
  112. package/node_modules/better-sqlite3/src/util/query-macros.cpp +0 -71
  113. package/node_modules/better-sqlite3/src/util/row-builder.cpp +0 -49
package/dist/main.mjs CHANGED
@@ -17,21 +17,16 @@ import { withFlakesEnv } from "@clawdlets/core/lib/nix-flakes";
17
17
  import { resolveBaseFlake } from "@clawdlets/core/lib/base-flake";
18
18
  import { getHostEncryptedAgeKeyFile, getHostExtraFilesDir, getHostExtraFilesKeyPath, getHostExtraFilesSecretsDir, getHostOpenTofuDir, getHostRemoteSecretsDir, getHostSecretsDir, getLocalOperatorAgeKeyPath, getRepoLayout } from "@clawdlets/core/repo-layout";
19
19
  import { collectDoctorChecks } from "@clawdlets/core/doctor";
20
+ import { resolveHostNameOrExit } from "@clawdlets/core/lib/host-resolve";
20
21
  import { ensureDir, writeFileAtomic } from "@clawdlets/core/lib/fs-safe";
21
22
  import { splitDotPath } from "@clawdlets/core/lib/dot-path";
22
- import { safeCattleLabelValue } from "@clawdlets/core/lib/cattle-planner";
23
- import { openCattleState } from "@clawdlets/core/lib/cattle-state";
24
- import { CATTLE_LABEL_PERSONA, buildCattleLabelSelector, destroyCattleServer, listCattleServers, reapExpiredCattle } from "@clawdlets/core/lib/hcloud-cattle";
25
- import { parseTtlToSeconds } from "@clawdlets/core/lib/ttl";
26
- import { CATTLE_TASK_SCHEMA_VERSION, CattleTaskSchema } from "@clawdlets/core/lib/cattle-task";
27
- import { shellQuote, sshCapture, sshRun, validateTargetHost } from "@clawdlets/core/lib/ssh-remote";
28
- import { PersonaNameSchema, sanitizeOperatorId } from "@clawdlets/core/lib/identifiers";
29
- import { CLF_PROTOCOL_VERSION, createClfClient } from "@clawdlets/clf-queue";
30
23
  import { formatDotenvValue, parseDotenv } from "@clawdlets/core/lib/dotenv-file";
31
24
  import { looksLikeSshPrivateKey, parseSshPublicKeysFromText } from "@clawdlets/core/lib/ssh";
25
+ import { shellQuote, sshCapture, sshRun, validateTargetHost } from "@clawdlets/core/lib/ssh-remote";
26
+ import { loadHostContextOrExit } from "@clawdlets/core/lib/context";
32
27
  import { tmpdir } from "node:os";
33
28
  import { downloadTemplate } from "giget";
34
- import { fileURLToPath } from "node:url";
29
+ import { fileURLToPath, pathToFileURL } from "node:url";
35
30
  import { normalizeTemplateSource } from "@clawdlets/core/lib/template-source";
36
31
  import { ageKeygen } from "@clawdlets/core/lib/age-keygen";
37
32
  import { parseAgeKeyFile } from "@clawdlets/core/lib/age";
@@ -39,11 +34,29 @@ import { mkpasswdYescryptHash } from "@clawdlets/core/lib/mkpasswd";
39
34
  import { upsertSopsCreationRule } from "@clawdlets/core/lib/sops-config";
40
35
  import { sopsDecryptYamlFile, sopsEncryptYamlToFile } from "@clawdlets/core/lib/sops";
41
36
  import { getHostAgeKeySopsCreationRulePathRegex, getHostSecretsSopsCreationRulePathRegex } from "@clawdlets/core/lib/sops-rules";
37
+ import { sanitizeOperatorId } from "@clawdlets/core/lib/identifiers";
42
38
  import { buildSecretsInitTemplate, isPlaceholderSecretValue, listSecretsInitPlaceholders, parseSecretsInitJson, resolveSecretsInitFromJsonArg, validateSecretsInitNonInteractive } from "@clawdlets/core/lib/secrets-init";
43
39
  import { readYamlScalarFromMapping } from "@clawdlets/core/lib/yaml-scalar";
44
40
  import { createSecretsTar } from "@clawdlets/core/lib/secrets-tar";
45
41
  import YAML from "yaml";
46
42
 
43
+ //#region rolldown:runtime
44
+ var __defProp = Object.defineProperty;
45
+ var __exportAll = (all, symbols) => {
46
+ let target = {};
47
+ for (var name in all) {
48
+ __defProp(target, name, {
49
+ get: all[name],
50
+ enumerable: true
51
+ });
52
+ }
53
+ if (symbols) {
54
+ __defProp(target, Symbol.toStringTag, { value: "Module" });
55
+ }
56
+ return target;
57
+ };
58
+
59
+ //#endregion
47
60
  //#region src/lib/wizard.ts
48
61
  const NAV_BACK = Symbol("clawdlets.nav.back");
49
62
  const NAV_EXIT = Symbol("clawdlets.nav.exit");
@@ -77,7 +90,7 @@ function validateBotId(value) {
77
90
  if (!v) return "bot id required";
78
91
  if (!/^[a-z][a-z0-9_-]*$/.test(v)) return "use: [a-z][a-z0-9_-]*";
79
92
  }
80
- const list = defineCommand({
93
+ const list$1 = defineCommand({
81
94
  meta: {
82
95
  name: "list",
83
96
  description: "List bots (from fleet/clawdlets.json)."
@@ -88,7 +101,7 @@ const list = defineCommand({
88
101
  console.log((config$1.fleet.botOrder || []).join("\n"));
89
102
  }
90
103
  });
91
- const add$1 = defineCommand({
104
+ const add$2 = defineCommand({
92
105
  meta: {
93
106
  name: "add",
94
107
  description: "Add a bot id to fleet/clawdlets.json."
@@ -150,7 +163,7 @@ const add$1 = defineCommand({
150
163
  console.log(`ok: added bot ${botId}`);
151
164
  }
152
165
  });
153
- const rm = defineCommand({
166
+ const rm$1 = defineCommand({
154
167
  meta: {
155
168
  name: "rm",
156
169
  description: "Remove a bot id from fleet/clawdlets.json."
@@ -189,9 +202,9 @@ const bot = defineCommand({
189
202
  description: "Manage fleet bots."
190
203
  },
191
204
  subCommands: {
192
- add: add$1,
193
- list,
194
- rm
205
+ add: add$2,
206
+ list: list$1,
207
+ rm: rm$1
195
208
  }
196
209
  });
197
210
 
@@ -369,41 +382,18 @@ async function requireDeployGate(params) {
369
382
  }));
370
383
  }
371
384
 
372
- //#endregion
373
- //#region src/lib/host-resolve.ts
374
- function printHostTips(lines) {
375
- for (const l of lines) console.error(`tip: ${l}`);
376
- }
377
- function resolveHostNameOrExit(params) {
378
- const { config: config$1 } = loadClawdletsConfig({
379
- repoRoot: findRepoRoot(params.cwd),
380
- runtimeDir: params.runtimeDir
381
- });
382
- const resolved = resolveHostName({
383
- config: config$1,
384
- host: params.hostArg
385
- });
386
- if (!resolved.ok) {
387
- console.error(`warn: ${resolved.message}`);
388
- printHostTips(resolved.tips);
389
- process$1.exitCode = 1;
390
- return null;
391
- }
392
- return resolved.host;
393
- }
394
-
395
385
  //#endregion
396
386
  //#region src/commands/bootstrap.ts
397
387
  async function purgeKnownHosts(ipv4, opts) {
398
- const rm$1 = async (host$1) => {
388
+ const rm$2 = async (host$1) => {
399
389
  if (opts.dryRun) {
400
390
  console.log(`ssh-keygen -R ${host$1}`);
401
391
  return;
402
392
  }
403
393
  await run("ssh-keygen", ["-R", host$1]);
404
394
  };
405
- await rm$1(ipv4);
406
- await rm$1(`[${ipv4}]:22`);
395
+ await rm$2(ipv4);
396
+ await rm$2(`[${ipv4}]:22`);
407
397
  }
408
398
  function resolveHostFromFlake(flakeBase) {
409
399
  const hashIndex = flakeBase.indexOf("#");
@@ -813,960 +803,96 @@ const validate = defineCommand({
813
803
  });
814
804
  const get = defineCommand({
815
805
  meta: {
816
- name: "get",
817
- description: "Get a value from fleet/clawdlets.json (dot path)."
818
- },
819
- args: {
820
- path: {
821
- type: "string",
822
- description: "Dot path (e.g. fleet.botOrder)."
823
- },
824
- json: {
825
- type: "boolean",
826
- description: "JSON output.",
827
- default: false
828
- }
829
- },
830
- async run({ args }) {
831
- const { config } = loadClawdletsConfig({ repoRoot: findRepoRoot(process$1.cwd()) });
832
- const parts = splitDotPath(String(args.path || ""));
833
- const v = getAtPath(config, parts);
834
- if (args.json) console.log(JSON.stringify({
835
- path: parts.join("."),
836
- value: v
837
- }, null, 2));
838
- else console.log(typeof v === "string" ? v : JSON.stringify(v, null, 2));
839
- }
840
- });
841
- const set$2 = defineCommand({
842
- meta: {
843
- name: "set",
844
- description: "Set a value in fleet/clawdlets.json (dot path)."
845
- },
846
- args: {
847
- path: {
848
- type: "string",
849
- description: "Dot path (e.g. fleet.botOrder)."
850
- },
851
- value: {
852
- type: "string",
853
- description: "String value."
854
- },
855
- "value-json": {
856
- type: "string",
857
- description: "JSON value (parsed)."
858
- },
859
- delete: {
860
- type: "boolean",
861
- description: "Delete the key at path.",
862
- default: false
863
- }
864
- },
865
- async run({ args }) {
866
- const { configPath, config } = loadClawdletsConfigRaw({ repoRoot: findRepoRoot(process$1.cwd()) });
867
- const parts = splitDotPath(String(args.path || ""));
868
- const next = structuredClone(config);
869
- if (args.delete) {
870
- if (!deleteAtPath(next, parts)) throw new Error(`path not found: ${parts.join(".")}`);
871
- } else if (args["value-json"] !== void 0) {
872
- let parsed;
873
- try {
874
- parsed = JSON.parse(String(args["value-json"]));
875
- } catch {
876
- throw new Error("invalid --value-json (must be valid JSON)");
877
- }
878
- setAtPath(next, parts, parsed);
879
- } else if (args.value !== void 0) setAtPath(next, parts, String(args.value));
880
- else throw new Error("set requires --value or --value-json (or --delete)");
881
- try {
882
- await writeClawdletsConfig({
883
- configPath,
884
- config: ClawdletsConfigSchema.parse(next)
885
- });
886
- console.log("ok");
887
- } catch (err) {
888
- let details = "";
889
- if (Array.isArray(err?.errors)) details = err.errors.map((e) => (Array.isArray(e.path) ? e.path.join(".") : "") || e.message).filter(Boolean).join(", ");
890
- const msg = details ? `config update failed; revert or fix validation errors: ${details}` : "config update failed; revert or fix validation errors";
891
- throw new Error(msg);
892
- }
893
- }
894
- });
895
- const config = defineCommand({
896
- meta: {
897
- name: "config",
898
- description: "Canonical config (fleet/clawdlets.json)."
899
- },
900
- subCommands: {
901
- init,
902
- show: show$1,
903
- validate,
904
- get,
905
- set: set$2
906
- }
907
- });
908
-
909
- //#endregion
910
- //#region src/lib/context.ts
911
- function loadRepoContext(params) {
912
- const repoRoot = findRepoRoot(params.cwd);
913
- const { layout, config: config$1 } = loadClawdletsConfig({
914
- repoRoot,
915
- runtimeDir: params.runtimeDir
916
- });
917
- return {
918
- repoRoot,
919
- layout,
920
- config: config$1
921
- };
922
- }
923
- function loadHostContextOrExit(params) {
924
- const hostName = resolveHostNameOrExit({
925
- cwd: params.cwd,
926
- runtimeDir: params.runtimeDir,
927
- hostArg: params.hostArg
928
- });
929
- if (!hostName) return null;
930
- const { repoRoot, layout, config: config$1 } = loadRepoContext({
931
- cwd: params.cwd,
932
- runtimeDir: params.runtimeDir
933
- });
934
- const hostCfg = config$1.hosts[hostName];
935
- if (!hostCfg) throw new Error(`missing host in fleet/clawdlets.json: ${hostName}`);
936
- return {
937
- repoRoot,
938
- layout,
939
- config: config$1,
940
- hostName,
941
- hostCfg
942
- };
943
- }
944
-
945
- //#endregion
946
- //#region src/commands/cattle/common.ts
947
- function requireEnabled(params) {
948
- if (params.enabled) return;
949
- throw new Error(params.hint);
950
- }
951
- function requireFile(pathname, label) {
952
- if (fs.existsSync(pathname)) return;
953
- throw new Error(`${label} missing: ${pathname}`);
954
- }
955
- function readJsonFile(filePath) {
956
- const raw = fs.readFileSync(filePath, "utf8");
957
- try {
958
- return JSON.parse(raw);
959
- } catch (e) {
960
- throw new Error(`invalid JSON: ${filePath} (${String(e?.message || e)})`);
961
- }
962
- }
963
- function requireTtlSeconds(ttlRaw) {
964
- const parsed = parseTtlToSeconds(ttlRaw);
965
- if (!parsed) throw new Error(`invalid --ttl: ${ttlRaw} (expected e.g. 30m, 2h, 1d)`);
966
- return {
967
- seconds: parsed.seconds,
968
- normalized: parsed.raw
969
- };
970
- }
971
- function unixSecondsNow() {
972
- return Math.floor(Date.now() / 1e3);
973
- }
974
- function formatAgeSeconds(seconds) {
975
- const s = Math.max(0, Math.floor(seconds));
976
- const d = Math.floor(s / 86400);
977
- const h = Math.floor(s % 86400 / 3600);
978
- const m = Math.floor(s % 3600 / 60);
979
- const ss = s % 60;
980
- if (d > 0) return `${d}d${h}h`;
981
- if (h > 0) return `${h}h${m}m`;
982
- if (m > 0) return `${m}m${ss}s`;
983
- return `${ss}s`;
984
- }
985
- function formatTable(rows) {
986
- if (rows.length === 0) return "";
987
- const widths = [];
988
- for (const r of rows) for (let i = 0; i < r.length; i++) widths[i] = Math.max(widths[i] || 0, String(r[i] ?? "").length);
989
- return rows.map((r) => r.map((c, i) => String(c ?? "").padEnd(widths[i] || 0)).join(" ").trimEnd()).join("\n");
990
- }
991
- async function resolveTailscaleIpv4(hostname) {
992
- const name = String(hostname || "").trim();
993
- if (!name) throw new Error("hostname missing for tailscale ip resolution");
994
- const ip = (await capture("tailscale", [
995
- "ip",
996
- "--1",
997
- "--4",
998
- name
999
- ], { maxOutputBytes: 4096 })).trim();
1000
- if (!ip) throw new Error(`tailscale ip returned empty output for ${name}`);
1001
- return ip;
1002
- }
1003
- function loadTaskFromFile(taskFile) {
1004
- const raw = readJsonFile(taskFile);
1005
- const parsed = CattleTaskSchema.safeParse(raw);
1006
- if (!parsed.success) throw new Error(`invalid task file (expected schemaVersion ${CATTLE_TASK_SCHEMA_VERSION}): ${taskFile}`);
1007
- return parsed.data;
1008
- }
1009
- async function waitForClfJobTerminal(params) {
1010
- const start = Date.now();
1011
- while (true) {
1012
- const job = (await params.client.show(params.jobId)).job;
1013
- if (job?.status === "done" || job?.status === "failed" || job?.status === "canceled") return job;
1014
- if (Date.now() - start > params.timeoutMs) throw new Error(`timeout waiting for job ${params.jobId} (last=${String(job?.status || "")})`);
1015
- await new Promise((r) => setTimeout(r, params.pollMs));
1016
- }
1017
- }
1018
- function resolveOne(servers, idOrName) {
1019
- const v = String(idOrName || "").trim();
1020
- if (!v) throw new Error("missing id/name");
1021
- const byId = servers.find((s) => s.id === v);
1022
- if (byId) return byId;
1023
- const byName = servers.find((s) => s.name === v);
1024
- if (byName) return byName;
1025
- throw new Error(`cattle server not found: ${v}`);
1026
- }
1027
-
1028
- //#endregion
1029
- //#region src/commands/cattle/destroy.ts
1030
- const cattleDestroy = defineCommand({
1031
- meta: {
1032
- name: "destroy",
1033
- description: "Destroy cattle servers (Hetzner delete)."
1034
- },
1035
- args: {
1036
- runtimeDir: {
1037
- type: "string",
1038
- description: "Runtime directory (default: .clawdlets)."
1039
- },
1040
- envFile: {
1041
- type: "string",
1042
- description: "Env file for deploy creds (default: <runtimeDir>/env)."
1043
- },
1044
- host: {
1045
- type: "string",
1046
- description: "Host name (defaults to clawdlets.json defaultHost / sole host)."
1047
- },
1048
- idOrName: {
1049
- type: "string",
1050
- description: "Cattle server id or name."
1051
- },
1052
- all: {
1053
- type: "boolean",
1054
- description: "Destroy all cattle servers.",
1055
- default: false
1056
- },
1057
- persona: {
1058
- type: "string",
1059
- description: "Filter by persona (with --all)."
1060
- },
1061
- dryRun: {
1062
- type: "boolean",
1063
- description: "Print plan without deleting.",
1064
- default: false
1065
- }
1066
- },
1067
- async run({ args }) {
1068
- const cwd = process$1.cwd();
1069
- const ctx = loadHostContextOrExit({
1070
- cwd,
1071
- runtimeDir: args.runtimeDir,
1072
- hostArg: args.host
1073
- });
1074
- if (!ctx) return;
1075
- const { layout, config: config$1 } = ctx;
1076
- requireEnabled({
1077
- enabled: Boolean(config$1.cattle?.enabled),
1078
- hint: "cattle is disabled (set cattle.enabled=true in fleet/clawdlets.json)"
1079
- });
1080
- const deployCreds = loadDeployCreds({
1081
- cwd,
1082
- runtimeDir: args.runtimeDir,
1083
- envFile: args.envFile
1084
- });
1085
- const hcloudToken = String(deployCreds.values.HCLOUD_TOKEN || "").trim();
1086
- if (!hcloudToken) throw new Error("missing HCLOUD_TOKEN (set in .clawdlets/env or env var; run: clawdlets env init)");
1087
- const personaFilterRaw = String(args.persona || "").trim();
1088
- const personaFilter = personaFilterRaw ? safeCattleLabelValue(personaFilterRaw, "persona") : "";
1089
- const servers = await listCattleServers({
1090
- token: hcloudToken,
1091
- labelSelector: buildCattleLabelSelector(personaFilter ? { [CATTLE_LABEL_PERSONA]: personaFilter } : {})
1092
- });
1093
- const targets = [];
1094
- if (args.all) targets.push(...servers);
1095
- else {
1096
- const idOrName = String(args.idOrName || "").trim();
1097
- if (!idOrName) throw new Error("missing <idOrName> (or pass --all)");
1098
- targets.push(resolveOne(servers, idOrName));
1099
- }
1100
- if (targets.length === 0) {
1101
- console.log("ok: no matching cattle servers");
1102
- return;
1103
- }
1104
- const st = openCattleState(layout.cattleDbPath);
1105
- try {
1106
- if (args.dryRun) {
1107
- console.log(formatTable([[
1108
- "ID",
1109
- "NAME",
1110
- "PERSONA",
1111
- "TASK",
1112
- "STATUS"
1113
- ], ...targets.map((s) => [
1114
- s.id,
1115
- s.name,
1116
- s.persona || "-",
1117
- s.taskId || "-",
1118
- s.status
1119
- ])]));
1120
- return;
1121
- }
1122
- const now = unixSecondsNow();
1123
- for (const t of targets) {
1124
- await destroyCattleServer({
1125
- token: hcloudToken,
1126
- id: t.id
1127
- });
1128
- st.markDeletedById(t.id, now);
1129
- }
1130
- } finally {
1131
- st.close();
1132
- }
1133
- console.log(`ok: destroyed ${targets.length} cattle server(s)`);
1134
- }
1135
- });
1136
-
1137
- //#endregion
1138
- //#region src/commands/cattle/list.ts
1139
- const cattleList = defineCommand({
1140
- meta: {
1141
- name: "list",
1142
- description: "List active cattle servers (Hetzner + local state reconciliation)."
1143
- },
1144
- args: {
1145
- runtimeDir: {
1146
- type: "string",
1147
- description: "Runtime directory (default: .clawdlets)."
1148
- },
1149
- envFile: {
1150
- type: "string",
1151
- description: "Env file for deploy creds (default: <runtimeDir>/env)."
1152
- },
1153
- host: {
1154
- type: "string",
1155
- description: "Host name (defaults to clawdlets.json defaultHost / sole host)."
1156
- },
1157
- json: {
1158
- type: "boolean",
1159
- description: "Output JSON.",
1160
- default: false
1161
- }
1162
- },
1163
- async run({ args }) {
1164
- const cwd = process$1.cwd();
1165
- const ctx = loadHostContextOrExit({
1166
- cwd,
1167
- runtimeDir: args.runtimeDir,
1168
- hostArg: args.host
1169
- });
1170
- if (!ctx) return;
1171
- const { layout, config: config$1 } = ctx;
1172
- requireEnabled({
1173
- enabled: Boolean(config$1.cattle?.enabled),
1174
- hint: "cattle is disabled (set cattle.enabled=true in fleet/clawdlets.json)"
1175
- });
1176
- const deployCreds = loadDeployCreds({
1177
- cwd,
1178
- runtimeDir: args.runtimeDir,
1179
- envFile: args.envFile
1180
- });
1181
- const hcloudToken = String(deployCreds.values.HCLOUD_TOKEN || "").trim();
1182
- if (!hcloudToken) throw new Error("missing HCLOUD_TOKEN (set in .clawdlets/env or env var; run: clawdlets env init)");
1183
- const servers = await listCattleServers({
1184
- token: hcloudToken,
1185
- labelSelector: buildCattleLabelSelector()
1186
- });
1187
- const now = unixSecondsNow();
1188
- const byId = /* @__PURE__ */ new Map();
1189
- for (const s of servers) byId.set(s.id, s);
1190
- const st = openCattleState(layout.cattleDbPath);
1191
- try {
1192
- const activeLocal = st.listActive();
1193
- const remoteIds = new Set(servers.map((s) => s.id));
1194
- for (const local of activeLocal) if (!remoteIds.has(local.id)) st.markDeletedById(local.id, now);
1195
- for (const s of servers) {
1196
- const existing = st.findActiveByIdOrName(s.id);
1197
- st.upsertServer({
1198
- id: s.id,
1199
- name: s.name,
1200
- persona: s.persona || existing?.persona || "",
1201
- task: existing?.task || s.taskId || "",
1202
- taskId: s.taskId || existing?.taskId || "",
1203
- ttlSeconds: s.ttlSeconds,
1204
- createdAt: Math.floor(s.createdAt.getTime() / 1e3),
1205
- expiresAt: Math.floor(s.expiresAt.getTime() / 1e3),
1206
- labels: s.labels || existing?.labels || {},
1207
- lastStatus: s.status,
1208
- lastIpv4: s.ipv4
1209
- });
1210
- }
1211
- } finally {
1212
- st.close();
1213
- }
1214
- const sorted = [...servers].sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime() || a.id.localeCompare(b.id));
1215
- if (args.json) {
1216
- console.log(JSON.stringify({ servers: sorted }, null, 2));
1217
- return;
1218
- }
1219
- const rows = [[
1220
- "ID",
1221
- "NAME",
1222
- "PERSONA",
1223
- "TASK",
1224
- "STATUS",
1225
- "TTL"
1226
- ], ...sorted.map((s) => {
1227
- const ttlLeft = Math.max(0, Math.floor(s.expiresAt.getTime() / 1e3) - now);
1228
- return [
1229
- s.id,
1230
- s.name,
1231
- s.persona || "-",
1232
- s.taskId || "-",
1233
- s.status,
1234
- formatAgeSeconds(ttlLeft)
1235
- ];
1236
- })];
1237
- console.log(formatTable(rows));
1238
- }
1239
- });
1240
-
1241
- //#endregion
1242
- //#region src/commands/cattle/logs.ts
1243
- const cattleLogs = defineCommand({
1244
- meta: {
1245
- name: "logs",
1246
- description: "Stream logs from a cattle VM over tailnet SSH."
1247
- },
1248
- args: {
1249
- runtimeDir: {
1250
- type: "string",
1251
- description: "Runtime directory (default: .clawdlets)."
1252
- },
1253
- envFile: {
1254
- type: "string",
1255
- description: "Env file for deploy creds (default: <runtimeDir>/env)."
1256
- },
1257
- host: {
1258
- type: "string",
1259
- description: "Host name (defaults to clawdlets.json defaultHost / sole host)."
1260
- },
1261
- idOrName: {
1262
- type: "string",
1263
- description: "Cattle server id or name.",
1264
- required: true
1265
- },
1266
- lines: {
1267
- type: "string",
1268
- description: "Number of lines (default: 200).",
1269
- default: "200"
1270
- },
1271
- since: {
1272
- type: "string",
1273
- description: "Time window (journalctl syntax, e.g. '10m ago')."
1274
- },
1275
- follow: {
1276
- type: "boolean",
1277
- description: "Follow logs.",
1278
- default: false
1279
- }
1280
- },
1281
- async run({ args }) {
1282
- const cwd = process$1.cwd();
1283
- const ctx = loadHostContextOrExit({
1284
- cwd,
1285
- runtimeDir: args.runtimeDir,
1286
- hostArg: args.host
1287
- });
1288
- if (!ctx) return;
1289
- const { config: config$1 } = ctx;
1290
- requireEnabled({
1291
- enabled: Boolean(config$1.cattle?.enabled),
1292
- hint: "cattle is disabled (set cattle.enabled=true in fleet/clawdlets.json)"
1293
- });
1294
- const deployCreds = loadDeployCreds({
1295
- cwd,
1296
- runtimeDir: args.runtimeDir,
1297
- envFile: args.envFile
1298
- });
1299
- const hcloudToken = String(deployCreds.values.HCLOUD_TOKEN || "").trim();
1300
- if (!hcloudToken) throw new Error("missing HCLOUD_TOKEN (set in .clawdlets/env or env var; run: clawdlets env init)");
1301
- const targetHost = `admin@${await resolveTailscaleIpv4(resolveOne(await listCattleServers({
1302
- token: hcloudToken,
1303
- labelSelector: buildCattleLabelSelector()
1304
- }), String(args.idOrName || "")).name)}`;
1305
- const n = String(args.lines || "200").trim() || "200";
1306
- if (!/^\d+$/.test(n) || Number(n) <= 0) throw new Error(`invalid --lines: ${n}`);
1307
- const since = args.since ? String(args.since).trim() : "";
1308
- await sshRun(targetHost, [
1309
- "sudo",
1310
- "journalctl",
1311
- "-u",
1312
- shellQuote("clawdlets-cattle.service"),
1313
- "-n",
1314
- shellQuote(n),
1315
- ...since ? ["--since", shellQuote(since)] : [],
1316
- ...args.follow ? ["-f"] : [],
1317
- "--no-pager"
1318
- ].join(" "), { redact: [] });
1319
- }
1320
- });
1321
-
1322
- //#endregion
1323
- //#region src/commands/cattle/persona.ts
1324
- function getPersonasDir(repoRoot) {
1325
- return path.join(repoRoot, "cattle", "personas");
1326
- }
1327
- const personaAdd = defineCommand({
1328
- meta: {
1329
- name: "add",
1330
- description: "Create a cattle persona skeleton under cattle/personas/<name>/."
1331
- },
1332
- args: {
1333
- name: {
1334
- type: "string",
1335
- description: "Persona name (safe: [a-z][a-z0-9_-]*).",
1336
- required: true
1337
- },
1338
- force: {
1339
- type: "boolean",
1340
- description: "Overwrite existing files.",
1341
- default: false
1342
- },
1343
- dryRun: {
1344
- type: "boolean",
1345
- description: "Print planned writes without writing.",
1346
- default: false
1347
- }
1348
- },
1349
- async run({ args }) {
1350
- const repoRoot = findRepoRoot(process$1.cwd());
1351
- const name = PersonaNameSchema.parse(String(args.name || "").trim());
1352
- const personasDir = getPersonasDir(repoRoot);
1353
- const dir = path.join(personasDir, name);
1354
- const soulPath = path.join(dir, "SOUL.md");
1355
- const configPath = path.join(dir, "config.json");
1356
- const skillsDir = path.join(dir, "skills");
1357
- const memoryDir = path.join(dir, "memory");
1358
- const soulText = `# ${name}\n\n- tone: (fill)\n- values: (fill)\n- constraints: (fill)\n`;
1359
- const configJson = {
1360
- schemaVersion: 1,
1361
- model: {
1362
- primary: "",
1363
- fallbacks: []
1364
- },
1365
- skills: { allowBundled: [] },
1366
- defaults: { maxConcurrent: 1 }
1367
- };
1368
- const plannedWrites = [
1369
- soulPath,
1370
- configPath,
1371
- skillsDir,
1372
- memoryDir
1373
- ];
1374
- if (args.dryRun) {
1375
- for (const p$1 of plannedWrites) console.log(`planned: ${path.relative(repoRoot, p$1)}`);
1376
- return;
1377
- }
1378
- await ensureDir(skillsDir);
1379
- await ensureDir(memoryDir);
1380
- if (!args.force) {
1381
- if (fs.existsSync(soulPath)) throw new Error(`already exists: ${soulPath} (pass --force to overwrite)`);
1382
- if (fs.existsSync(configPath)) throw new Error(`already exists: ${configPath} (pass --force to overwrite)`);
1383
- }
1384
- await writeFileAtomic(soulPath, soulText.endsWith("\n") ? soulText : `${soulText}\n`);
1385
- await writeFileAtomic(configPath, `${JSON.stringify(configJson, null, 2)}\n`);
1386
- console.log(`ok: created cattle/personas/${name}`);
1387
- }
1388
- });
1389
- const personaList = defineCommand({
1390
- meta: {
1391
- name: "list",
1392
- description: "List cattle personas under cattle/personas/."
1393
- },
1394
- args: { json: {
1395
- type: "boolean",
1396
- description: "Output JSON.",
1397
- default: false
1398
- } },
1399
- async run({ args }) {
1400
- const personasDir = getPersonasDir(findRepoRoot(process$1.cwd()));
1401
- const out = [];
1402
- if (fs.existsSync(personasDir)) for (const ent of fs.readdirSync(personasDir, { withFileTypes: true })) {
1403
- if (!ent.isDirectory()) continue;
1404
- const name = ent.name;
1405
- if (!PersonaNameSchema.safeParse(name).success) continue;
1406
- out.push(name);
1407
- }
1408
- out.sort();
1409
- if (args.json) console.log(JSON.stringify({ personas: out }, null, 2));
1410
- else for (const n of out) console.log(n);
1411
- }
1412
- });
1413
- const cattlePersona = defineCommand({
1414
- meta: {
1415
- name: "persona",
1416
- description: "Cattle persona registry helpers (cattle/personas/<name>/)."
1417
- },
1418
- subCommands: {
1419
- add: personaAdd,
1420
- list: personaList
1421
- }
1422
- });
1423
-
1424
- //#endregion
1425
- //#region src/commands/cattle/reap.ts
1426
- const cattleReap = defineCommand({
1427
- meta: {
1428
- name: "reap",
1429
- description: "Destroy expired cattle servers (TTL enforcement)."
1430
- },
1431
- args: {
1432
- runtimeDir: {
1433
- type: "string",
1434
- description: "Runtime directory (default: .clawdlets)."
1435
- },
1436
- envFile: {
1437
- type: "string",
1438
- description: "Env file for deploy creds (default: <runtimeDir>/env)."
1439
- },
1440
- host: {
1441
- type: "string",
1442
- description: "Host name (defaults to clawdlets.json defaultHost / sole host)."
1443
- },
1444
- dryRun: {
1445
- type: "boolean",
1446
- description: "Print plan without deleting.",
1447
- default: false
1448
- }
1449
- },
1450
- async run({ args }) {
1451
- const cwd = process$1.cwd();
1452
- const ctx = loadHostContextOrExit({
1453
- cwd,
1454
- runtimeDir: args.runtimeDir,
1455
- hostArg: args.host
1456
- });
1457
- if (!ctx) return;
1458
- const { layout, config: config$1 } = ctx;
1459
- requireEnabled({
1460
- enabled: Boolean(config$1.cattle?.enabled),
1461
- hint: "cattle is disabled (set cattle.enabled=true in fleet/clawdlets.json)"
1462
- });
1463
- const deployCreds = loadDeployCreds({
1464
- cwd,
1465
- runtimeDir: args.runtimeDir,
1466
- envFile: args.envFile
1467
- });
1468
- const hcloudToken = String(deployCreds.values.HCLOUD_TOKEN || "").trim();
1469
- if (!hcloudToken) throw new Error("missing HCLOUD_TOKEN (set in .clawdlets/env or env var; run: clawdlets env init)");
1470
- const now = unixSecondsNow();
1471
- const res = await reapExpiredCattle({
1472
- token: hcloudToken,
1473
- labelSelector: buildCattleLabelSelector(),
1474
- now: /* @__PURE__ */ new Date(now * 1e3),
1475
- dryRun: args.dryRun
1476
- });
1477
- const expired = res.expired;
1478
- if (expired.length === 0) {
1479
- console.log("ok: no expired cattle servers");
1480
- return;
1481
- }
1482
- console.log(formatTable([[
1483
- "ID",
1484
- "NAME",
1485
- "PERSONA",
1486
- "TASK",
1487
- "EXPIRES",
1488
- "STATUS"
1489
- ], ...expired.map((s) => [
1490
- s.id,
1491
- s.name,
1492
- s.persona || "-",
1493
- s.taskId || "-",
1494
- String(Math.floor(s.expiresAt.getTime() / 1e3)),
1495
- s.status
1496
- ])]));
1497
- if (args.dryRun) return;
1498
- const st = openCattleState(layout.cattleDbPath);
1499
- try {
1500
- for (const id of res.deletedIds) st.markDeletedById(id, now);
1501
- } finally {
1502
- st.close();
1503
- }
1504
- console.log(`ok: reaped ${res.deletedIds.length} cattle server(s)`);
1505
- }
1506
- });
1507
-
1508
- //#endregion
1509
- //#region src/commands/cattle/spawn.ts
1510
- const cattleSpawn = defineCommand({
1511
- meta: {
1512
- name: "spawn",
1513
- description: "Enqueue a cattle.spawn job via clf-orchestrator (no secrets in user_data)."
1514
- },
1515
- args: {
1516
- runtimeDir: {
1517
- type: "string",
1518
- description: "Runtime directory (default: .clawdlets)."
1519
- },
1520
- host: {
1521
- type: "string",
1522
- description: "Host name (defaults to clawdlets.json defaultHost / sole host)."
1523
- },
1524
- persona: {
1525
- type: "string",
1526
- description: "Persona name.",
1527
- required: true
1528
- },
1529
- taskFile: {
1530
- type: "string",
1531
- description: "Task JSON file (schemaVersion 1).",
1532
- required: true
1533
- },
1534
- ttl: {
1535
- type: "string",
1536
- description: "TTL override (default: cattle.hetzner.defaultTtl)."
1537
- },
1538
- image: {
1539
- type: "string",
1540
- description: "Hetzner image override (default: cattle.hetzner.image)."
1541
- },
1542
- serverType: {
1543
- type: "string",
1544
- description: "Hetzner server type override (default: cattle.hetzner.serverType)."
1545
- },
1546
- location: {
1547
- type: "string",
1548
- description: "Hetzner location override (default: cattle.hetzner.location)."
1549
- },
1550
- autoShutdown: {
1551
- type: "boolean",
1552
- description: "Auto poweroff after task (default: cattle.defaults.autoShutdown)."
1553
- },
1554
- withGithubToken: {
1555
- type: "boolean",
1556
- description: "Include GITHUB_TOKEN in cattle env (explicit).",
1557
- default: false
1558
- },
1559
- socket: {
1560
- type: "string",
1561
- description: "clf-orchestrator unix socket path (default: /run/clf/orchestrator.sock)."
1562
- },
1563
- requester: {
1564
- type: "string",
1565
- description: "Requester id (default: $USER)."
1566
- },
1567
- idempotencyKey: {
1568
- type: "string",
1569
- description: "Idempotency key (optional)."
1570
- },
1571
- wait: {
1572
- type: "boolean",
1573
- description: "Wait for job completion.",
1574
- default: true
1575
- },
1576
- waitTimeout: {
1577
- type: "string",
1578
- description: "Wait timeout seconds.",
1579
- default: "300"
1580
- },
1581
- dryRun: {
1582
- type: "boolean",
1583
- description: "Print enqueue request without enqueueing.",
1584
- default: false
1585
- }
1586
- },
1587
- async run({ args }) {
1588
- const cwd = process$1.cwd();
1589
- const ctx = loadHostContextOrExit({
1590
- cwd,
1591
- runtimeDir: args.runtimeDir,
1592
- hostArg: args.host
1593
- });
1594
- if (!ctx) return;
1595
- const { layout, config: config$1 } = ctx;
1596
- requireEnabled({
1597
- enabled: Boolean(config$1.cattle?.enabled),
1598
- hint: "cattle is disabled (set cattle.enabled=true in fleet/clawdlets.json)"
1599
- });
1600
- const persona = String(args.persona || "").trim();
1601
- if (!persona) throw new Error("missing --persona");
1602
- const taskFileRaw = String(args.taskFile || "").trim();
1603
- if (!taskFileRaw) throw new Error("missing --task-file");
1604
- const taskFile = path.isAbsolute(taskFileRaw) ? taskFileRaw : path.resolve(cwd, taskFileRaw);
1605
- requireFile(taskFile, "task file");
1606
- const task = {
1607
- ...loadTaskFromFile(taskFile),
1608
- callbackUrl: ""
1609
- };
1610
- const ttlRaw = String(args.ttl || config$1.cattle?.hetzner?.defaultTtl || "").trim();
1611
- if (ttlRaw) requireTtlSeconds(ttlRaw);
1612
- const payload = {
1613
- persona,
1614
- task,
1615
- ttl: ttlRaw,
1616
- image: String(args.image || config$1.cattle?.hetzner?.image || "").trim(),
1617
- serverType: String(args.serverType || config$1.cattle?.hetzner?.serverType || "").trim(),
1618
- location: String(args.location || config$1.cattle?.hetzner?.location || "").trim(),
1619
- ...typeof args.autoShutdown === "boolean" ? { autoShutdown: Boolean(args.autoShutdown) } : typeof config$1.cattle?.defaults?.autoShutdown === "boolean" ? { autoShutdown: Boolean(config$1.cattle.defaults.autoShutdown) } : {},
1620
- ...args.withGithubToken ? { withGithubToken: true } : {}
1621
- };
1622
- const socketPath = String(args.socket || process$1.env.CLF_SOCKET_PATH || "/run/clf/orchestrator.sock").trim();
1623
- if (!socketPath) throw new Error("missing --socket (or set CLF_SOCKET_PATH)");
1624
- const request = {
1625
- protocolVersion: CLF_PROTOCOL_VERSION,
1626
- requester: sanitizeOperatorId(String(args.requester || process$1.env.USER || "operator")),
1627
- idempotencyKey: String(args.idempotencyKey || "").trim(),
1628
- kind: "cattle.spawn",
1629
- payload,
1630
- runAt: "",
1631
- priority: 0
1632
- };
1633
- if (args.dryRun) {
1634
- console.log(JSON.stringify({
1635
- action: "clf.jobs.enqueue",
1636
- socketPath,
1637
- request
1638
- }, null, 2));
1639
- return;
1640
- }
1641
- const client = createClfClient({ socketPath });
1642
- const res = await client.enqueue(request);
1643
- const waitTimeoutRaw = String(args.waitTimeout || "300").trim();
1644
- if (!/^\d+$/.test(waitTimeoutRaw) || Number(waitTimeoutRaw) <= 0) throw new Error(`invalid --wait-timeout: ${waitTimeoutRaw}`);
1645
- const timeoutMs = Number(waitTimeoutRaw) * 1e3;
1646
- if (!args.wait) {
1647
- console.log(res.jobId);
1648
- return;
1649
- }
1650
- const job = await waitForClfJobTerminal({
1651
- client,
1652
- jobId: res.jobId,
1653
- timeoutMs,
1654
- pollMs: 1e3
1655
- });
1656
- if (job.status !== "done") {
1657
- const err = String(job.lastError || "").trim();
1658
- throw new Error(`spawn job ${res.jobId} ${job.status}${err ? `: ${err}` : ""}`);
1659
- }
1660
- const server$1 = job.result?.server;
1661
- if (server$1 && typeof server$1 === "object") {
1662
- const id = String(server$1.id || "").trim();
1663
- const name = String(server$1.name || "").trim();
1664
- const ipv4 = String(server$1.ipv4 || "").trim();
1665
- const createdAtIso = String(server$1.createdAt || "").trim();
1666
- const expiresAtIso = String(server$1.expiresAt || "").trim();
1667
- const createdAt = Number.isFinite(Date.parse(createdAtIso)) ? Math.floor(Date.parse(createdAtIso) / 1e3) : unixSecondsNow();
1668
- const expiresAt = Number.isFinite(Date.parse(expiresAtIso)) ? Math.floor(Date.parse(expiresAtIso) / 1e3) : 0;
1669
- const ttlSeconds = typeof server$1.ttlSeconds === "number" && Number.isFinite(server$1.ttlSeconds) ? Math.max(0, Math.floor(server$1.ttlSeconds)) : Math.max(0, expiresAt - createdAt);
1670
- const labels = server$1.labels && typeof server$1.labels === "object" && !Array.isArray(server$1.labels) ? server$1.labels : {};
1671
- if (id && name) {
1672
- const st = openCattleState(layout.cattleDbPath);
1673
- try {
1674
- st.upsertServer({
1675
- id,
1676
- name,
1677
- persona: String(server$1.persona || persona),
1678
- task: String(server$1.taskId || task.taskId),
1679
- taskId: String(server$1.taskId || task.taskId),
1680
- ttlSeconds,
1681
- createdAt,
1682
- expiresAt,
1683
- labels,
1684
- lastStatus: String(server$1.status || "unknown"),
1685
- lastIpv4: ipv4
1686
- });
1687
- } finally {
1688
- st.close();
1689
- }
1690
- }
1691
- console.log(`ok: spawned ${name || "cattle"} (id=${id || "?"} ipv4=${ipv4 || "?"} job=${res.jobId})`);
1692
- return;
806
+ name: "get",
807
+ description: "Get a value from fleet/clawdlets.json (dot path)."
808
+ },
809
+ args: {
810
+ path: {
811
+ type: "string",
812
+ description: "Dot path (e.g. fleet.botOrder)."
813
+ },
814
+ json: {
815
+ type: "boolean",
816
+ description: "JSON output.",
817
+ default: false
1693
818
  }
1694
- console.log(`ok: spawn completed (job=${res.jobId})`);
819
+ },
820
+ async run({ args }) {
821
+ const { config } = loadClawdletsConfig({ repoRoot: findRepoRoot(process$1.cwd()) });
822
+ const parts = splitDotPath(String(args.path || ""));
823
+ const v = getAtPath(config, parts);
824
+ if (args.json) console.log(JSON.stringify({
825
+ path: parts.join("."),
826
+ value: v
827
+ }, null, 2));
828
+ else console.log(typeof v === "string" ? v : JSON.stringify(v, null, 2));
1695
829
  }
1696
830
  });
1697
-
1698
- //#endregion
1699
- //#region src/commands/cattle/ssh.ts
1700
- const cattleSsh = defineCommand({
831
+ const set$2 = defineCommand({
1701
832
  meta: {
1702
- name: "ssh",
1703
- description: "SSH into a cattle VM over tailnet (admin@<tailscale-ip>)."
833
+ name: "set",
834
+ description: "Set a value in fleet/clawdlets.json (dot path)."
1704
835
  },
1705
836
  args: {
1706
- runtimeDir: {
837
+ path: {
1707
838
  type: "string",
1708
- description: "Runtime directory (default: .clawdlets)."
839
+ description: "Dot path (e.g. fleet.botOrder)."
1709
840
  },
1710
- envFile: {
841
+ value: {
1711
842
  type: "string",
1712
- description: "Env file for deploy creds (default: <runtimeDir>/env)."
843
+ description: "String value."
1713
844
  },
1714
- host: {
845
+ "value-json": {
1715
846
  type: "string",
1716
- description: "Host name (defaults to clawdlets.json defaultHost / sole host)."
847
+ description: "JSON value (parsed)."
1717
848
  },
1718
- idOrName: {
1719
- type: "string",
1720
- description: "Cattle server id or name.",
1721
- required: true
849
+ delete: {
850
+ type: "boolean",
851
+ description: "Delete the key at path.",
852
+ default: false
1722
853
  }
1723
854
  },
1724
855
  async run({ args }) {
1725
- const cwd = process$1.cwd();
1726
- const ctx = loadHostContextOrExit({
1727
- cwd,
1728
- runtimeDir: args.runtimeDir,
1729
- hostArg: args.host
1730
- });
1731
- if (!ctx) return;
1732
- const { config: config$1 } = ctx;
1733
- requireEnabled({
1734
- enabled: Boolean(config$1.cattle?.enabled),
1735
- hint: "cattle is disabled (set cattle.enabled=true in fleet/clawdlets.json)"
1736
- });
1737
- const deployCreds = loadDeployCreds({
1738
- cwd,
1739
- runtimeDir: args.runtimeDir,
1740
- envFile: args.envFile
1741
- });
1742
- const hcloudToken = String(deployCreds.values.HCLOUD_TOKEN || "").trim();
1743
- if (!hcloudToken) throw new Error("missing HCLOUD_TOKEN (set in .clawdlets/env or env var; run: clawdlets env init)");
1744
- await run("ssh", [
1745
- "-t",
1746
- "--",
1747
- `admin@${await resolveTailscaleIpv4(resolveOne(await listCattleServers({
1748
- token: hcloudToken,
1749
- labelSelector: buildCattleLabelSelector()
1750
- }), String(args.idOrName || "")).name)}`
1751
- ], { redact: [] });
856
+ const { configPath, config } = loadClawdletsConfigRaw({ repoRoot: findRepoRoot(process$1.cwd()) });
857
+ const parts = splitDotPath(String(args.path || ""));
858
+ const next = structuredClone(config);
859
+ if (args.delete) {
860
+ if (!deleteAtPath(next, parts)) throw new Error(`path not found: ${parts.join(".")}`);
861
+ } else if (args["value-json"] !== void 0) {
862
+ let parsed;
863
+ try {
864
+ parsed = JSON.parse(String(args["value-json"]));
865
+ } catch {
866
+ throw new Error("invalid --value-json (must be valid JSON)");
867
+ }
868
+ setAtPath(next, parts, parsed);
869
+ } else if (args.value !== void 0) setAtPath(next, parts, String(args.value));
870
+ else throw new Error("set requires --value or --value-json (or --delete)");
871
+ try {
872
+ await writeClawdletsConfig({
873
+ configPath,
874
+ config: ClawdletsConfigSchema.parse(next)
875
+ });
876
+ console.log("ok");
877
+ } catch (err) {
878
+ let details = "";
879
+ if (Array.isArray(err?.errors)) details = err.errors.map((e) => (Array.isArray(e.path) ? e.path.join(".") : "") || e.message).filter(Boolean).join(", ");
880
+ const msg = details ? `config update failed; revert or fix validation errors: ${details}` : "config update failed; revert or fix validation errors";
881
+ throw new Error(msg);
882
+ }
1752
883
  }
1753
884
  });
1754
-
1755
- //#endregion
1756
- //#region src/commands/cattle.ts
1757
- const cattle = defineCommand({
885
+ const config = defineCommand({
1758
886
  meta: {
1759
- name: "cattle",
1760
- description: "Cattle (ephemeral agents on Hetzner Cloud)."
887
+ name: "config",
888
+ description: "Canonical config (fleet/clawdlets.json)."
1761
889
  },
1762
890
  subCommands: {
1763
- spawn: cattleSpawn,
1764
- list: cattleList,
1765
- destroy: cattleDestroy,
1766
- reap: cattleReap,
1767
- logs: cattleLogs,
1768
- ssh: cattleSsh,
1769
- persona: cattlePersona
891
+ init,
892
+ show: show$1,
893
+ validate,
894
+ get,
895
+ set: set$2
1770
896
  }
1771
897
  });
1772
898
 
@@ -2022,7 +1148,7 @@ function toStringArray(v) {
2022
1148
  if (Array.isArray(v)) return v.map((x) => String(x));
2023
1149
  return [String(v)];
2024
1150
  }
2025
- const add = defineCommand({
1151
+ const add$1 = defineCommand({
2026
1152
  meta: {
2027
1153
  name: "add",
2028
1154
  description: "Add a host entry to fleet/clawdlets.json."
@@ -2325,7 +1451,7 @@ const host = defineCommand({
2325
1451
  description: "Manage host config (fleet/clawdlets.json)."
2326
1452
  },
2327
1453
  subCommands: {
2328
- add,
1454
+ add: add$1,
2329
1455
  "set-default": setDefault,
2330
1456
  set: set$1
2331
1457
  }
@@ -2888,6 +2014,141 @@ const lockdown = defineCommand({
2888
2014
  }
2889
2015
  });
2890
2016
 
2017
+ //#endregion
2018
+ //#region src/commands/plugin.ts
2019
+ async function loadPlugins() {
2020
+ return await Promise.resolve().then(() => plugins_exports);
2021
+ }
2022
+ function resolveSlug(args) {
2023
+ const raw = String(args.name || args._?.[0] || "").trim();
2024
+ if (!raw) throw new Error("missing plugin name (pass --name or first arg)");
2025
+ return raw;
2026
+ }
2027
+ const list = defineCommand({
2028
+ meta: {
2029
+ name: "list",
2030
+ description: "List installed plugins."
2031
+ },
2032
+ args: {
2033
+ json: {
2034
+ type: "boolean",
2035
+ description: "Output JSON.",
2036
+ default: false
2037
+ },
2038
+ runtimeDir: {
2039
+ type: "string",
2040
+ description: "Runtime directory (default: .clawdlets)."
2041
+ }
2042
+ },
2043
+ async run({ args }) {
2044
+ const { listInstalledPlugins: listInstalledPlugins$1 } = await loadPlugins();
2045
+ const errors = [];
2046
+ const plugins = listInstalledPlugins$1({
2047
+ cwd: process$1.cwd(),
2048
+ runtimeDir: args.runtimeDir,
2049
+ onError: (err) => errors.push(err)
2050
+ });
2051
+ const payload = {
2052
+ plugins,
2053
+ errors: errors.map((e) => ({
2054
+ slug: e.slug,
2055
+ error: e.error.message
2056
+ }))
2057
+ };
2058
+ if (args.json) {
2059
+ console.log(JSON.stringify(payload, null, 2));
2060
+ return;
2061
+ }
2062
+ for (const err of errors) console.error(`warn: skipping plugin ${err.slug}: ${err.error.message}`);
2063
+ if (plugins.length === 0) {
2064
+ console.log("ok: no plugins installed");
2065
+ return;
2066
+ }
2067
+ for (const p$1 of plugins) console.log(`${p$1.command}\t${p$1.packageName}@${p$1.version}`);
2068
+ }
2069
+ });
2070
+ const add = defineCommand({
2071
+ meta: {
2072
+ name: "add",
2073
+ description: "Install a plugin into .clawdlets/plugins."
2074
+ },
2075
+ args: {
2076
+ name: {
2077
+ type: "string",
2078
+ description: "Plugin name (e.g. cattle)."
2079
+ },
2080
+ package: {
2081
+ type: "string",
2082
+ description: "Package to install (default: @clawdlets/plugin-<name>)."
2083
+ },
2084
+ version: {
2085
+ type: "string",
2086
+ description: "Package version/tag (default: latest)."
2087
+ },
2088
+ allowThirdParty: {
2089
+ type: "boolean",
2090
+ description: "Allow third-party plugins (unsafe).",
2091
+ default: false
2092
+ },
2093
+ runtimeDir: {
2094
+ type: "string",
2095
+ description: "Runtime directory (default: .clawdlets)."
2096
+ }
2097
+ },
2098
+ async run({ args }) {
2099
+ const slug = resolveSlug(args);
2100
+ const packageName = String(args.package || `@clawdlets/plugin-${slug}`).trim();
2101
+ if (!args.allowThirdParty && !packageName.startsWith("@clawdlets/")) throw new Error("third-party plugins disabled (pass --allow-third-party to override)");
2102
+ const { installPlugin: installPlugin$1 } = await loadPlugins();
2103
+ const plugin = await installPlugin$1({
2104
+ cwd: process$1.cwd(),
2105
+ runtimeDir: args.runtimeDir,
2106
+ slug,
2107
+ packageName,
2108
+ version: args.version,
2109
+ allowThirdParty: args.allowThirdParty
2110
+ });
2111
+ console.log(`ok: installed ${plugin.command} (${plugin.packageName}@${plugin.version})`);
2112
+ }
2113
+ });
2114
+ const rm = defineCommand({
2115
+ meta: {
2116
+ name: "rm",
2117
+ description: "Remove an installed plugin."
2118
+ },
2119
+ args: {
2120
+ name: {
2121
+ type: "string",
2122
+ description: "Plugin name (e.g. cattle)."
2123
+ },
2124
+ runtimeDir: {
2125
+ type: "string",
2126
+ description: "Runtime directory (default: .clawdlets)."
2127
+ }
2128
+ },
2129
+ async run({ args }) {
2130
+ const slug = resolveSlug(args);
2131
+ const { removePlugin: removePlugin$1 } = await loadPlugins();
2132
+ removePlugin$1({
2133
+ cwd: process$1.cwd(),
2134
+ runtimeDir: args.runtimeDir,
2135
+ slug
2136
+ });
2137
+ console.log(`ok: removed ${slug}`);
2138
+ }
2139
+ });
2140
+ const plugin = defineCommand({
2141
+ meta: {
2142
+ name: "plugin",
2143
+ description: "Plugin manager (install/remove/list)."
2144
+ },
2145
+ subCommands: {
2146
+ add,
2147
+ list,
2148
+ rm
2149
+ }
2150
+ });
2151
+
2891
2152
  //#endregion
2892
2153
  //#region src/lib/template-spec.ts
2893
2154
  function firstNonEmpty(...values) {
@@ -2898,6 +2159,8 @@ function firstNonEmpty(...values) {
2898
2159
  }
2899
2160
  function resolveTemplateSourcePath() {
2900
2161
  const moduleDir = path.dirname(fileURLToPath(import.meta.url));
2162
+ const packagedDistPath = path.resolve(moduleDir, "config", "template-source.json");
2163
+ if (fs.existsSync(packagedDistPath)) return packagedDistPath;
2901
2164
  const packagedPath = path.resolve(moduleDir, "..", "config", "template-source.json");
2902
2165
  if (fs.existsSync(packagedPath)) return packagedPath;
2903
2166
  const repoRootPath = path.resolve(moduleDir, "..", "..", "..", "..", "config", "template-source.json");
@@ -4704,11 +3967,11 @@ const serverAudit = defineCommand({
4704
3967
  const sudo = needsSudo(targetHost);
4705
3968
  const bots = config$1.fleet.botOrder ?? [];
4706
3969
  const checks = [];
4707
- const add$2 = (c) => checks.push(c);
3970
+ const add$3 = (c) => checks.push(c);
4708
3971
  const must = async (label, cmd) => {
4709
3972
  const out = await trySshCapture(targetHost, cmd, { tty: sudo && args.sshTty });
4710
3973
  if (!out.ok) {
4711
- add$2({
3974
+ add$3({
4712
3975
  status: "missing",
4713
3976
  label,
4714
3977
  detail: out.out
@@ -4726,7 +3989,7 @@ const serverAudit = defineCommand({
4726
3989
  ].join(" "));
4727
3990
  if (tailscaled) {
4728
3991
  const parsed = parseSystemctlShow(tailscaled);
4729
- add$2({
3992
+ add$3({
4730
3993
  status: parsed.ActiveState === "active" ? "ok" : "missing",
4731
3994
  label: "tailscale service state",
4732
3995
  detail: `${parsed.ActiveState || "?"}/${parsed.SubState || "?"}`
@@ -4740,19 +4003,19 @@ const serverAudit = defineCommand({
4740
4003
  ].join(" "));
4741
4004
  if (autoconnect) {
4742
4005
  const parsed = parseSystemctlShow(autoconnect);
4743
- add$2({
4006
+ add$3({
4744
4007
  status: parsed.ActiveState === "active" ? "ok" : "missing",
4745
4008
  label: "tailscale autoconnect state",
4746
4009
  detail: `${parsed.ActiveState || "?"}/${parsed.SubState || "?"}`
4747
4010
  });
4748
4011
  }
4749
4012
  }
4750
- if (Array.isArray(bots) && bots.length > 0) add$2({
4013
+ if (Array.isArray(bots) && bots.length > 0) add$3({
4751
4014
  status: "ok",
4752
4015
  label: "fleet bots list",
4753
4016
  detail: bots.join(", ")
4754
4017
  });
4755
- else add$2({
4018
+ else add$3({
4756
4019
  status: "warn",
4757
4020
  label: "fleet bots list",
4758
4021
  detail: "(empty)"
@@ -4770,17 +4033,17 @@ const serverAudit = defineCommand({
4770
4033
  const loadState = parsed.LoadState || "";
4771
4034
  const activeState = parsed.ActiveState || "";
4772
4035
  const subState = parsed.SubState || "";
4773
- if (loadState && loadState !== "loaded") add$2({
4036
+ if (loadState && loadState !== "loaded") add$3({
4774
4037
  status: "missing",
4775
4038
  label: `${unit} load state`,
4776
4039
  detail: `LoadState=${loadState}`
4777
4040
  });
4778
- else if (activeState === "active" && subState === "running") add$2({
4041
+ else if (activeState === "active" && subState === "running") add$3({
4779
4042
  status: "ok",
4780
4043
  label: `${unit} state`,
4781
4044
  detail: `${activeState}/${subState}`
4782
4045
  });
4783
- else add$2({
4046
+ else add$3({
4784
4047
  status: "missing",
4785
4048
  label: `${unit} state`,
4786
4049
  detail: `${activeState || "?"}/${subState || "?"}`
@@ -4973,6 +4236,267 @@ const server = defineCommand({
4973
4236
  }
4974
4237
  });
4975
4238
 
4239
+ //#endregion
4240
+ //#region src/commands/registry.ts
4241
+ const baseCommands = {
4242
+ bot,
4243
+ bootstrap,
4244
+ config,
4245
+ doctor,
4246
+ env,
4247
+ host,
4248
+ fleet,
4249
+ image,
4250
+ infra,
4251
+ lockdown,
4252
+ plugin,
4253
+ project,
4254
+ secrets,
4255
+ server
4256
+ };
4257
+ const baseCommandNames = Object.freeze(Object.keys(baseCommands));
4258
+
4259
+ //#endregion
4260
+ //#region src/lib/plugins.ts
4261
+ var plugins_exports = /* @__PURE__ */ __exportAll({
4262
+ findPluginByCommand: () => findPluginByCommand,
4263
+ installPlugin: () => installPlugin,
4264
+ listInstalledPlugins: () => listInstalledPlugins,
4265
+ listReservedCommands: () => listReservedCommands,
4266
+ loadPluginCommand: () => loadPluginCommand,
4267
+ removePlugin: () => removePlugin
4268
+ });
4269
+ const PLUGIN_MANIFEST = "clawdlets-plugin.json";
4270
+ const RESERVED_COMMANDS = new Set(baseCommandNames);
4271
+ const SAFE_SLUG_RE = /^[a-z][a-z0-9_-]*$/;
4272
+ const PACKAGE_NAME_RE = /^(?:@[a-z0-9][a-z0-9-._]*\/)?[a-z0-9][a-z0-9-._]*$/;
4273
+ function readJsonFile(filePath) {
4274
+ const raw = fs.readFileSync(filePath, "utf8");
4275
+ return JSON.parse(raw);
4276
+ }
4277
+ function writeJsonFile(filePath, data) {
4278
+ const dir = path.dirname(filePath);
4279
+ fs.mkdirSync(dir, { recursive: true });
4280
+ const tmp = path.join(dir, `.${path.basename(filePath)}.tmp.${process.pid}`);
4281
+ fs.writeFileSync(tmp, `${JSON.stringify(data, null, 2)}\n`, "utf8");
4282
+ fs.renameSync(tmp, filePath);
4283
+ }
4284
+ function assertSafeSlug(value) {
4285
+ if (!SAFE_SLUG_RE.test(value)) throw new Error(`invalid plugin command: ${value} (expected [a-z][a-z0-9_-]*)`);
4286
+ }
4287
+ function isReservedCommand(value) {
4288
+ return RESERVED_COMMANDS.has(value);
4289
+ }
4290
+ function assertCommandName(value) {
4291
+ assertSafeSlug(value);
4292
+ if (isReservedCommand(value)) throw new Error(`plugin command reserved: ${value}`);
4293
+ }
4294
+ function resolvePluginsDir(params) {
4295
+ return getRepoLayout(findRepoRoot(params.cwd), params.runtimeDir).pluginsDir;
4296
+ }
4297
+ function resolveInstallDir(params) {
4298
+ return path.join(params.pluginsDir, params.slug);
4299
+ }
4300
+ function resolvePackageDir(params) {
4301
+ return path.join(params.installDir, "node_modules", ...params.packageName.split("/"));
4302
+ }
4303
+ function readPluginPackageMeta(packageDir) {
4304
+ const pkgPath = path.join(packageDir, "package.json");
4305
+ if (!fs.existsSync(pkgPath)) throw new Error(`plugin package.json missing: ${pkgPath}`);
4306
+ const pkg = readJsonFile(pkgPath);
4307
+ const command = String(pkg?.clawdlets?.command || "").trim();
4308
+ const entry = String(pkg?.clawdlets?.entry || "").trim();
4309
+ if (!command) throw new Error(`plugin missing clawdlets.command in ${pkgPath}`);
4310
+ if (!entry) throw new Error(`plugin missing clawdlets.entry in ${pkgPath}`);
4311
+ assertCommandName(command);
4312
+ return {
4313
+ command,
4314
+ entry
4315
+ };
4316
+ }
4317
+ function assertPackageName(value) {
4318
+ if (!PACKAGE_NAME_RE.test(value)) throw new Error(`invalid plugin package name: ${value}`);
4319
+ }
4320
+ function resolveManifestPath(installDir) {
4321
+ return path.join(installDir, PLUGIN_MANIFEST);
4322
+ }
4323
+ function normalizeManifest(slug, manifest) {
4324
+ if (!manifest || typeof manifest !== "object") throw new Error("plugin manifest invalid");
4325
+ const packageName = String(manifest.packageName || "").trim();
4326
+ const version = String(manifest.version || "").trim();
4327
+ const command = String(manifest.command || "").trim();
4328
+ const entry = String(manifest.entry || "").trim();
4329
+ if (!packageName) throw new Error("plugin manifest missing packageName");
4330
+ assertPackageName(packageName);
4331
+ if (!version) throw new Error("plugin manifest missing version");
4332
+ if (!command) throw new Error("plugin manifest missing command");
4333
+ if (!entry) throw new Error("plugin manifest missing entry");
4334
+ assertCommandName(command);
4335
+ if (command !== slug) throw new Error(`plugin manifest command mismatch (expected ${slug}, got ${command})`);
4336
+ return {
4337
+ slug,
4338
+ packageName,
4339
+ version,
4340
+ command,
4341
+ entry
4342
+ };
4343
+ }
4344
+ function readPluginManifest(installDir, slug) {
4345
+ return normalizeManifest(slug, readJsonFile(resolveManifestPath(installDir)));
4346
+ }
4347
+ function writePluginManifest(installDir, manifest) {
4348
+ writeJsonFile(resolveManifestPath(installDir), manifest);
4349
+ }
4350
+ function deriveManifestFromInstall(installDir, slug) {
4351
+ const pkgPath = path.join(installDir, "package.json");
4352
+ if (!fs.existsSync(pkgPath)) throw new Error(`plugin install missing package.json: ${pkgPath}`);
4353
+ const pkg = readJsonFile(pkgPath);
4354
+ const deps = Object.keys(pkg.dependencies || {});
4355
+ if (deps.length !== 1) throw new Error(`plugin install must declare exactly one dependency (found ${deps.length})`);
4356
+ const packageName = deps[0] || "";
4357
+ if (!packageName) throw new Error("plugin dependency missing");
4358
+ assertPackageName(packageName);
4359
+ const packageDir = resolvePackageDir({
4360
+ installDir,
4361
+ packageName
4362
+ });
4363
+ const meta = readPluginPackageMeta(packageDir);
4364
+ if (meta.command !== slug) throw new Error(`plugin command mismatch: expected ${slug} got ${meta.command}`);
4365
+ const pluginPkgPath = path.join(packageDir, "package.json");
4366
+ const pluginPkg = readJsonFile(pluginPkgPath);
4367
+ const version = String(pluginPkg.version || "").trim();
4368
+ if (!version) throw new Error(`plugin version missing in ${pluginPkgPath}`);
4369
+ return {
4370
+ slug,
4371
+ packageName,
4372
+ version,
4373
+ command: meta.command,
4374
+ entry: meta.entry
4375
+ };
4376
+ }
4377
+ function listInstalledPlugins(params) {
4378
+ const pluginsDir = resolvePluginsDir(params);
4379
+ if (!fs.existsSync(pluginsDir)) return [];
4380
+ const entries = fs.readdirSync(pluginsDir, { withFileTypes: true });
4381
+ const out = [];
4382
+ for (const ent of entries) {
4383
+ if (!ent.isDirectory()) continue;
4384
+ const slug = ent.name;
4385
+ if (slug.startsWith(".")) continue;
4386
+ try {
4387
+ assertSafeSlug(slug);
4388
+ const installDir = resolveInstallDir({
4389
+ pluginsDir,
4390
+ slug
4391
+ });
4392
+ let manifest;
4393
+ try {
4394
+ manifest = readPluginManifest(installDir, slug);
4395
+ } catch {
4396
+ manifest = deriveManifestFromInstall(installDir, slug);
4397
+ writePluginManifest(installDir, manifest);
4398
+ }
4399
+ const packageDir = resolvePackageDir({
4400
+ installDir,
4401
+ packageName: manifest.packageName
4402
+ });
4403
+ out.push({
4404
+ ...manifest,
4405
+ installDir,
4406
+ packageDir
4407
+ });
4408
+ } catch (error) {
4409
+ params.onError?.({
4410
+ slug,
4411
+ error: error instanceof Error ? error : new Error(String(error))
4412
+ });
4413
+ continue;
4414
+ }
4415
+ }
4416
+ return out.sort((a, b) => a.command.localeCompare(b.command));
4417
+ }
4418
+ function findPluginByCommand(params) {
4419
+ const cmd = params.command.trim();
4420
+ if (!cmd || cmd.startsWith("-") || isReservedCommand(cmd)) return null;
4421
+ return listInstalledPlugins(params).find((p$1) => p$1.command === cmd) || null;
4422
+ }
4423
+ async function loadPluginCommand(plugin$1) {
4424
+ const entryRel = plugin$1.entry.trim();
4425
+ if (!entryRel) throw new Error(`plugin entry empty for ${plugin$1.command}`);
4426
+ if (path.isAbsolute(entryRel)) throw new Error(`plugin entry must be relative: ${entryRel}`);
4427
+ if (entryRel.split(/[/\\\\]+/).includes("..")) throw new Error(`plugin entry must not contain .. segments: ${entryRel}`);
4428
+ const entryPath = path.resolve(plugin$1.packageDir, entryRel);
4429
+ const entryRelPath = path.relative(plugin$1.packageDir, entryPath);
4430
+ if (entryRelPath.startsWith("..") || path.isAbsolute(entryRelPath)) throw new Error(`plugin entry escapes package: ${entryRel}`);
4431
+ if (!fs.existsSync(entryPath)) throw new Error(`plugin entry missing: ${entryPath}`);
4432
+ const mod = await import(pathToFileURL(entryPath).href);
4433
+ const command = mod.command || mod.plugin?.command || mod.default?.command || mod.default;
4434
+ if (!command) throw new Error(`plugin entry ${entryPath} does not export a command`);
4435
+ return command;
4436
+ }
4437
+ async function installPlugin(params) {
4438
+ const pluginsDir = resolvePluginsDir(params);
4439
+ const slug = params.slug.trim();
4440
+ if (!slug) throw new Error("plugin name required");
4441
+ assertCommandName(slug);
4442
+ const packageName = params.packageName.trim();
4443
+ if (!packageName) throw new Error("package name required");
4444
+ assertPackageName(packageName);
4445
+ if (!params.allowThirdParty && !packageName.startsWith("@clawdlets/")) throw new Error("third-party plugins disabled (pass --allow-third-party to override)");
4446
+ const installDir = resolveInstallDir({
4447
+ pluginsDir,
4448
+ slug
4449
+ });
4450
+ if (fs.existsSync(installDir) && fs.readdirSync(installDir).length > 0) throw new Error(`plugin already installed: ${slug} (${installDir})`);
4451
+ fs.mkdirSync(installDir, { recursive: true });
4452
+ const depVersion = params.version?.trim() || "latest";
4453
+ const pkgJson = {
4454
+ name: `clawdlets-plugin-${slug}`,
4455
+ private: true,
4456
+ type: "module",
4457
+ description: `clawdlets plugin install (${slug})`,
4458
+ dependencies: { [packageName]: depVersion }
4459
+ };
4460
+ writeJsonFile(path.join(installDir, "package.json"), pkgJson);
4461
+ await run("npm", [
4462
+ "install",
4463
+ "--omit=dev",
4464
+ "--no-audit",
4465
+ "--no-fund"
4466
+ ], { cwd: installDir });
4467
+ const manifest = deriveManifestFromInstall(installDir, slug);
4468
+ writePluginManifest(installDir, manifest);
4469
+ const packageDir = resolvePackageDir({
4470
+ installDir,
4471
+ packageName: manifest.packageName
4472
+ });
4473
+ return {
4474
+ ...manifest,
4475
+ installDir,
4476
+ packageDir
4477
+ };
4478
+ }
4479
+ function removePlugin(params) {
4480
+ const pluginsDir = resolvePluginsDir(params);
4481
+ const slug = params.slug.trim();
4482
+ if (!slug) throw new Error("plugin name required");
4483
+ assertSafeSlug(slug);
4484
+ const installDir = resolveInstallDir({
4485
+ pluginsDir,
4486
+ slug
4487
+ });
4488
+ const rel = path.relative(pluginsDir, installDir);
4489
+ if (rel.startsWith("..") || path.isAbsolute(rel)) throw new Error(`plugin path escapes plugins dir: ${installDir}`);
4490
+ if (!fs.existsSync(installDir)) throw new Error(`plugin not installed: ${slug}`);
4491
+ fs.rmSync(installDir, {
4492
+ recursive: true,
4493
+ force: true
4494
+ });
4495
+ }
4496
+ function listReservedCommands() {
4497
+ return [...RESERVED_COMMANDS].sort();
4498
+ }
4499
+
4976
4500
  //#endregion
4977
4501
  //#region src/lib/version.ts
4978
4502
  function resolvePackageRoot(fromUrl = import.meta.url) {
@@ -5001,37 +4525,65 @@ const main = defineCommand({
5001
4525
  name: "clawdlets",
5002
4526
  description: "Clawdbot fleet helper (CLI-first; runtime state in .clawdlets/; secrets in /secrets)."
5003
4527
  },
5004
- subCommands: {
5005
- bot,
5006
- bootstrap,
5007
- cattle,
5008
- config,
5009
- doctor,
5010
- env,
5011
- host,
5012
- fleet,
5013
- image,
5014
- infra,
5015
- lockdown,
5016
- project,
5017
- secrets,
5018
- server
5019
- }
4528
+ subCommands: baseCommands
5020
4529
  });
5021
- {
4530
+ function resolveRuntimeDir(rawArgs) {
4531
+ for (let i = 0; i < rawArgs.length; i += 1) {
4532
+ const arg = rawArgs[i] || "";
4533
+ if (arg === "--runtime-dir" || arg === "--runtimeDir") {
4534
+ const next = rawArgs[i + 1];
4535
+ if (next) return String(next);
4536
+ }
4537
+ if (arg.startsWith("--runtime-dir=")) return arg.slice(14);
4538
+ if (arg.startsWith("--runtimeDir=")) return arg.slice(13);
4539
+ }
4540
+ }
4541
+ function findCommandToken(rawArgs) {
4542
+ for (let i = 0; i < rawArgs.length; i += 1) {
4543
+ const arg = rawArgs[i];
4544
+ if (!arg) continue;
4545
+ if (arg === "--") continue;
4546
+ if (arg === "--runtime-dir" || arg === "--runtimeDir") {
4547
+ i += 1;
4548
+ continue;
4549
+ }
4550
+ if (arg.startsWith("--runtime-dir=") || arg.startsWith("--runtimeDir=")) continue;
4551
+ if (arg.startsWith("-")) continue;
4552
+ return {
4553
+ index: i,
4554
+ command: arg
4555
+ };
4556
+ }
4557
+ return null;
4558
+ }
4559
+ async function mainEntry() {
5022
4560
  const [nodeBin, script, ...rest] = process.argv;
5023
4561
  const normalized = rest.filter((a) => a !== "--");
5024
4562
  if (normalized.includes("--version") || normalized.includes("-v")) {
5025
4563
  console.log(readCliVersion());
5026
4564
  process.exit(0);
4565
+ return;
5027
4566
  }
5028
4567
  process.argv = [
5029
4568
  nodeBin,
5030
4569
  script,
5031
4570
  ...normalized
5032
4571
  ];
4572
+ const runtimeDir = resolveRuntimeDir(normalized);
4573
+ const commandToken = findCommandToken(normalized);
4574
+ const command = commandToken?.command ?? "";
4575
+ const pluginMatch = findPluginByCommand({
4576
+ cwd: process.cwd(),
4577
+ runtimeDir,
4578
+ command
4579
+ });
4580
+ if (pluginMatch) {
4581
+ await runMain(await loadPluginCommand(pluginMatch), { rawArgs: commandToken ? normalized.slice(commandToken.index + 1) : [] });
4582
+ return;
4583
+ }
4584
+ await runMain(main);
5033
4585
  }
5034
- runMain(main);
4586
+ mainEntry();
5035
4587
 
5036
4588
  //#endregion
5037
4589
  export { };