clawchef 0.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.
Files changed (62) hide show
  1. package/README.md +405 -0
  2. package/dist/api.d.ts +14 -0
  3. package/dist/api.js +49 -0
  4. package/dist/assertions.d.ts +2 -0
  5. package/dist/assertions.js +32 -0
  6. package/dist/cli.d.ts +2 -0
  7. package/dist/cli.js +115 -0
  8. package/dist/env.d.ts +1 -0
  9. package/dist/env.js +14 -0
  10. package/dist/errors.d.ts +3 -0
  11. package/dist/errors.js +6 -0
  12. package/dist/index.d.ts +2 -0
  13. package/dist/index.js +18 -0
  14. package/dist/logger.d.ts +7 -0
  15. package/dist/logger.js +17 -0
  16. package/dist/openclaw/command-provider.d.ts +15 -0
  17. package/dist/openclaw/command-provider.js +489 -0
  18. package/dist/openclaw/factory.d.ts +3 -0
  19. package/dist/openclaw/factory.js +13 -0
  20. package/dist/openclaw/mock-provider.d.ts +15 -0
  21. package/dist/openclaw/mock-provider.js +65 -0
  22. package/dist/openclaw/provider.d.ts +20 -0
  23. package/dist/openclaw/provider.js +1 -0
  24. package/dist/openclaw/remote-provider.d.ts +19 -0
  25. package/dist/openclaw/remote-provider.js +158 -0
  26. package/dist/orchestrator.d.ts +4 -0
  27. package/dist/orchestrator.js +243 -0
  28. package/dist/recipe.d.ts +20 -0
  29. package/dist/recipe.js +522 -0
  30. package/dist/schema.d.ts +626 -0
  31. package/dist/schema.js +143 -0
  32. package/dist/template.d.ts +2 -0
  33. package/dist/template.js +30 -0
  34. package/dist/types.d.ts +136 -0
  35. package/dist/types.js +1 -0
  36. package/package.json +41 -0
  37. package/recipes/content-from-sample.yaml +20 -0
  38. package/recipes/openclaw-from-zero.yaml +45 -0
  39. package/recipes/openclaw-local.yaml +65 -0
  40. package/recipes/openclaw-remote-http.yaml +38 -0
  41. package/recipes/openclaw-telegram-mock.yaml +22 -0
  42. package/recipes/openclaw-telegram.yaml +19 -0
  43. package/recipes/sample.yaml +49 -0
  44. package/recipes/snippets/readme-template.md +3 -0
  45. package/src/api.ts +65 -0
  46. package/src/assertions.ts +37 -0
  47. package/src/cli.ts +123 -0
  48. package/src/env.ts +16 -0
  49. package/src/errors.ts +6 -0
  50. package/src/index.ts +20 -0
  51. package/src/logger.ts +17 -0
  52. package/src/openclaw/command-provider.ts +594 -0
  53. package/src/openclaw/factory.ts +16 -0
  54. package/src/openclaw/mock-provider.ts +104 -0
  55. package/src/openclaw/provider.ts +44 -0
  56. package/src/openclaw/remote-provider.ts +264 -0
  57. package/src/orchestrator.ts +271 -0
  58. package/src/recipe.ts +621 -0
  59. package/src/schema.ts +157 -0
  60. package/src/template.ts +41 -0
  61. package/src/types.ts +150 -0
  62. package/tsconfig.json +16 -0
