@clawops/cli 0.2.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.
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ StateError,
4
+ UsageError
5
+ } from "./chunk-ZSE4QRKE.js";
6
+
7
+ // src/config/store.ts
8
+ import { readFileSync, writeFileSync, mkdirSync, renameSync } from "fs";
9
+ import { randomUUID } from "crypto";
10
+ import os from "os";
11
+ import path from "path";
12
+ import process from "process";
13
+ import { z } from "zod";
14
+ var CredentialsRefSchema = z.object({
15
+ source: z.enum(["env", "cli-profile", "file", "instance-metadata"]),
16
+ envVars: z.array(z.string()).optional(),
17
+ profileName: z.string().optional()
18
+ });
19
+ var LocalOptsSchema = z.object({
20
+ host: z.string(),
21
+ sshUser: z.string(),
22
+ sshPort: z.number(),
23
+ sshKeyPath: z.string()
24
+ });
25
+ var StackConfigSchema = z.object({
26
+ provider: z.string(),
27
+ stateUrl: z.string(),
28
+ region: z.string().optional(),
29
+ credentialsRef: CredentialsRefSchema,
30
+ localOpts: LocalOptsSchema.optional()
31
+ });
32
+ var ClawopsConfigSchema = z.object({
33
+ version: z.literal(1),
34
+ defaults: z.object({
35
+ stack: z.string(),
36
+ provider: z.string()
37
+ }),
38
+ stacks: z.record(StackConfigSchema),
39
+ ssh: z.object({
40
+ keyPath: z.string(),
41
+ knownHostsPath: z.string()
42
+ }),
43
+ mcp: z.object({ auditLogPath: z.string() }).optional()
44
+ });
45
+ function getConfigDir() {
46
+ return process.env["CLAWOPS_HOME"] ?? path.join(os.homedir(), ".clawops");
47
+ }
48
+ function getConfigPath() {
49
+ return path.join(getConfigDir(), "config.json");
50
+ }
51
+ function getConfig() {
52
+ try {
53
+ const raw = readFileSync(getConfigPath(), "utf-8");
54
+ const parsed = JSON.parse(raw);
55
+ const result = ClawopsConfigSchema.safeParse(parsed);
56
+ if (!result.success) {
57
+ throw new StateError(
58
+ `Config file is invalid: ${result.error.issues.map((i) => i.message).join(", ")}. Run \`clawops init\` to reinitialise.`
59
+ );
60
+ }
61
+ return result.data;
62
+ } catch (err) {
63
+ if (err.code === "ENOENT") return null;
64
+ throw err;
65
+ }
66
+ }
67
+ function requireConfig() {
68
+ const cfg = getConfig();
69
+ if (!cfg) {
70
+ throw new UsageError("No config found. Run `clawops init` first.");
71
+ }
72
+ return cfg;
73
+ }
74
+ function setConfig(config) {
75
+ const dir = getConfigDir();
76
+ mkdirSync(dir, { recursive: true });
77
+ const dest = getConfigPath();
78
+ const tmp = path.join(dir, `.config.${randomUUID()}.tmp`);
79
+ writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", "utf-8");
80
+ renameSync(tmp, dest);
81
+ }
82
+
83
+ export {
84
+ getConfigDir,
85
+ getConfigPath,
86
+ getConfig,
87
+ requireConfig,
88
+ setConfig
89
+ };
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ buildContext
4
+ } from "./chunk-PERSDQMT.js";
5
+ import {
6
+ validatePlan
7
+ } from "./chunk-YTH4L2GN.js";
8
+ import {
9
+ getConfig
10
+ } from "./chunk-ALSUDYA7.js";
11
+ import {
12
+ UsageError
13
+ } from "./chunk-ZSE4QRKE.js";
14
+
15
+ // src/plan/generate.ts
16
+ import { randomUUID } from "crypto";
17
+ var PREVIEW_LINE_RE = /^([+~-])\s+(\S+)\s+(\S+)?/;
18
+ function parseDiff(lines) {
19
+ const create = [];
20
+ const update = [];
21
+ const del = [];
22
+ for (const line of lines) {
23
+ const m = PREVIEW_LINE_RE.exec(line.trimStart());
24
+ if (!m) continue;
25
+ const [, op, resourceType, name] = m;
26
+ const ref = {
27
+ urn: `urn:pulumi:::clawops::${resourceType}::${name ?? ""}`,
28
+ type: resourceType,
29
+ ...name ? { name } : {}
30
+ };
31
+ if (op === "+") create.push(ref);
32
+ else if (op === "~") update.push({ resource: ref, before: null, after: null });
33
+ else if (op === "-") del.push(ref);
34
+ }
35
+ return {
36
+ create,
37
+ update,
38
+ delete: del,
39
+ totalChanges: create.length + update.length + del.length
40
+ };
41
+ }
42
+ async function generatePlan(intent, _opts) {
43
+ if (intent.provider === "local") {
44
+ throw new UsageError(
45
+ "plan/apply is not supported for the local provider. Use `clawops up` directly."
46
+ );
47
+ }
48
+ const { version } = await import("./package-YZFYSSQM.js");
49
+ const config = getConfig();
50
+ const instanceType = intent.instanceType ?? "small";
51
+ const openclawVersion = intent.openclawVersion ?? "latest";
52
+ const network = intent.network ?? {
53
+ allowedSshCidrs: [],
54
+ allowedGatewayCidrs: []
55
+ };
56
+ const stackName = intent.stackName;
57
+ const region = intent.region ?? config?.stacks[stackName]?.region;
58
+ const plan = {
59
+ apiVersion: "clawops.dev/v1",
60
+ kind: "DeployPlan",
61
+ metadata: {
62
+ name: stackName,
63
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
64
+ generator: "clawops",
65
+ generatorVersion: version
66
+ },
67
+ spec: {
68
+ provider: intent.provider,
69
+ region,
70
+ stackName,
71
+ instanceType,
72
+ openclaw: { version: openclawVersion },
73
+ network,
74
+ ...intent.tags ? { tags: intent.tags } : {}
75
+ }
76
+ };
77
+ try {
78
+ const ctx = buildContext({ stack: stackName, provider: intent.provider });
79
+ const stack = await ctx.getStack();
80
+ await stack.setConfig("instanceType", { value: instanceType });
81
+ if (region) await stack.setConfig("region", { value: region });
82
+ await stack.setConfig("openclawVersion", { value: openclawVersion });
83
+ const outputLines = [];
84
+ const preview = await stack.preview({
85
+ onOutput: (line) => outputLines.push(line)
86
+ });
87
+ const diff = parseDiff(outputLines);
88
+ if (diff.totalChanges === 0 && preview.changeSummary) {
89
+ const summary = preview.changeSummary;
90
+ diff.totalChanges = (summary["create"] ?? 0) + (summary["update"] ?? 0) + (summary["delete"] ?? 0);
91
+ }
92
+ plan.diff = diff;
93
+ } catch (err) {
94
+ process.stderr.write(
95
+ `[clawops] Warning: preview failed, diff section omitted: ${err instanceof Error ? err.message : String(err)}
96
+ `
97
+ );
98
+ }
99
+ const validation = validatePlan(plan);
100
+ if (!validation.ok) {
101
+ throw new Error(`Generated plan failed schema validation:
102
+ ${validation.errors.join("\n")}`);
103
+ }
104
+ return plan;
105
+ }
106
+ function planId() {
107
+ return randomUUID().slice(0, 8);
108
+ }
109
+
110
+ export {
111
+ generatePlan,
112
+ planId
113
+ };
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ StateError
4
+ } from "./chunk-ZSE4QRKE.js";
5
+
6
+ // src/pulumi/outputs.ts
7
+ function extractBaseOutputs(raw) {
8
+ const required = [
9
+ "instanceId",
10
+ "publicIp",
11
+ "gatewayUrl",
12
+ "sshHost",
13
+ "sshPort",
14
+ "sshUser",
15
+ "region",
16
+ "provisionedAt"
17
+ ];
18
+ for (const key of required) {
19
+ if (raw[key] === void 0 || raw[key] === null) {
20
+ throw new StateError(
21
+ `Stack output is missing required field "${key}". The stack may need to be re-deployed with \`clawops up\`.`
22
+ );
23
+ }
24
+ }
25
+ return {
26
+ instanceId: String(raw["instanceId"]),
27
+ publicIp: String(raw["publicIp"]),
28
+ gatewayUrl: String(raw["gatewayUrl"]),
29
+ sshHost: String(raw["sshHost"]),
30
+ sshPort: Number(raw["sshPort"]),
31
+ sshUser: String(raw["sshUser"]),
32
+ region: String(raw["region"]),
33
+ provisionedAt: String(raw["provisionedAt"])
34
+ };
35
+ }
36
+
37
+ export {
38
+ extractBaseOutputs
39
+ };
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ readLocalState
4
+ } from "./chunk-PRYLTCS4.js";
5
+ import {
6
+ getConfigDir,
7
+ requireConfig
8
+ } from "./chunk-ALSUDYA7.js";
9
+ import {
10
+ UsageError
11
+ } from "./chunk-ZSE4QRKE.js";
12
+
13
+ // src/cli/context.ts
14
+ function buildContext(args) {
15
+ const config = requireConfig();
16
+ const stackName = typeof args.stack === "string" ? args.stack : config.defaults.stack;
17
+ const providerName = typeof args.provider === "string" ? args.provider : config.stacks[stackName]?.provider ?? config.defaults.provider;
18
+ const adapter = loadProvider(providerName);
19
+ const localState = providerName === "local" ? readLocalState(stackName) : void 0;
20
+ let stackCache = null;
21
+ return {
22
+ config,
23
+ adapter,
24
+ stackName,
25
+ localState,
26
+ async getStack() {
27
+ if (providerName === "local") {
28
+ throw new UsageError(
29
+ "The local provider does not use Pulumi stacks. Use `clawops up` to bootstrap the host instead."
30
+ );
31
+ }
32
+ if (stackCache) return stackCache;
33
+ const stackConfig = config.stacks[stackName];
34
+ if (!stackConfig) {
35
+ throw new UsageError(
36
+ `Stack "${stackName}" not found in config. Run \`clawops init\` or use \`--stack\` to specify a different stack.`
37
+ );
38
+ }
39
+ const { getOrCreateStack } = await import("./automation-JG2YBEL7.js");
40
+ stackCache = await getOrCreateStack({
41
+ stack: stackName,
42
+ stateUrl: stackConfig.stateUrl,
43
+ program: adapter.program,
44
+ configDir: getConfigDir()
45
+ });
46
+ return stackCache;
47
+ }
48
+ };
49
+ }
50
+ function loadProvider(name) {
51
+ return makeProviderProxy(name);
52
+ }
53
+ function makeProviderProxy(name) {
54
+ let resolved = null;
55
+ const resolve = async () => {
56
+ if (resolved) return resolved;
57
+ switch (name) {
58
+ case "gcp": {
59
+ const mod = await import("./gcp-OHWCTFLL.js");
60
+ resolved = mod.default;
61
+ return resolved;
62
+ }
63
+ case "aws": {
64
+ const mod = await import("./aws-A3323GNM.js");
65
+ resolved = mod.default;
66
+ return resolved;
67
+ }
68
+ case "azure": {
69
+ const mod = await import("./azure-LNWBK2ZI.js");
70
+ resolved = mod.default;
71
+ return resolved;
72
+ }
73
+ case "local": {
74
+ const mod = await import("./local-DXBEVZ5C.js");
75
+ resolved = mod.default;
76
+ return resolved;
77
+ }
78
+ default:
79
+ throw new UsageError(
80
+ `Provider "${name}" is not yet supported. Supported providers: gcp, aws, azure, local`
81
+ );
82
+ }
83
+ };
84
+ return {
85
+ name,
86
+ get program() {
87
+ return async () => {
88
+ const adapter = await resolve();
89
+ return adapter.program();
90
+ };
91
+ },
92
+ getConnectionInfo: (outputs) => {
93
+ if (!resolved) throw new UsageError("Provider not yet loaded. Call getStack() first.");
94
+ return resolved.getConnectionInfo(outputs);
95
+ },
96
+ normalizeInstanceType: (alias) => {
97
+ if (!resolved) throw new UsageError("Provider not yet loaded. Call getStack() first.");
98
+ return resolved.normalizeInstanceType(alias);
99
+ },
100
+ defaultRegion: () => {
101
+ if (!resolved) throw new UsageError("Provider not yet loaded. Call getStack() first.");
102
+ return resolved.defaultRegion();
103
+ },
104
+ stateBackendUrl: (bucket) => {
105
+ if (!resolved) throw new UsageError("Provider not yet loaded. Call getStack() first.");
106
+ return resolved.stateBackendUrl(bucket);
107
+ },
108
+ validateConfig: () => resolve().then((a) => a.validateConfig())
109
+ };
110
+ }
111
+
112
+ export {
113
+ buildContext
114
+ };
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ getConfigDir
4
+ } from "./chunk-ALSUDYA7.js";
5
+
6
+ // src/providers/local/state.ts
7
+ import { readFileSync, writeFileSync, mkdirSync, renameSync } from "fs";
8
+ import { randomUUID } from "crypto";
9
+ import path from "path";
10
+ function stateDir() {
11
+ return path.join(getConfigDir(), "state");
12
+ }
13
+ function statePath(stackName) {
14
+ return path.join(stateDir(), `${stackName}.json`);
15
+ }
16
+ function readLocalState(stackName) {
17
+ try {
18
+ const raw = readFileSync(statePath(stackName), "utf-8");
19
+ return JSON.parse(raw);
20
+ } catch (err) {
21
+ if (err.code === "ENOENT") return null;
22
+ throw err;
23
+ }
24
+ }
25
+ function writeLocalState(stackName, state) {
26
+ const dir = stateDir();
27
+ mkdirSync(dir, { recursive: true });
28
+ const dest = statePath(stackName);
29
+ const tmp = path.join(dir, `.state-${randomUUID()}.tmp`);
30
+ writeFileSync(tmp, JSON.stringify(state, null, 2) + "\n", "utf-8");
31
+ renameSync(tmp, dest);
32
+ }
33
+
34
+ export {
35
+ readLocalState,
36
+ writeLocalState
37
+ };
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ buildContext
4
+ } from "./chunk-PERSDQMT.js";
5
+ import {
6
+ validatePlan
7
+ } from "./chunk-YTH4L2GN.js";
8
+ import {
9
+ UsageError
10
+ } from "./chunk-ZSE4QRKE.js";
11
+
12
+ // src/plan/apply.ts
13
+ async function applyPlan(plan, opts) {
14
+ const validation = validatePlan(plan);
15
+ if (!validation.ok) {
16
+ throw new UsageError(
17
+ `Invalid deploy plan:
18
+ ${validation.errors.join("\n")}`
19
+ );
20
+ }
21
+ if (plan.spec.provider === "local") {
22
+ throw new UsageError(
23
+ "plan/apply is not supported for the local provider. Use `clawops up` directly."
24
+ );
25
+ }
26
+ const ctx = buildContext({
27
+ stack: plan.spec.stackName,
28
+ provider: plan.spec.provider
29
+ });
30
+ const stack = await ctx.getStack();
31
+ await stack.setConfig("instanceType", { value: plan.spec.instanceType });
32
+ if (plan.spec.region) {
33
+ await stack.setConfig("region", { value: plan.spec.region });
34
+ }
35
+ await stack.setConfig("openclawVersion", { value: plan.spec.openclaw.version });
36
+ const start = Date.now();
37
+ const result = await stack.up({ onOutput: opts?.onOutput });
38
+ const outputs = Object.fromEntries(
39
+ Object.entries(result.outputs).map(([k, v]) => [k, v.value])
40
+ );
41
+ const changeSummary = {};
42
+ if (result.summary.resourceChanges) {
43
+ for (const [op, count] of Object.entries(result.summary.resourceChanges)) {
44
+ changeSummary[op] = count;
45
+ }
46
+ }
47
+ return {
48
+ outputs,
49
+ changeSummary,
50
+ durationMs: Date.now() - start
51
+ };
52
+ }
53
+
54
+ export {
55
+ applyPlan
56
+ };
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/plan/validate.ts
4
+ import Ajv from "ajv/dist/2020";
5
+ import addFormats from "ajv-formats";
6
+ import { readFileSync } from "fs";
7
+ import { fileURLToPath } from "url";
8
+ import { join, dirname } from "path";
9
+ var __dirname = dirname(fileURLToPath(import.meta.url));
10
+ var _validate;
11
+ function getValidator() {
12
+ if (_validate) return _validate;
13
+ const ajv = new Ajv({ strict: false });
14
+ addFormats(ajv);
15
+ const schema = JSON.parse(
16
+ readFileSync(join(__dirname, "../../spec/deploy-plan.schema.json"), "utf-8")
17
+ );
18
+ _validate = ajv.compile(schema);
19
+ return _validate;
20
+ }
21
+ function validatePlan(plan) {
22
+ const validate = getValidator();
23
+ const ok = validate(plan);
24
+ if (ok) return { ok: true, errors: [] };
25
+ const errors = (validate.errors ?? []).map(
26
+ (e) => `${e.instancePath} ${e.message ?? ""}`.trim()
27
+ );
28
+ return { ok: false, errors };
29
+ }
30
+ function assertValidPlan(plan) {
31
+ const result = validatePlan(plan);
32
+ if (!result.ok) {
33
+ throw new Error(`Invalid deploy plan:
34
+ ${result.errors.join("\n")}`);
35
+ }
36
+ }
37
+
38
+ export {
39
+ validatePlan,
40
+ assertValidPlan
41
+ };
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/errors/index.ts
4
+ var ClawopsError = class extends Error {
5
+ constructor(message, errorClass, exitCode, retryable = false) {
6
+ super(message);
7
+ this.errorClass = errorClass;
8
+ this.exitCode = exitCode;
9
+ this.retryable = retryable;
10
+ this.name = this.constructor.name;
11
+ if (Error.captureStackTrace) Error.captureStackTrace(this, this.constructor);
12
+ }
13
+ errorClass;
14
+ exitCode;
15
+ retryable;
16
+ };
17
+ var UsageError = class extends ClawopsError {
18
+ constructor(message) {
19
+ super(message, "UsageError", 2, false);
20
+ }
21
+ };
22
+ var StateError = class extends ClawopsError {
23
+ constructor(message) {
24
+ super(message, "StateError", 4, false);
25
+ }
26
+ };
27
+ var ProviderError = class extends ClawopsError {
28
+ constructor(message, retryable = false) {
29
+ super(message, "ProviderError", 5, retryable);
30
+ }
31
+ };
32
+ var NetworkError = class extends ClawopsError {
33
+ constructor(message) {
34
+ super(message, "NetworkError", 6, true);
35
+ }
36
+ };
37
+
38
+ export {
39
+ ClawopsError,
40
+ UsageError,
41
+ StateError,
42
+ ProviderError,
43
+ NetworkError
44
+ };