@clawops/cli 2.0.2 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +877 -804
- package/dist/{apply-WOUTUUQH.js → apply-HJ5GAKHW.js} +7 -7
- package/dist/{automation-UKRM3BD2.js → automation-MKCJ46I6.js} +1 -1
- package/dist/{bootstrap-HNGKY6WW.js → bootstrap-NN647RCY.js} +2 -2
- package/dist/{chunk-HSPHXG6W.js → chunk-3Z5TDXCJ.js} +2 -2
- package/dist/{chunk-CX5SL5HP.js → chunk-64PONSA5.js} +10 -1
- package/dist/{chunk-LEVIW2PV.js → chunk-BELTQGHT.js} +16 -2
- package/dist/{preflight-MWWM5JT3.js → chunk-BTW7NHOH.js} +3 -2
- package/dist/{chunk-A2I76FTA.js → chunk-ELU3XQEA.js} +1 -1
- package/dist/{chunk-5TXCUOZV.js → chunk-GFUK6KDR.js} +9 -9
- package/dist/{chunk-RATEIAFU.js → chunk-OFTAXLOY.js} +127 -13
- package/dist/{chunk-WTZ2CT35.js → chunk-QDNFADJI.js} +12 -5
- package/dist/{chunk-ZMMRIWFS.js → chunk-R3DTA35B.js} +22 -3
- package/dist/{preflight-EF52S3WW.js → chunk-TC2VYPLH.js} +3 -2
- package/dist/chunk-X56CQ6GB.js +157 -0
- package/dist/{chunk-BSLIYB5Q.js → chunk-XFN3YDZE.js} +1 -1
- package/dist/{chunk-SDZKCICH.js → chunk-YEBFVLOE.js} +12 -9
- package/dist/cli.js +169 -83
- package/dist/{context-M4JBBIAV.js → context-7VI3R6EK.js} +4 -4
- package/dist/flows-RI7FGHAM.js +120 -0
- package/dist/{generate-J6MIJGII.js → generate-JNJ63ZYZ.js} +6 -6
- package/dist/{harden-72JMRT4E.js → harden-3KKCR7BL.js} +541 -3
- package/dist/{mcp-wire-NTGL5ITF.js → mcp-wire-B57DUNOD.js} +10 -10
- package/dist/{package-3A6BOALU.js → package-SAUDFQLG.js} +46 -10
- package/dist/preflight-7W23GP4R.js +19 -0
- package/dist/preflight-IE2YWBAC.js +13 -0
- package/dist/private-only-DIA26I2E.js +74 -0
- package/dist/{server-RWM2JI3D.js → server-RHON6QMN.js} +10 -10
- package/dist/{stack-config-R4SPSUQZ.js → stack-config-LUTS2XAD.js} +2 -2
- package/dist/{state-PRBIIN7I.js → state-AOAYLYY5.js} +2 -2
- package/dist/{store-SDUR52Z5.js → store-OVB3DAJR.js} +1 -1
- package/dist/tailscale-cutover-6YP2QKFM.js +113 -0
- package/dist/{validate-YVI3BJ55.js → validate-JXKN5WD6.js} +1 -1
- package/package.json +38 -8
- package/spec/deploy-plan.schema.json +16 -1
- package/spec/mcp-tools.yaml +67 -9
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
getConfigDir
|
|
4
|
+
} from "./chunk-64PONSA5.js";
|
|
5
|
+
|
|
6
|
+
// src/config/secrets.ts
|
|
7
|
+
import { readFileSync, existsSync } from "fs";
|
|
8
|
+
import path from "path";
|
|
9
|
+
function secretsDir(configDir) {
|
|
10
|
+
return path.join(configDir ?? getConfigDir(), "secrets");
|
|
11
|
+
}
|
|
12
|
+
function resolveSecretRef(name, configDir) {
|
|
13
|
+
const p = path.join(secretsDir(configDir), name);
|
|
14
|
+
if (!existsSync(p)) return null;
|
|
15
|
+
try {
|
|
16
|
+
return readFileSync(p, "utf-8").trim();
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// src/harden/modules/tailscale.ts
|
|
23
|
+
var AUTH_KEY_SECRET = "TAILSCALE_AUTH_KEY";
|
|
24
|
+
var KEY_PATH = "/run/clawops-tailscale.key";
|
|
25
|
+
function parseStatus(json) {
|
|
26
|
+
let body;
|
|
27
|
+
try {
|
|
28
|
+
body = JSON.parse(json);
|
|
29
|
+
} catch {
|
|
30
|
+
return { state: "unknown" };
|
|
31
|
+
}
|
|
32
|
+
const b = body;
|
|
33
|
+
const state = typeof b.BackendState === "string" ? b.BackendState : "unknown";
|
|
34
|
+
const ips = Array.isArray(b.Self?.TailscaleIPs) ? b.Self?.TailscaleIPs : [];
|
|
35
|
+
const ipv4 = ips.find((i) => typeof i === "string" && isTailscaleIpv4(i));
|
|
36
|
+
const hostname = typeof b.Self?.HostName === "string" ? b.Self.HostName : void 0;
|
|
37
|
+
return { state, ...ipv4 ? { ipv4 } : {}, ...hostname ? { hostname } : {} };
|
|
38
|
+
}
|
|
39
|
+
function isTailscaleIpv4(ip) {
|
|
40
|
+
const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(ip.trim());
|
|
41
|
+
if (!m) return false;
|
|
42
|
+
const octets = m.slice(1).map(Number);
|
|
43
|
+
if (octets.some((o) => o > 255)) return false;
|
|
44
|
+
return octets[0] === 100 && octets[1] >= 64 && octets[1] <= 127;
|
|
45
|
+
}
|
|
46
|
+
function tailnetHostname(stack) {
|
|
47
|
+
const slug = stack.trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
48
|
+
return `clawops-${slug || "stack"}`.slice(0, 63);
|
|
49
|
+
}
|
|
50
|
+
async function installed(exec) {
|
|
51
|
+
const r = await exec("command -v tailscale >/dev/null 2>&1 && echo yes || echo no");
|
|
52
|
+
return r.stdout.trim() === "yes";
|
|
53
|
+
}
|
|
54
|
+
var DAEMON_DOWN = /failed to connect to local tailscaled|is tailscaled running|connection refused/i;
|
|
55
|
+
async function status(exec) {
|
|
56
|
+
const r = await exec("tailscale status --json 2>&1 || true");
|
|
57
|
+
if (DAEMON_DOWN.test(r.stdout)) return { state: "unknown", daemonDown: true };
|
|
58
|
+
return parseStatus(r.stdout);
|
|
59
|
+
}
|
|
60
|
+
async function ensureDaemon(exec, sudo) {
|
|
61
|
+
const r = await exec(
|
|
62
|
+
`command -v systemctl >/dev/null 2>&1 && ${sudo}systemctl start tailscaled >/dev/null 2>&1 && echo started || echo no`
|
|
63
|
+
);
|
|
64
|
+
return r.stdout.trim() === "started";
|
|
65
|
+
}
|
|
66
|
+
async function rootPrefix(exec) {
|
|
67
|
+
return (await exec("id -u")).stdout.trim() === "0" ? "" : "sudo -n ";
|
|
68
|
+
}
|
|
69
|
+
function shellQuote(text) {
|
|
70
|
+
return `'${text.replace(/'/g, `'\\''`)}'`;
|
|
71
|
+
}
|
|
72
|
+
function makeTailscaleModule(stack) {
|
|
73
|
+
return {
|
|
74
|
+
id: "tailscale",
|
|
75
|
+
label: "Tailscale membership (joins the host to your tailnet)",
|
|
76
|
+
defaultOn: false,
|
|
77
|
+
providers: "all",
|
|
78
|
+
async check(exec) {
|
|
79
|
+
if (!await installed(exec)) {
|
|
80
|
+
return { status: "missing", detail: "Tailscale is not installed on this host." };
|
|
81
|
+
}
|
|
82
|
+
const s = await status(exec);
|
|
83
|
+
if (s.state === "Running" && s.ipv4) {
|
|
84
|
+
return { status: "applied", detail: `Joined as ${s.hostname ?? "this host"} on ${s.ipv4}.` };
|
|
85
|
+
}
|
|
86
|
+
if (s.daemonDown) {
|
|
87
|
+
return {
|
|
88
|
+
status: "drifted",
|
|
89
|
+
detail: "Tailscale is installed but the tailscaled daemon is not running, so this host is on no tailnet whatever it was joined to before. Start it with `systemctl start tailscaled`, or check `systemctl status tailscaled` for why it stopped."
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (s.state === "unknown") {
|
|
93
|
+
return {
|
|
94
|
+
status: "drifted",
|
|
95
|
+
detail: "Tailscale is installed but did not report a status that could be parsed."
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
status: "missing",
|
|
100
|
+
detail: `Tailscale is installed but not on a tailnet (state: ${s.state}).`
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
async apply(exec) {
|
|
104
|
+
const key = resolveSecretRef(AUTH_KEY_SECRET);
|
|
105
|
+
if (!key) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`No auth key. Create one in the Tailscale admin console and store it with \`clawops secret set ${AUTH_KEY_SECRET}\`. clawops does not prompt for it here, so a key never reaches a terminal scrollback or a CI log.`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
if (!await installed(exec)) {
|
|
111
|
+
const r2 = await exec("curl -fsSL https://tailscale.com/install.sh | sh");
|
|
112
|
+
if (!await installed(exec)) {
|
|
113
|
+
throw new Error(`Tailscale install failed: ${(r2.stderr || r2.stdout).slice(0, 300)}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const sudo = await rootPrefix(exec);
|
|
117
|
+
if ((await status(exec)).daemonDown && !await ensureDaemon(exec, sudo)) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
"Tailscale is installed but tailscaled is not running and could not be started. On a systemd host, `systemctl status tailscaled` says why; on a host without an init manager, tailscaled has to be supervised by whatever does run there."
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
const hostname = tailnetHostname(stack ?? (await exec("hostname")).stdout);
|
|
123
|
+
const join = [
|
|
124
|
+
`umask 077`,
|
|
125
|
+
`trap 'rm -f ${KEY_PATH}' EXIT INT TERM`,
|
|
126
|
+
`cat > ${KEY_PATH}`,
|
|
127
|
+
`tailscale up --auth-key=file:${KEY_PATH} --hostname=${hostname} --accept-routes 2>&1`
|
|
128
|
+
].join("\n");
|
|
129
|
+
const r = await exec(`${sudo}sh -c ${shellQuote(join)}`, { stdin: key });
|
|
130
|
+
const s = await status(exec);
|
|
131
|
+
if (s.state === "Running" && s.ipv4) {
|
|
132
|
+
return {
|
|
133
|
+
changed: true,
|
|
134
|
+
detail: `Joined the tailnet as ${s.hostname ?? hostname} on ${s.ipv4}. clawops still reaches this host on its public address; nothing has been pointed at the new one yet.`
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
throw new Error(
|
|
138
|
+
`tailscale up did not bring the host onto the network (state: ${s.state}). ` + // The key is never interpolated into a message. An expired or single-use key is the
|
|
139
|
+
// usual cause and Tailscale says so in output that does not contain the key itself.
|
|
140
|
+
redactKey(r.stdout || r.stderr, key).slice(0, 300)
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
var tailscaleModule = makeTailscaleModule();
|
|
146
|
+
function redactKey(text, key) {
|
|
147
|
+
if (!key) return text;
|
|
148
|
+
return text.split(key).join("[redacted]");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export {
|
|
152
|
+
parseStatus,
|
|
153
|
+
isTailscaleIpv4,
|
|
154
|
+
rootPrefix,
|
|
155
|
+
makeTailscaleModule,
|
|
156
|
+
tailscaleModule
|
|
157
|
+
};
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
validatePlan
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-XFN3YDZE.js";
|
|
5
5
|
import {
|
|
6
6
|
buildContext,
|
|
7
7
|
loadAdapterModule
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-QDNFADJI.js";
|
|
9
9
|
import {
|
|
10
10
|
getConfig
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-64PONSA5.js";
|
|
12
12
|
import {
|
|
13
13
|
StateError,
|
|
14
14
|
UsageError
|
|
@@ -16,11 +16,12 @@ import {
|
|
|
16
16
|
|
|
17
17
|
// src/plan/generate.ts
|
|
18
18
|
import { randomUUID } from "crypto";
|
|
19
|
-
var PREVIEW_LINE_RE = /^([+~-])\s+(\S+)\s+(\S+)?/;
|
|
19
|
+
var PREVIEW_LINE_RE = /^(\+-|-\+|[+~-])\s+(\S+)\s+(\S+)?/;
|
|
20
20
|
function parseDiff(lines) {
|
|
21
21
|
const create = [];
|
|
22
22
|
const update = [];
|
|
23
23
|
const del = [];
|
|
24
|
+
const replace = [];
|
|
24
25
|
const seen = /* @__PURE__ */ new Set();
|
|
25
26
|
for (const line of lines) {
|
|
26
27
|
const m = PREVIEW_LINE_RE.exec(line.trimStart());
|
|
@@ -34,7 +35,8 @@ function parseDiff(lines) {
|
|
|
34
35
|
type: resourceType,
|
|
35
36
|
...name ? { name } : {}
|
|
36
37
|
};
|
|
37
|
-
if (op === "
|
|
38
|
+
if (op === "+-" || op === "-+") replace.push(ref);
|
|
39
|
+
else if (op === "+") create.push(ref);
|
|
38
40
|
else if (op === "~") update.push({ resource: ref, before: null, after: null });
|
|
39
41
|
else if (op === "-") del.push(ref);
|
|
40
42
|
}
|
|
@@ -42,7 +44,8 @@ function parseDiff(lines) {
|
|
|
42
44
|
create,
|
|
43
45
|
update,
|
|
44
46
|
delete: del,
|
|
45
|
-
|
|
47
|
+
replace,
|
|
48
|
+
totalChanges: create.length + update.length + del.length + replace.length
|
|
46
49
|
};
|
|
47
50
|
}
|
|
48
51
|
var INSTANCE_ALIASES = ["micro", "small", "medium", "large", "gpu"];
|
|
@@ -68,7 +71,7 @@ async function generatePlan(intent, _opts) {
|
|
|
68
71
|
"plan/apply is not supported for the local provider. Use `clawops up` directly."
|
|
69
72
|
);
|
|
70
73
|
}
|
|
71
|
-
const { version } = await import("./package-
|
|
74
|
+
const { version } = await import("./package-SAUDFQLG.js");
|
|
72
75
|
const config = getConfig();
|
|
73
76
|
const instanceType = await resolveInstanceType(intent);
|
|
74
77
|
const { guardOpenclawVersion, defaultOpenclawVersion } = await import("./version-guard-XVYW5WFJ.js");
|
|
@@ -120,7 +123,7 @@ A plan is only as good as the state it was computed against, so this is not a pl
|
|
|
120
123
|
);
|
|
121
124
|
}
|
|
122
125
|
try {
|
|
123
|
-
const { writeStackConfig } = await import("./stack-config-
|
|
126
|
+
const { writeStackConfig } = await import("./stack-config-LUTS2XAD.js");
|
|
124
127
|
await writeStackConfig(stack, plan);
|
|
125
128
|
const outputLines = [];
|
|
126
129
|
const preview = await stack.preview({
|
|
@@ -148,7 +151,7 @@ A plan is only as good as the state it was computed against, so this is not a pl
|
|
|
148
151
|
throw new Error(`Generated plan failed schema validation:
|
|
149
152
|
${validation.errors.join("\n")}`);
|
|
150
153
|
}
|
|
151
|
-
const { validatePlanConfig, validatePlanNetwork } = await import("./validate-
|
|
154
|
+
const { validatePlanConfig, validatePlanNetwork } = await import("./validate-JXKN5WD6.js");
|
|
152
155
|
const cfgCheck = await validatePlanConfig(plan);
|
|
153
156
|
for (const w of cfgCheck.warnings) process.stderr.write(`[clawops] warning: ${w}
|
|
154
157
|
`);
|