@clawops/cli 2.0.1 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +862 -807
- package/dist/{apply-KYFPWI5U.js → apply-HJ5GAKHW.js} +7 -7
- package/dist/{automation-RMAIBNVC.js → automation-MKCJ46I6.js} +3 -3
- 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-STLRHLLV.js → chunk-A2DS5SB7.js} +12 -9
- package/dist/{chunk-KC3GC3PE.js → chunk-AYUX4FOF.js} +127 -13
- 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-IVWP4QUJ.js → chunk-GFUK6KDR.js} +10 -10
- package/dist/{chunk-W3OLNNJW.js → chunk-PSOSBJVO.js} +1 -1
- package/dist/{chunk-5XT5IAJK.js → chunk-QDNFADJI.js} +12 -5
- package/dist/{chunk-LBE6COC4.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/{cli-WLD3EBEM.js → cli-NH7VUNOU.js} +1 -1
- package/dist/cli.js +156 -82
- package/dist/{context-7EVC6W45.js → context-7VI3R6EK.js} +4 -4
- package/dist/flows-RI7FGHAM.js +120 -0
- package/dist/{generate-I6YQ5V2T.js → generate-YEM73KVB.js} +6 -6
- package/dist/{harden-72JMRT4E.js → harden-3KKCR7BL.js} +541 -3
- package/dist/{mcp-wire-3SWBM7D6.js → mcp-wire-TE2ZL6HD.js} +10 -10
- package/dist/{package-5BCMEBTE.js → package-JWDMWPV3.js} +47 -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-GBA7BJGK.js → server-ZJPXPRZT.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 +39 -8
- package/spec/deploy-plan.schema.json +16 -1
- package/spec/mcp-tools.yaml +67 -9
|
@@ -13,16 +13,20 @@ import {
|
|
|
13
13
|
} from "./chunk-2I7KCMXZ.js";
|
|
14
14
|
import {
|
|
15
15
|
writeStackConfig
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-3Z5TDXCJ.js";
|
|
17
17
|
import {
|
|
18
18
|
validatePlan
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-XFN3YDZE.js";
|
|
20
20
|
import {
|
|
21
21
|
buildContext
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-QDNFADJI.js";
|
|
23
23
|
import {
|
|
24
24
|
GATEWAY_PORT
|
|
25
25
|
} from "./chunk-VOEJYRIK.js";
|
|
26
|
+
import {
|
|
27
|
+
getConfig,
|
|
28
|
+
setConfig
|
|
29
|
+
} from "./chunk-64PONSA5.js";
|
|
26
30
|
import {
|
|
27
31
|
UsageError
|
|
28
32
|
} from "./chunk-KGXPLI7W.js";
|
|
@@ -49,6 +53,9 @@ ${validation.errors.join("\n")}`
|
|
|
49
53
|
provider: plan.spec.provider
|
|
50
54
|
});
|
|
51
55
|
const stack = await ctx.getStack();
|
|
56
|
+
const { guardPrivateOnlyApply } = await import("./private-only-DIA26I2E.js");
|
|
57
|
+
const { probeSsh } = await import("./tailscale-cutover-6YP2QKFM.js");
|
|
58
|
+
await guardPrivateOnlyApply(plan, ctx, opts?.probeTailnet ?? ((conn2) => probeSsh(conn2, opts?.signal)));
|
|
52
59
|
await writeStackConfig(stack, plan);
|
|
53
60
|
if (plan.metadata.stackVersion !== void 0) {
|
|
54
61
|
const currentInfo = await stack.info();
|
|
@@ -67,6 +74,7 @@ The diff you reviewed may no longer reflect what will be applied.
|
|
|
67
74
|
}
|
|
68
75
|
const start = Date.now();
|
|
69
76
|
const result = await stack.up({ onOutput: opts?.onOutput, signal: opts?.signal });
|
|
77
|
+
recordPublicExposure(ctx.stackName, plan);
|
|
70
78
|
const outputs = Object.fromEntries(
|
|
71
79
|
Object.entries(result.outputs).map(([k, v]) => [k, v.value])
|
|
72
80
|
);
|
|
@@ -238,6 +246,17 @@ async function verifyChannels(session, cfg, signal) {
|
|
|
238
246
|
);
|
|
239
247
|
}
|
|
240
248
|
}
|
|
249
|
+
function recordPublicExposure(stackName, plan) {
|
|
250
|
+
const config = getConfig();
|
|
251
|
+
const stackConfig = config?.stacks[stackName];
|
|
252
|
+
if (!config || !stackConfig?.tailscale) return;
|
|
253
|
+
const privateOnly = plan.spec.network.tailscale?.privateOnly === true;
|
|
254
|
+
if ((stackConfig.tailscale.privateOnly ?? false) === privateOnly) return;
|
|
255
|
+
const tailscale = { ...stackConfig.tailscale };
|
|
256
|
+
if (privateOnly) tailscale.privateOnly = true;
|
|
257
|
+
else delete tailscale.privateOnly;
|
|
258
|
+
setConfig({ ...config, stacks: { ...config.stacks, [stackName]: { ...stackConfig, tailscale } } });
|
|
259
|
+
}
|
|
241
260
|
|
|
242
261
|
export {
|
|
243
262
|
applyPlan
|
|
@@ -154,9 +154,10 @@ async function createBucket(bucket, project, token, region) {
|
|
|
154
154
|
);
|
|
155
155
|
if (!res.ok) throw new Error(`Could not create gs://${bucket}: ${res.status} ${await res.text()}`);
|
|
156
156
|
}
|
|
157
|
+
|
|
157
158
|
export {
|
|
159
|
+
resolveProjectId,
|
|
158
160
|
gcloudConfiguredProject,
|
|
159
|
-
gcpPreflight,
|
|
160
161
|
projectFromIni,
|
|
161
|
-
|
|
162
|
+
gcpPreflight
|
|
162
163
|
};
|
|
@@ -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
|
+
};
|