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.
- package/dist/main.mjs +556 -1004
- package/node_modules/@clawdlets/core/dist/lib/context.d.ts +21 -0
- package/node_modules/@clawdlets/core/dist/lib/context.d.ts.map +1 -0
- package/node_modules/@clawdlets/core/dist/lib/context.js +19 -0
- package/node_modules/@clawdlets/core/dist/lib/context.js.map +1 -0
- package/node_modules/@clawdlets/core/dist/lib/host-resolve.d.ts +6 -0
- package/node_modules/@clawdlets/core/dist/lib/host-resolve.d.ts.map +1 -0
- package/node_modules/@clawdlets/core/dist/lib/host-resolve.js +20 -0
- package/node_modules/@clawdlets/core/dist/lib/host-resolve.js.map +1 -0
- package/node_modules/@clawdlets/core/dist/repo-layout.d.ts +1 -0
- package/node_modules/@clawdlets/core/dist/repo-layout.d.ts.map +1 -1
- package/node_modules/@clawdlets/core/dist/repo-layout.js +2 -0
- package/node_modules/@clawdlets/core/dist/repo-layout.js.map +1 -1
- package/node_modules/@clawdlets/core/package.json +1 -3
- package/package.json +3 -6
- package/node_modules/@clawdlets/clf-queue/dist/client.d.ts +0 -21
- package/node_modules/@clawdlets/clf-queue/dist/client.d.ts.map +0 -1
- package/node_modules/@clawdlets/clf-queue/dist/client.js +0 -132
- package/node_modules/@clawdlets/clf-queue/dist/client.js.map +0 -1
- package/node_modules/@clawdlets/clf-queue/dist/index.d.ts +0 -9
- package/node_modules/@clawdlets/clf-queue/dist/index.d.ts.map +0 -1
- package/node_modules/@clawdlets/clf-queue/dist/index.js +0 -5
- package/node_modules/@clawdlets/clf-queue/dist/index.js.map +0 -1
- package/node_modules/@clawdlets/clf-queue/dist/jobs.d.ts +0 -32
- package/node_modules/@clawdlets/clf-queue/dist/jobs.d.ts.map +0 -1
- package/node_modules/@clawdlets/clf-queue/dist/jobs.js +0 -24
- package/node_modules/@clawdlets/clf-queue/dist/jobs.js.map +0 -1
- package/node_modules/@clawdlets/clf-queue/dist/protocol.d.ts +0 -118
- package/node_modules/@clawdlets/clf-queue/dist/protocol.d.ts.map +0 -1
- package/node_modules/@clawdlets/clf-queue/dist/protocol.js +0 -46
- package/node_modules/@clawdlets/clf-queue/dist/protocol.js.map +0 -1
- package/node_modules/@clawdlets/clf-queue/dist/queue/bootstrap-tokens.d.ts +0 -3
- package/node_modules/@clawdlets/clf-queue/dist/queue/bootstrap-tokens.d.ts.map +0 -1
- package/node_modules/@clawdlets/clf-queue/dist/queue/bootstrap-tokens.js +0 -112
- package/node_modules/@clawdlets/clf-queue/dist/queue/bootstrap-tokens.js.map +0 -1
- package/node_modules/@clawdlets/clf-queue/dist/queue/jobs.d.ts +0 -3
- package/node_modules/@clawdlets/clf-queue/dist/queue/jobs.d.ts.map +0 -1
- package/node_modules/@clawdlets/clf-queue/dist/queue/jobs.js +0 -313
- package/node_modules/@clawdlets/clf-queue/dist/queue/jobs.js.map +0 -1
- package/node_modules/@clawdlets/clf-queue/dist/queue/migrate.d.ts +0 -2
- package/node_modules/@clawdlets/clf-queue/dist/queue/migrate.d.ts.map +0 -1
- package/node_modules/@clawdlets/clf-queue/dist/queue/migrate.js +0 -74
- package/node_modules/@clawdlets/clf-queue/dist/queue/migrate.js.map +0 -1
- package/node_modules/@clawdlets/clf-queue/dist/queue/open.d.ts +0 -3
- package/node_modules/@clawdlets/clf-queue/dist/queue/open.d.ts.map +0 -1
- package/node_modules/@clawdlets/clf-queue/dist/queue/open.js +0 -27
- package/node_modules/@clawdlets/clf-queue/dist/queue/open.js.map +0 -1
- package/node_modules/@clawdlets/clf-queue/dist/queue/types.d.ts +0 -113
- package/node_modules/@clawdlets/clf-queue/dist/queue/types.d.ts.map +0 -1
- package/node_modules/@clawdlets/clf-queue/dist/queue/types.js +0 -2
- package/node_modules/@clawdlets/clf-queue/dist/queue/types.js.map +0 -1
- package/node_modules/@clawdlets/clf-queue/dist/queue/util.d.ts +0 -10
- package/node_modules/@clawdlets/clf-queue/dist/queue/util.d.ts.map +0 -1
- package/node_modules/@clawdlets/clf-queue/dist/queue/util.js +0 -30
- package/node_modules/@clawdlets/clf-queue/dist/queue/util.js.map +0 -1
- package/node_modules/@clawdlets/clf-queue/dist/queue.d.ts +0 -3
- package/node_modules/@clawdlets/clf-queue/dist/queue.d.ts.map +0 -1
- package/node_modules/@clawdlets/clf-queue/dist/queue.js +0 -2
- package/node_modules/@clawdlets/clf-queue/dist/queue.js.map +0 -1
- package/node_modules/@clawdlets/clf-queue/package.json +0 -36
- package/node_modules/@clawdlets/core/dist/lib/cattle-state.d.ts +0 -25
- package/node_modules/@clawdlets/core/dist/lib/cattle-state.d.ts.map +0 -1
- package/node_modules/@clawdlets/core/dist/lib/cattle-state.js +0 -136
- package/node_modules/@clawdlets/core/dist/lib/cattle-state.js.map +0 -1
- package/node_modules/better-sqlite3/LICENSE +0 -21
- package/node_modules/better-sqlite3/README.md +0 -99
- package/node_modules/better-sqlite3/binding.gyp +0 -38
- package/node_modules/better-sqlite3/deps/common.gypi +0 -68
- package/node_modules/better-sqlite3/deps/copy.js +0 -31
- package/node_modules/better-sqlite3/deps/defines.gypi +0 -41
- package/node_modules/better-sqlite3/deps/download.sh +0 -122
- package/node_modules/better-sqlite3/deps/patches/1208.patch +0 -15
- package/node_modules/better-sqlite3/deps/sqlite3/sqlite3.c +0 -265969
- package/node_modules/better-sqlite3/deps/sqlite3/sqlite3.h +0 -13968
- package/node_modules/better-sqlite3/deps/sqlite3/sqlite3ext.h +0 -730
- package/node_modules/better-sqlite3/deps/sqlite3.gyp +0 -80
- package/node_modules/better-sqlite3/deps/test_extension.c +0 -21
- package/node_modules/better-sqlite3/lib/database.js +0 -90
- package/node_modules/better-sqlite3/lib/index.js +0 -3
- package/node_modules/better-sqlite3/lib/methods/aggregate.js +0 -43
- package/node_modules/better-sqlite3/lib/methods/backup.js +0 -67
- package/node_modules/better-sqlite3/lib/methods/function.js +0 -31
- package/node_modules/better-sqlite3/lib/methods/inspect.js +0 -7
- package/node_modules/better-sqlite3/lib/methods/pragma.js +0 -12
- package/node_modules/better-sqlite3/lib/methods/serialize.js +0 -16
- package/node_modules/better-sqlite3/lib/methods/table.js +0 -189
- package/node_modules/better-sqlite3/lib/methods/transaction.js +0 -78
- package/node_modules/better-sqlite3/lib/methods/wrappers.js +0 -54
- package/node_modules/better-sqlite3/lib/sqlite-error.js +0 -20
- package/node_modules/better-sqlite3/lib/util.js +0 -12
- package/node_modules/better-sqlite3/package.json +0 -59
- package/node_modules/better-sqlite3/src/addon.cpp +0 -47
- package/node_modules/better-sqlite3/src/better_sqlite3.cpp +0 -74
- package/node_modules/better-sqlite3/src/objects/backup.cpp +0 -120
- package/node_modules/better-sqlite3/src/objects/backup.hpp +0 -36
- package/node_modules/better-sqlite3/src/objects/database.cpp +0 -417
- package/node_modules/better-sqlite3/src/objects/database.hpp +0 -103
- package/node_modules/better-sqlite3/src/objects/statement-iterator.cpp +0 -113
- package/node_modules/better-sqlite3/src/objects/statement-iterator.hpp +0 -50
- package/node_modules/better-sqlite3/src/objects/statement.cpp +0 -383
- package/node_modules/better-sqlite3/src/objects/statement.hpp +0 -58
- package/node_modules/better-sqlite3/src/util/bind-map.cpp +0 -73
- package/node_modules/better-sqlite3/src/util/binder.cpp +0 -193
- package/node_modules/better-sqlite3/src/util/constants.cpp +0 -172
- package/node_modules/better-sqlite3/src/util/custom-aggregate.cpp +0 -121
- package/node_modules/better-sqlite3/src/util/custom-function.cpp +0 -59
- package/node_modules/better-sqlite3/src/util/custom-table.cpp +0 -409
- package/node_modules/better-sqlite3/src/util/data-converter.cpp +0 -17
- package/node_modules/better-sqlite3/src/util/data.cpp +0 -194
- package/node_modules/better-sqlite3/src/util/helpers.cpp +0 -109
- package/node_modules/better-sqlite3/src/util/macros.cpp +0 -70
- package/node_modules/better-sqlite3/src/util/query-macros.cpp +0 -71
- 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$
|
|
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$
|
|
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$
|
|
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$
|
|
406
|
-
await rm$
|
|
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
|
-
|
|
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: "
|
|
1703
|
-
description: "
|
|
833
|
+
name: "set",
|
|
834
|
+
description: "Set a value in fleet/clawdlets.json (dot path)."
|
|
1704
835
|
},
|
|
1705
836
|
args: {
|
|
1706
|
-
|
|
837
|
+
path: {
|
|
1707
838
|
type: "string",
|
|
1708
|
-
description: "
|
|
839
|
+
description: "Dot path (e.g. fleet.botOrder)."
|
|
1709
840
|
},
|
|
1710
|
-
|
|
841
|
+
value: {
|
|
1711
842
|
type: "string",
|
|
1712
|
-
description: "
|
|
843
|
+
description: "String value."
|
|
1713
844
|
},
|
|
1714
|
-
|
|
845
|
+
"value-json": {
|
|
1715
846
|
type: "string",
|
|
1716
|
-
description: "
|
|
847
|
+
description: "JSON value (parsed)."
|
|
1717
848
|
},
|
|
1718
|
-
|
|
1719
|
-
type: "
|
|
1720
|
-
description: "
|
|
1721
|
-
|
|
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
|
|
1726
|
-
const
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
})
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
"
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
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: "
|
|
1760
|
-
description: "
|
|
887
|
+
name: "config",
|
|
888
|
+
description: "Canonical config (fleet/clawdlets.json)."
|
|
1761
889
|
},
|
|
1762
890
|
subCommands: {
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
-
|
|
4586
|
+
mainEntry();
|
|
5035
4587
|
|
|
5036
4588
|
//#endregion
|
|
5037
4589
|
export { };
|