package/README.md ADDED
@@ -0,0 +1,405 @@
1
+ # clawchef
2
+
3
+ Recipe-driven OpenClaw environment orchestrator.
4
+
5
+ ## What it does
6
+
7
+ - Parses a YAML recipe.
8
+ - Accepts recipe input from local file/dir/archive and HTTP URL/archive.
9
+ - Resolves `${var}` parameters from `--var`, environment, and defaults.
10
+ - Auto-loads environment variables from `.env` in the current working directory.
11
+ - Requires secrets to be injected via `--var` / `CLAWCHEF_VAR_*` (no inline secrets in recipe).
12
+ - Prepares OpenClaw version (install or reuse).
13
+ - When installed OpenClaw version mismatches recipe version, prompts: ignore / abort / force reinstall (silent mode auto-picks force reinstall).
14
+ - Always runs factory reset first (with confirmation prompt unless `-s/--silent` is used).
15
+ - If `openclaw` is missing, auto-installs the recipe version and skips factory reset.
16
+ - Starts OpenClaw gateway service after each recipe execution.
17
+ - Creates workspaces and agents (default workspace path: `~/.openclaw/workspaces/<workspace-name>`).
18
+ - Materializes files into target workspaces.
19
+ - Installs skills.
20
+ - Configures channels with `openclaw channels add`.
21
+ - Enables channel plugins before channel configuration.
22
+ - Supports interactive channel login at the end of execution (`channels[].login: true`).
23
+ - Supports remote HTTP orchestration via runtime flags (`--provider remote`) when OpenClaw is reachable via API.
24
+ - Writes preset conversation messages.
25
+ - Runs agent and validates reply output.
26
+
27
+ ## Install and run
28
+
29
+ ```bash
30
+ npm install
31
+ npm run build
32
+ npm i -g .
33
+ clawchef cook recipes/sample.yaml
34
+ ```
35
+
36
+ Run recipe from URL:
37
+
38
+ ```bash
39
+ clawchef cook https://example.com/recipes/sample.yaml --provider remote -s
40
+ ```
41
+
42
+ Run recipe from archive (default `recipe.yaml`):
43
+
44
+ ```bash
45
+ clawchef cook ./bundle.tgz --provider mock -s
46
+ ```
47
+
48
+ Run specific recipe in directory or archive:
49
+
50
+ ```bash
51
+ clawchef cook ./recipes-pack:team/recipe-prod.yaml --provider remote -s
52
+ clawchef cook https://example.com/recipes-pack.zip:team/recipe-prod.yaml --provider remote -s
53
+ ```
54
+
55
+ Dev mode:
56
+
57
+ ```bash
58
+ clawchef cook recipes/sample.yaml --verbose
59
+ ```
60
+
61
+ Run sample with mock provider:
62
+
63
+ ```bash
64
+ clawchef cook recipes/sample.yaml --provider mock -s
65
+ ```
66
+
67
+ Run `content_from` sample:
68
+
69
+ ```bash
70
+ clawchef cook recipes/content-from-sample.yaml --provider mock -s
71
+ ```
72
+
73
+ Skip reset confirmation prompt:
74
+
75
+ ```bash
76
+ clawchef cook recipes/sample.yaml -s
77
+ ```
78
+
79
+ From-zero OpenClaw bootstrap (recommended):
80
+
81
+ ```bash
82
+ CLAWCHEF_VAR_OPENAI_API_KEY=sk-... clawchef cook recipes/openclaw-from-zero.yaml --verbose
83
+ ```
84
+
85
+ Telegram channel setup only:
86
+
87
+ ```bash
88
+ CLAWCHEF_VAR_TELEGRAM_BOT_TOKEN=123456:abc... clawchef cook recipes/openclaw-telegram.yaml -s
89
+ ```
90
+
91
+ Telegram mock channel setup (for tests):
92
+
93
+ ```bash
94
+ CLAWCHEF_VAR_TELEGRAM_MOCK_API_KEY=test-key clawchef cook recipes/openclaw-telegram-mock.yaml -s
95
+ ```
96
+
97
+ Remote HTTP orchestration:
98
+
99
+ ```bash
100
+ CLAWCHEF_REMOTE_BASE_URL=https://remote-openclaw.example.com \
101
+ CLAWCHEF_REMOTE_API_KEY=secret-token \
102
+ clawchef cook recipes/openclaw-remote-http.yaml --provider remote -s --verbose
103
+ ```
104
+
105
+ Validate recipe structure only:
106
+
107
+ ```bash
108
+ clawchef validate recipes/sample.yaml
109
+ ```
110
+
111
+ Validate recipe from URL:
112
+
113
+ ```bash
114
+ clawchef validate https://example.com/recipes/sample.yaml
115
+ ```
116
+
117
+ Validate recipe in archive:
118
+
119
+ ```bash
120
+ clawchef validate ./bundle.zip
121
+ clawchef validate ./bundle.zip:custom/recipe.yaml
122
+ ```
123
+
124
+ ## Node.js API
125
+
126
+ You can call clawchef directly from Node.js (without invoking CLI commands).
127
+
128
+ ```ts
129
+ import { cook, validate } from "clawchef";
130
+
131
+ await validate("recipes/sample.yaml");
132
+
133
+ await cook("recipes/sample.yaml", {
134
+ provider: "command",
135
+ silent: true,
136
+ vars: {
137
+ openai_api_key: process.env.OPENAI_API_KEY ?? "",
138
+ },
139
+ });
140
+ ```
141
+
142
+ `cook()` options:
143
+
144
+ - `vars`: template variables (`Record<string, string>`)
145
+ - `provider`: `command | remote | mock`
146
+ - `remote`: remote provider config (same fields as CLI remote flags)
147
+ - `dryRun`, `allowMissing`, `verbose`
148
+ - `silent` (default: `true` in Node API)
149
+ - `loadDotEnvFromCwd` (default: `true`)
150
+
151
+ Notes:
152
+
153
+ - `validate()` throws on invalid recipe.
154
+ - `cook()` throws on runtime/configuration errors.
155
+
156
+ ## Variable precedence
157
+
158
+ 1. `--var key=value`
159
+ 2. `CLAWCHEF_VAR_<KEY_IN_UPPERCASE>`
160
+ 3. `params.<key>.default`
161
+
162
+ If `params.<key>.required: true` and no value is found, run fails.
163
+
164
+ If `.env` exists in the directory where `clawchef` is executed, it is loaded before recipe parsing.
165
+
166
+ ## Recipe reference formats
167
+
168
+ `cook` and `validate` accept:
169
+
170
+ - `path/to/recipe.yaml`
171
+ - `path/to/dir` (loads `path/to/dir/recipe.yaml`)
172
+ - `path/to/archive.zip` (loads `recipe.yaml` from extracted archive)
173
+ - `path/to/dir:custom/recipe.yaml`
174
+ - `path/to/archive.tgz:custom/recipe.yaml`
175
+ - `https://host/recipe.yaml`
176
+ - `https://host/archive.zip` (loads `recipe.yaml` from archive)
177
+ - `https://host/archive.zip:custom/recipe.yaml`
178
+
179
+ Supported archives: `.zip`, `.tar`, `.tar.gz`, `.tgz`.
180
+
181
+ ## OpenClaw provider
182
+
183
+ Provider is selected at runtime when running `cook`:
184
+
185
+ - `--provider command` (default)
186
+ - `--provider remote`
187
+ - `--provider mock`
188
+
189
+ `mock` provider is useful for local testing of orchestration and output checks.
190
+
191
+ ### Remote HTTP provider
192
+
193
+ Use `--provider remote` when clawchef cannot run commands on the target machine and must drive configuration via an HTTP endpoint.
194
+
195
+ Required runtime config:
196
+
197
+ - `--remote-base-url` or `CLAWCHEF_REMOTE_BASE_URL`
198
+
199
+ Optional runtime config:
200
+
201
+ - `--remote-api-key` or `CLAWCHEF_REMOTE_API_KEY`
202
+ - `--remote-api-header` or `CLAWCHEF_REMOTE_API_HEADER` (default: `Authorization`)
203
+ - `--remote-api-scheme` or `CLAWCHEF_REMOTE_API_SCHEME` (default: `Bearer`; set empty string to send raw key)
204
+ - `--remote-timeout-ms` or `CLAWCHEF_REMOTE_TIMEOUT_MS` (default: `60000`)
205
+ - `--remote-operation-path` or `CLAWCHEF_REMOTE_OPERATION_PATH` (default: `/v1/clawchef/operation`)
206
+
207
+ Request payload format (POST):
208
+
209
+ ```json
210
+ {
211
+ "operation": "create_workspace",
212
+ "recipe_version": "2026.2.9",
213
+ "payload": {
214
+ "workspace": {
215
+ "name": "demo",
216
+ "path": "/home/runner/.openclaw/workspaces/demo"
217
+ }
218
+ }
219
+ }
220
+ ```
221
+
222
+ Expected response format:
223
+
224
+ ```json
225
+ {
226
+ "ok": true,
227
+ "message": "workspace created",
228
+ "output": "optional agent output",
229
+ "installed_this_run": false
230
+ }
231
+ ```
232
+
233
+ Supported operation values sent by clawchef:
234
+
235
+ - `ensure_version`, `factory_reset`, `start_gateway`
236
+ - `create_workspace`, `create_agent`, `materialize_file`, `install_skill`
237
+ - `configure_channel`, `login_channel`
238
+ - `run_agent`
239
+
240
+ For `run_agent`, clawchef expects `output` in response for assertions.
241
+
242
+ `command` provider now defaults to the current OpenClaw CLI shape (`openclaw 2026.x`), including:
243
+
244
+ - version check: `openclaw --version`
245
+ - reset: `openclaw reset --scope full --yes --non-interactive`
246
+ - workspace prep: `openclaw onboard --non-interactive --accept-risk --mode local --flow quickstart --auth-choice skip --skip-channels --skip-skills --skip-health --skip-ui --skip-daemon --workspace <path>`
247
+ - agent create: `openclaw agents add <name> --workspace <path> --model <model> --non-interactive --json`
248
+ - run turn: `openclaw agent --local --agent <name> --message <prompt> --json`
249
+
250
+ ### Bootstrap config
251
+
252
+ You can provide OpenClaw onboarding and auth parameters under `openclaw.bootstrap`.
253
+ This is used by default workspace creation unless `openclaw.commands.create_workspace` is explicitly overridden.
254
+
255
+ Supported fields include:
256
+
257
+ - onboarding: `mode`, `flow`, `non_interactive`, `accept_risk`, `reset`
258
+ - setup toggles: `skip_channels`, `skip_skills`, `skip_health`, `skip_ui`, `skip_daemon`, `install_daemon`
259
+ - auth/provider: `auth_choice`, `openai_api_key`, `anthropic_api_key`, `openrouter_api_key`, `xai_api_key`, `gemini_api_key`, `ai_gateway_api_key`, `cloudflare_ai_gateway_api_key`, `token`, `token_provider`, `token_profile_id`
260
+
261
+ When `openclaw.bootstrap` contains provider keys, `clawchef` also injects them into runtime env for `openclaw agent --local`.
262
+
263
+ For `command` provider, default command templates are:
264
+
265
+ - `use_version`: `${bin} --version`
266
+ - `install_version`: `npm install -g openclaw@${version}`
267
+ - `uninstall_version`: `npm uninstall -g openclaw`
268
+ - `factory_reset`: `${bin} reset --scope full --yes --non-interactive`
269
+ - `start_gateway`: `${bin} gateway start`
270
+ - `enable_plugin`: `${bin} plugins enable ${channel_q}`
271
+ - `login_channel`: `${bin} channels login --channel ${channel_q}${account_arg}`
272
+ - `create_workspace`: generated from `openclaw.bootstrap` (override with `openclaw.commands.create_workspace`)
273
+ - `create_agent`: `${bin} agents add ${agent} --workspace ${workspace_path} --model ${model} --non-interactive --json`
274
+ - `install_skill`: `${bin} skills check`
275
+ - `send_message`: `true` (messages are staged internally for prompt assembly)
276
+ - `run_agent`: `${bin} agent --local --agent ${agent} --message ${prompt_q} --json`
277
+
278
+ You can override any command under `openclaw.commands` in recipe.
279
+
280
+ ## Channels
281
+
282
+ Use `channels[]` to configure accounts via `openclaw channels add`.
283
+ If `login: true` is set, clawchef runs channel login at the end of `cook` (after gateway start).
284
+
285
+ Example:
286
+
287
+ ```yaml
288
+ channels:
289
+ - channel: "telegram"
290
+ token: "${telegram_bot_token}"
291
+ account: "default"
292
+ login: true
293
+
294
+ - channel: "slack"
295
+ bot_token: "${slack_bot_token}"
296
+ app_token: "${slack_app_token}"
297
+ name: "team-workspace"
298
+
299
+ - channel: "discord"
300
+ token_file: "${discord_token_file}"
301
+ extra_flags:
302
+ webhook_path: "/discord/webhook"
303
+ ```
304
+
305
+ Supported common fields:
306
+
307
+ - required: `channel`
308
+ - optional: `account`, `name`, `token`, `token_file`, `use_env`, `bot_token`, `access_token`, `app_token`, `webhook_url`, `webhook_path`, `signal_number`, `password`, `login`, `login_mode`, `login_account`
309
+ - advanced passthrough: `extra_flags` (`snake_case` keys become `--kebab-case` CLI flags)
310
+
311
+ ### Telegram mock channel (for recipe tests)
312
+
313
+ Use `channel: "telegram-mock"` when you need to test Telegram-related recipe flows without connecting to the real Telegram network.
314
+
315
+ `clawchef` treats `telegram-mock` like any other channel and passes mock-specific flags through `extra_flags`.
316
+
317
+ Example:
318
+
319
+ ```yaml
320
+ params:
321
+ telegram_mock_api_key:
322
+ required: true
323
+
324
+ channels:
325
+ - channel: "telegram-mock"
326
+ account: "testbot"
327
+ token: "${telegram_mock_api_key}"
328
+ extra_flags:
329
+ mock_bind: "127.0.0.1:18790"
330
+ mock_api_key: "${telegram_mock_api_key}"
331
+ mode: "webhook"
332
+ ```
333
+
334
+ Typical test setup:
335
+
336
+ - Start OpenClaw with the telegram-mock plugin enabled.
337
+ - Run `clawchef cook ...` to configure workspaces/agents/channels.
338
+ - Use your external test program (HTTP API or Node.js SDK) to inject inbound mock messages and assert outbound events.
339
+
340
+ Login fields:
341
+
342
+ - `login: true` enables channel login step
343
+ - `login_mode`: currently supports `interactive`
344
+ - `login_account`: override account used for login (defaults to `account`)
345
+
346
+ Security rules:
347
+
348
+ - Do not inline secret values in `channels[]`.
349
+ - Use `${var}` placeholders and inject values via `--var` / `CLAWCHEF_VAR_*`.
350
+
351
+ ## Workspace path behavior
352
+
353
+ - `workspaces[].path` is optional.
354
+ - If omitted, clawchef uses `~/.openclaw/workspaces/<workspace-name>`.
355
+ - If provided, relative paths are resolved from the recipe file directory.
356
+ - For direct URL recipe files, relative workspace paths are resolved from the current working directory.
357
+ - For directory/archive recipe references, relative workspace paths are resolved from the selected recipe file directory.
358
+
359
+ ## File content references
360
+
361
+ In `files[]`, set exactly one of:
362
+
363
+ - `content`: inline text in recipe
364
+ - `content_from`: load text from another file/URL
365
+ - `source`: copy raw file bytes from another file/URL
366
+
367
+ `content_from` and `source` accept:
368
+
369
+ - path relative to recipe file location
370
+ - absolute filesystem path
371
+ - HTTP/HTTPS URL
372
+
373
+ Useful placeholders when overriding commands:
374
+
375
+ - common: `${bin}`, `${version}`, `${workspace}`, `${agent}`, `${channel}`, `${channel_q}`, `${account}`, `${account_q}`, `${account_arg}`
376
+ - paths: `${path}` / `${workspace_path}` (shell-quoted), `${path_raw}` / `${workspace_path_raw}` (raw)
377
+ - message: `${content}`, `${content_q}`, `${message_file}`, `${message_file_raw}` (`${role}` is always `user`)
378
+ - run prompt: `${prompt}`, `${prompt_q}`
379
+
380
+ ## Secret handling
381
+
382
+ - Do not put plaintext API keys/tokens in recipe files.
383
+ - Use `${var}` placeholders in recipe and pass values via:
384
+ - `--var openai_api_key=...`
385
+ - `CLAWCHEF_VAR_OPENAI_API_KEY=...`
386
+ - Inline secrets in `openclaw.bootstrap.*` are rejected by validation.
387
+
388
+ ## Conversation message format
389
+
390
+ `conversations[].messages[]` uses:
391
+
392
+ - `content` (required): the user message sent to agent
393
+ - `expect` (optional): output assertion for that message
394
+
395
+ When `conversations[].run: true`, each message triggers one agent run.
396
+ If `run` is omitted, a message still triggers a run when it has `expect`.
397
+
398
+ `expect` supports:
399
+
400
+ - `contains: ["..."]`
401
+ - `not_contains: ["..."]`
402
+ - `regex: ["..."]`
403
+ - `equals: "..."`
404
+
405
+ All configured assertions must pass.
package/dist/api.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ import type { OpenClawProvider, OpenClawRemoteConfig } from "./types.js";
2
+ export interface CookOptions {
3
+ vars?: Record<string, string>;
4
+ dryRun?: boolean;
5
+ allowMissing?: boolean;
6
+ verbose?: boolean;
7
+ silent?: boolean;
8
+ provider?: OpenClawProvider;
9
+ remote?: Partial<OpenClawRemoteConfig>;
10
+ loadDotEnvFromCwd?: boolean;
11
+ }
12
+ export declare function cook(recipeRef: string, options?: CookOptions): Promise<void>;
13
+ export declare function validate(recipeRef: string): Promise<void>;
14
+ export type { OpenClawProvider, OpenClawRemoteConfig };
package/dist/api.js ADDED
@@ -0,0 +1,49 @@
1
+ import YAML from "js-yaml";
2
+ import { ClawChefError } from "./errors.js";
3
+ import { importDotEnvFromCwd } from "./env.js";
4
+ import { Logger } from "./logger.js";
5
+ import { runRecipe } from "./orchestrator.js";
6
+ import { loadRecipe, loadRecipeText } from "./recipe.js";
7
+ import { recipeSchema } from "./schema.js";
8
+ function normalizeCookOptions(options) {
9
+ return {
10
+ vars: options.vars ?? {},
11
+ dryRun: Boolean(options.dryRun),
12
+ allowMissing: Boolean(options.allowMissing),
13
+ verbose: Boolean(options.verbose),
14
+ silent: options.silent ?? true,
15
+ provider: options.provider ?? "command",
16
+ remote: options.remote ?? {},
17
+ };
18
+ }
19
+ export async function cook(recipeRef, options = {}) {
20
+ if (options.loadDotEnvFromCwd ?? true) {
21
+ importDotEnvFromCwd();
22
+ }
23
+ const runOptions = normalizeCookOptions(options);
24
+ const logger = new Logger(runOptions.verbose);
25
+ const loaded = await loadRecipe(recipeRef, runOptions);
26
+ try {
27
+ await runRecipe(loaded.recipe, loaded.origin, runOptions, logger);
28
+ }
29
+ finally {
30
+ if (loaded.cleanup) {
31
+ await loaded.cleanup();
32
+ }
33
+ }
34
+ }
35
+ export async function validate(recipeRef) {
36
+ const loaded = await loadRecipeText(recipeRef);
37
+ try {
38
+ const raw = YAML.load(loaded.source);
39
+ const result = recipeSchema.safeParse(raw);
40
+ if (!result.success) {
41
+ throw new ClawChefError(`Validation failed: ${result.error.message}`);
42
+ }
43
+ }
44
+ finally {
45
+ if (loaded.cleanup) {
46
+ await loaded.cleanup();
47
+ }
48
+ }
49
+ }
@@ -0,0 +1,2 @@
1
+ import type { ConversationExpectDef } from "./types.js";
2
+ export declare function validateReply(reply: string, expect?: ConversationExpectDef): void;
@@ -0,0 +1,32 @@
1
+ import { ClawChefError } from "./errors.js";
2
+ export function validateReply(reply, expect) {
3
+ if (!expect) {
4
+ return;
5
+ }
6
+ for (const text of expect.contains ?? []) {
7
+ if (!reply.includes(text)) {
8
+ throw new ClawChefError(`Output assertion failed: must contain -> ${text}`);
9
+ }
10
+ }
11
+ for (const text of expect.not_contains ?? []) {
12
+ if (reply.includes(text)) {
13
+ throw new ClawChefError(`Output assertion failed: must not contain -> ${text}`);
14
+ }
15
+ }
16
+ for (const pattern of expect.regex ?? []) {
17
+ let re;
18
+ try {
19
+ re = new RegExp(pattern, "m");
20
+ }
21
+ catch (err) {
22
+ const msg = err instanceof Error ? err.message : String(err);
23
+ throw new ClawChefError(`Output assertion failed: invalid regex ${pattern} (${msg})`);
24
+ }
25
+ if (!re.test(reply)) {
26
+ throw new ClawChefError(`Output assertion failed: regex mismatch -> ${pattern}`);
27
+ }
28
+ }
29
+ if (expect.equals !== undefined && reply !== expect.equals) {
30
+ throw new ClawChefError("Output assertion failed: equals mismatch");
31
+ }
32
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function buildCli(): Command;
package/dist/cli.js ADDED
@@ -0,0 +1,115 @@
1
+ import { Command } from "commander";
2
+ import { ClawChefError } from "./errors.js";
3
+ import { Logger } from "./logger.js";
4
+ import { runRecipe } from "./orchestrator.js";
5
+ import { loadRecipe, loadRecipeText } from "./recipe.js";
6
+ import { recipeSchema } from "./schema.js";
7
+ import YAML from "js-yaml";
8
+ function parseVarFlags(values) {
9
+ const out = {};
10
+ for (const item of values) {
11
+ const idx = item.indexOf("=");
12
+ if (idx <= 0 || idx === item.length - 1) {
13
+ throw new ClawChefError(`Invalid --var format: ${item}. Expected key=value`);
14
+ }
15
+ const k = item.slice(0, idx).trim();
16
+ const v = item.slice(idx + 1).trim();
17
+ out[k] = v;
18
+ }
19
+ return out;
20
+ }
21
+ function readEnv(name) {
22
+ const value = process.env[name];
23
+ if (value === undefined) {
24
+ return undefined;
25
+ }
26
+ const trimmed = value.trim();
27
+ return trimmed.length > 0 ? trimmed : undefined;
28
+ }
29
+ function parseProvider(value) {
30
+ if (value === "command" || value === "mock" || value === "remote") {
31
+ return value;
32
+ }
33
+ throw new ClawChefError(`Invalid --provider value: ${value}. Expected command, remote, or mock`);
34
+ }
35
+ function parseOptionalInt(value, fieldName) {
36
+ if (value === undefined) {
37
+ return undefined;
38
+ }
39
+ const parsed = Number(value);
40
+ if (!Number.isInteger(parsed) || parsed <= 0) {
41
+ throw new ClawChefError(`${fieldName} must be a positive integer`);
42
+ }
43
+ return parsed;
44
+ }
45
+ export function buildCli() {
46
+ const program = new Command();
47
+ program
48
+ .name("clawchef")
49
+ .description("Run OpenClaw environment recipes")
50
+ .version("0.1.0");
51
+ program
52
+ .command("cook")
53
+ .argument("<recipe>", "Recipe path/URL/dir/archive[:file]")
54
+ .option("--var <key=value>", "Template variable", (v, p) => p.concat([v]), [])
55
+ .option("--dry-run", "Print actions without executing", false)
56
+ .option("--allow-missing", "Allow unresolved template variables", false)
57
+ .option("--verbose", "Verbose logging", false)
58
+ .option("-s, --silent", "Skip reset confirmation prompt", false)
59
+ .option("--provider <provider>", "Execution provider: command | remote | mock")
60
+ .option("--remote-base-url <url>", "Remote OpenClaw API base URL")
61
+ .option("--remote-api-key <key>", "Remote OpenClaw API key")
62
+ .option("--remote-api-header <header>", "Remote auth header name")
63
+ .option("--remote-api-scheme <scheme>", "Remote auth scheme (default: Bearer)")
64
+ .option("--remote-timeout-ms <ms>", "Remote operation timeout in milliseconds")
65
+ .option("--remote-operation-path <path>", "Remote operation endpoint path")
66
+ .action(async (recipeRef, opts) => {
67
+ const provider = parseProvider(opts.provider ?? readEnv("CLAWCHEF_PROVIDER") ?? "command");
68
+ const options = {
69
+ vars: parseVarFlags(opts.var),
70
+ dryRun: Boolean(opts.dryRun),
71
+ allowMissing: Boolean(opts.allowMissing),
72
+ verbose: Boolean(opts.verbose),
73
+ silent: Boolean(opts.silent),
74
+ provider,
75
+ remote: {
76
+ base_url: opts.remoteBaseUrl ?? readEnv("CLAWCHEF_REMOTE_BASE_URL"),
77
+ api_key: opts.remoteApiKey ?? readEnv("CLAWCHEF_REMOTE_API_KEY"),
78
+ api_header: opts.remoteApiHeader ?? readEnv("CLAWCHEF_REMOTE_API_HEADER"),
79
+ api_scheme: opts.remoteApiScheme ?? readEnv("CLAWCHEF_REMOTE_API_SCHEME"),
80
+ timeout_ms: parseOptionalInt(opts.remoteTimeoutMs ?? readEnv("CLAWCHEF_REMOTE_TIMEOUT_MS"), "remote-timeout-ms"),
81
+ operation_path: opts.remoteOperationPath ?? readEnv("CLAWCHEF_REMOTE_OPERATION_PATH"),
82
+ },
83
+ };
84
+ const logger = new Logger(options.verbose);
85
+ const loaded = await loadRecipe(recipeRef, options);
86
+ try {
87
+ await runRecipe(loaded.recipe, loaded.origin, options, logger);
88
+ }
89
+ finally {
90
+ if (loaded.cleanup) {
91
+ await loaded.cleanup();
92
+ }
93
+ }
94
+ });
95
+ program
96
+ .command("validate")
97
+ .argument("<recipe>", "Recipe path/URL/dir/archive[:file]")
98
+ .action(async (recipeRef) => {
99
+ const loaded = await loadRecipeText(recipeRef);
100
+ try {
101
+ const raw = YAML.load(loaded.source);
102
+ const result = recipeSchema.safeParse(raw);
103
+ if (!result.success) {
104
+ throw new ClawChefError(`Validation failed: ${result.error.message}`);
105
+ }
106
+ process.stdout.write("Recipe structure is valid\n");
107
+ }
108
+ finally {
109
+ if (loaded.cleanup) {
110
+ await loaded.cleanup();
111
+ }
112
+ }
113
+ });
114
+ return program;
115
+ }
package/dist/env.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function importDotEnvFromCwd(): void;
package/dist/env.js ADDED
@@ -0,0 +1,14 @@
1
+ import { existsSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { config as loadDotenv } from "dotenv";
4
+ import { ClawChefError } from "./errors.js";
5
+ export function importDotEnvFromCwd() {
6
+ const envPath = path.resolve(process.cwd(), ".env");
7
+ if (!existsSync(envPath)) {
8
+ return;
9
+ }
10
+ const result = loadDotenv({ path: envPath, override: false });
11
+ if (result.error) {
12
+ throw new ClawChefError(`Failed to load .env from current directory: ${result.error.message}`);
13
+ }
14
+ }
@@ -0,0 +1,3 @@
1
+ export declare class ClawChefError extends Error {
2
+ constructor(message: string);
3
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,6 @@
1
+ export class ClawChefError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = "ClawChefError";
5
+ }
6
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ import { buildCli } from "./cli.js";
3
+ import { importDotEnvFromCwd } from "./env.js";
4
+ import { ClawChefError } from "./errors.js";
5
+ async function main() {
6
+ importDotEnvFromCwd();
7
+ const program = buildCli();
8
+ await program.parseAsync(process.argv);
9
+ }
10
+ main().catch((err) => {
11
+ if (err instanceof ClawChefError) {
12
+ process.stderr.write(`[ERROR] ${err.message}\n`);
13
+ process.exit(1);
14
+ }
15
+ const msg = err instanceof Error ? err.stack ?? err.message : String(err);
16
+ process.stderr.write(`[FATAL] ${msg}\n`);
17
+ process.exit(1);
18
+ });
@@ -0,0 +1,7 @@
1
+ export declare class Logger {
2
+ private readonly verboseEnabled;
3
+ constructor(verboseEnabled: boolean);
4
+ info(message: string): void;
5
+ warn(message: string): void;
6
+ debug(message: string): void;
7
+ }