clawchef 0.1.6 → 0.1.7
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 +20 -12
- package/dist/api.d.ts +3 -1
- package/dist/api.js +10 -1
- package/dist/cli.js +34 -3
- package/dist/openclaw/command-provider.d.ts +1 -1
- package/dist/openclaw/command-provider.js +31 -23
- package/dist/openclaw/mock-provider.d.ts +1 -1
- package/dist/openclaw/mock-provider.js +1 -1
- package/dist/openclaw/provider.d.ts +1 -1
- package/dist/openclaw/remote-provider.d.ts +1 -1
- package/dist/openclaw/remote-provider.js +1 -1
- package/dist/orchestrator.js +74 -50
- package/dist/recipe.js +57 -24
- package/dist/schema.d.ts +72 -107
- package/dist/schema.js +15 -21
- package/dist/types.d.ts +6 -11
- package/package.json +1 -1
- package/recipes/content-from-sample.yaml +13 -9
- package/recipes/openclaw-from-zero.yaml +2 -2
- package/recipes/openclaw-local.yaml +8 -10
- package/recipes/openclaw-remote-http.yaml +6 -8
- package/recipes/sample.yaml +6 -8
- package/recipes/snippets/readme-template.md +3 -1
- package/src/api.ts +13 -2
- package/src/cli.ts +36 -4
- package/src/openclaw/command-provider.ts +34 -24
- package/src/openclaw/mock-provider.ts +1 -1
- package/src/openclaw/provider.ts +1 -1
- package/src/openclaw/remote-provider.ts +1 -1
- package/src/orchestrator.ts +74 -49
- package/src/recipe.ts +63 -24
- package/src/schema.ts +17 -22
- package/src/types.ts +6 -11
package/README.md
CHANGED
|
@@ -12,7 +12,8 @@ Recipe-driven OpenClaw environment orchestrator.
|
|
|
12
12
|
- Requires secrets to be injected via `--var` / `CLAWCHEF_VAR_*` (no inline secrets in recipe).
|
|
13
13
|
- Prepares OpenClaw version (install or reuse).
|
|
14
14
|
- When installed OpenClaw version mismatches recipe version, prompts: ignore / abort / force reinstall (silent mode auto-picks force reinstall).
|
|
15
|
-
-
|
|
15
|
+
- Supports scoped execution via `--scope full|files|workspace`.
|
|
16
|
+
- `full` scope runs factory reset first (with confirmation prompt unless `-s/--silent` is used).
|
|
16
17
|
- If `openclaw` is missing, auto-installs the recipe version and skips factory reset.
|
|
17
18
|
- Starts OpenClaw gateway service after each recipe execution.
|
|
18
19
|
- Creates workspaces and agents (default workspace path: `~/.openclaw/workspace-<workspace-name>`).
|
|
@@ -20,7 +21,6 @@ Recipe-driven OpenClaw environment orchestrator.
|
|
|
20
21
|
- Materializes files into target workspaces.
|
|
21
22
|
- Installs skills.
|
|
22
23
|
- Supports plugin preinstall via `openclaw.plugins[]` and runtime `--plugin` flags.
|
|
23
|
-
- Supports preserving existing OpenClaw state via `--keep-openclaw-state` (skip factory reset).
|
|
24
24
|
- Configures channels with `openclaw channels add`.
|
|
25
25
|
- Supports interactive channel login at the end of execution (`channels[].login: true`) for channels that expose login.
|
|
26
26
|
- Supports remote HTTP orchestration via runtime flags (`--provider remote`) when OpenClaw is reachable via API.
|
|
@@ -96,13 +96,19 @@ Use it only in CI/non-interactive flows where destructive reset behavior is expe
|
|
|
96
96
|
Keep existing OpenClaw state (skip reset and keep current version on mismatch):
|
|
97
97
|
|
|
98
98
|
```bash
|
|
99
|
-
clawchef cook recipes/sample.yaml --
|
|
99
|
+
clawchef cook recipes/sample.yaml --scope files
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Update only one workspace:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
clawchef cook recipes/sample.yaml --scope workspace --workspace workspace-meeting
|
|
100
106
|
```
|
|
101
107
|
|
|
102
108
|
From-zero OpenClaw bootstrap (recommended):
|
|
103
109
|
|
|
104
110
|
```bash
|
|
105
|
-
|
|
111
|
+
CLAWCHEF_VAR_LLM_API_KEY=sk-... clawchef cook recipes/openclaw-from-zero.yaml --verbose
|
|
106
112
|
```
|
|
107
113
|
|
|
108
114
|
Telegram channel setup only:
|
|
@@ -183,7 +189,7 @@ await cook("recipes/sample.yaml", {
|
|
|
183
189
|
provider: "command",
|
|
184
190
|
silent: true,
|
|
185
191
|
vars: {
|
|
186
|
-
|
|
192
|
+
llm_api_key: process.env.OPENAI_API_KEY ?? "",
|
|
187
193
|
},
|
|
188
194
|
});
|
|
189
195
|
|
|
@@ -196,6 +202,8 @@ await scaffold("./my-recipe-project", {
|
|
|
196
202
|
|
|
197
203
|
- `vars`: template variables (`Record<string, string>`)
|
|
198
204
|
- `plugins`: plugin npm specs to preinstall for this run (`string[]`)
|
|
205
|
+
- `scope`: `full | files | workspace` (default: `full`)
|
|
206
|
+
- `workspaceName`: required when `scope: "workspace"`
|
|
199
207
|
- `provider`: `command | remote | mock`
|
|
200
208
|
- `remote`: remote provider config (same fields as CLI remote flags)
|
|
201
209
|
- `envFile`: custom env file path/URL; when set, default cwd `.env` loading is skipped
|
|
@@ -318,7 +326,7 @@ Supported fields include:
|
|
|
318
326
|
|
|
319
327
|
- onboarding: `mode`, `flow`, `non_interactive`, `accept_risk`, `reset`
|
|
320
328
|
- setup toggles: `skip_channels`, `skip_skills`, `skip_health`, `skip_ui`, `skip_daemon`, `install_daemon`
|
|
321
|
-
- auth/provider: `auth_choice`, `
|
|
329
|
+
- auth/provider: `auth_choice`, `llm_api_key`, `token`, `token_provider`, `token_profile_id`
|
|
322
330
|
|
|
323
331
|
### Plugin preinstall
|
|
324
332
|
|
|
@@ -334,7 +342,7 @@ openclaw:
|
|
|
334
342
|
- "@scope/custom-channel-plugin@1.2.3"
|
|
335
343
|
```
|
|
336
344
|
|
|
337
|
-
When `openclaw.bootstrap` contains
|
|
345
|
+
When `openclaw.bootstrap` contains `llm_api_key`, clawchef maps it to the provider-specific runtime env by `auth_choice` for `openclaw agent --local`.
|
|
338
346
|
|
|
339
347
|
For `command` provider, default command templates are:
|
|
340
348
|
|
|
@@ -394,7 +402,7 @@ Supported common fields:
|
|
|
394
402
|
- `workspaces[].assets` is optional.
|
|
395
403
|
- If `assets` is set, clawchef recursively copies files from that directory into the workspace root.
|
|
396
404
|
- `assets` is resolved relative to the recipe file path (unless absolute path is given).
|
|
397
|
-
- `files[]` runs after assets copy, so
|
|
405
|
+
- `workspaces[].files[]` runs after assets copy, so explicit file entries can override copied asset files.
|
|
398
406
|
- Direct URL recipes do not support `workspaces[].assets` (assets must resolve to a local directory).
|
|
399
407
|
- If provided, relative paths are resolved from the recipe file directory.
|
|
400
408
|
- For direct URL recipe files, relative workspace paths are resolved from the current working directory.
|
|
@@ -410,10 +418,10 @@ workspaces:
|
|
|
410
418
|
|
|
411
419
|
## File content references
|
|
412
420
|
|
|
413
|
-
In `files[]`, set exactly one of:
|
|
421
|
+
In `workspaces[].files[]`, set exactly one of:
|
|
414
422
|
|
|
415
423
|
- `content`: inline text in recipe
|
|
416
|
-
- `content_from`: load text from another file/URL
|
|
424
|
+
- `content_from`: load text from another file/URL (loaded content supports `${var}` template rendering)
|
|
417
425
|
- `source`: copy raw file bytes from another file/URL
|
|
418
426
|
|
|
419
427
|
`content_from` and `source` accept:
|
|
@@ -433,8 +441,8 @@ Useful placeholders when overriding commands:
|
|
|
433
441
|
|
|
434
442
|
- Do not put plaintext API keys/tokens in recipe files.
|
|
435
443
|
- Use `${var}` placeholders in recipe and pass values via:
|
|
436
|
-
- `--var
|
|
437
|
-
- `
|
|
444
|
+
- `--var llm_api_key=...`
|
|
445
|
+
- `CLAWCHEF_VAR_LLM_API_KEY=...`
|
|
438
446
|
- Inline secrets in `openclaw.bootstrap.*` are rejected by validation.
|
|
439
447
|
|
|
440
448
|
## Conversation message format
|
package/dist/api.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { OpenClawProvider, OpenClawRemoteConfig } from "./types.js";
|
|
1
|
+
import type { OpenClawProvider, OpenClawRemoteConfig, RunScope } from "./types.js";
|
|
2
2
|
import type { ScaffoldOptions, ScaffoldResult } from "./scaffold.js";
|
|
3
3
|
export interface CookOptions {
|
|
4
4
|
vars?: Record<string, string>;
|
|
@@ -7,6 +7,8 @@ export interface CookOptions {
|
|
|
7
7
|
allowMissing?: boolean;
|
|
8
8
|
verbose?: boolean;
|
|
9
9
|
silent?: boolean;
|
|
10
|
+
scope?: RunScope;
|
|
11
|
+
workspaceName?: string;
|
|
10
12
|
provider?: OpenClawProvider;
|
|
11
13
|
remote?: Partial<OpenClawRemoteConfig>;
|
|
12
14
|
envFile?: string;
|
package/dist/api.js
CHANGED
|
@@ -8,14 +8,23 @@ import { scaffoldProject } from "./scaffold.js";
|
|
|
8
8
|
import { recipeSchema } from "./schema.js";
|
|
9
9
|
function normalizeCookOptions(options) {
|
|
10
10
|
const plugins = Array.from(new Set((options.plugins ?? []).map((value) => value.trim()).filter((value) => value.length > 0)));
|
|
11
|
+
const scope = options.scope ?? "full";
|
|
12
|
+
const workspaceName = options.workspaceName?.trim() || undefined;
|
|
13
|
+
if (scope === "workspace" && !workspaceName) {
|
|
14
|
+
throw new ClawChefError("scope=workspace requires workspaceName");
|
|
15
|
+
}
|
|
16
|
+
if (scope !== "workspace" && workspaceName) {
|
|
17
|
+
throw new ClawChefError("workspaceName is only allowed when scope=workspace");
|
|
18
|
+
}
|
|
11
19
|
return {
|
|
12
20
|
vars: options.vars ?? {},
|
|
13
21
|
plugins,
|
|
22
|
+
scope,
|
|
23
|
+
workspaceName,
|
|
14
24
|
dryRun: Boolean(options.dryRun),
|
|
15
25
|
allowMissing: Boolean(options.allowMissing),
|
|
16
26
|
verbose: Boolean(options.verbose),
|
|
17
27
|
silent: options.silent ?? true,
|
|
18
|
-
keepOpenClawState: false,
|
|
19
28
|
provider: options.provider ?? "command",
|
|
20
29
|
remote: options.remote ?? {},
|
|
21
30
|
};
|
package/dist/cli.js
CHANGED
|
@@ -8,8 +8,23 @@ import { recipeSchema } from "./schema.js";
|
|
|
8
8
|
import { scaffoldProject } from "./scaffold.js";
|
|
9
9
|
import YAML from "js-yaml";
|
|
10
10
|
import path from "node:path";
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
11
12
|
import { createInterface } from "node:readline/promises";
|
|
12
13
|
import { stdin as input, stdout as output } from "node:process";
|
|
14
|
+
function readPackageVersion() {
|
|
15
|
+
try {
|
|
16
|
+
const pkgPath = new URL("../package.json", import.meta.url);
|
|
17
|
+
const content = readFileSync(pkgPath, "utf8");
|
|
18
|
+
const parsed = JSON.parse(content);
|
|
19
|
+
if (parsed.version?.trim()) {
|
|
20
|
+
return parsed.version;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// ignore and use fallback
|
|
25
|
+
}
|
|
26
|
+
return "0.0.0";
|
|
27
|
+
}
|
|
13
28
|
function parseVarFlags(values) {
|
|
14
29
|
const out = {};
|
|
15
30
|
for (const item of values) {
|
|
@@ -40,6 +55,12 @@ function parseProvider(value) {
|
|
|
40
55
|
}
|
|
41
56
|
throw new ClawChefError(`Invalid --provider value: ${value}. Expected command, remote, or mock`);
|
|
42
57
|
}
|
|
58
|
+
function parseScope(value) {
|
|
59
|
+
if (value === "full" || value === "files" || value === "workspace") {
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
throw new ClawChefError(`Invalid --scope value: ${value}. Expected full, files, or workspace`);
|
|
63
|
+
}
|
|
43
64
|
function parseOptionalInt(value, fieldName) {
|
|
44
65
|
if (value === undefined) {
|
|
45
66
|
return undefined;
|
|
@@ -69,7 +90,7 @@ export function buildCli() {
|
|
|
69
90
|
program
|
|
70
91
|
.name("clawchef")
|
|
71
92
|
.description("Run OpenClaw environment recipes")
|
|
72
|
-
.version(
|
|
93
|
+
.version(readPackageVersion());
|
|
73
94
|
program
|
|
74
95
|
.command("cook")
|
|
75
96
|
.argument("<recipe>", "Recipe path/URL/dir/archive[:file]")
|
|
@@ -78,7 +99,8 @@ export function buildCli() {
|
|
|
78
99
|
.option("--allow-missing", "Allow unresolved template variables", false)
|
|
79
100
|
.option("--verbose", "Verbose logging", false)
|
|
80
101
|
.option("-s, --silent", "Skip reset confirmation prompt", false)
|
|
81
|
-
.option("--
|
|
102
|
+
.option("--scope <scope>", "Run scope: full | files | workspace", "full")
|
|
103
|
+
.option("--workspace <name>", "Workspace name (required when --scope workspace)")
|
|
82
104
|
.option("--dotenv-ref <path-or-url>", "Load env vars from local file or HTTP URL")
|
|
83
105
|
.option("--provider <provider>", "Execution provider: command | remote | mock")
|
|
84
106
|
.option("--plugin <npm-spec>", "Preinstall plugin package (repeatable)", (v, p) => p.concat([v]), [])
|
|
@@ -96,14 +118,23 @@ export function buildCli() {
|
|
|
96
118
|
importDotEnvFromCwd();
|
|
97
119
|
}
|
|
98
120
|
const provider = parseProvider(opts.provider ?? readEnv("CLAWCHEF_PROVIDER") ?? "command");
|
|
121
|
+
const scope = parseScope(String(opts.scope ?? "full"));
|
|
122
|
+
const workspaceName = opts.workspace?.trim() ? String(opts.workspace).trim() : undefined;
|
|
123
|
+
if (scope === "workspace" && !workspaceName) {
|
|
124
|
+
throw new ClawChefError("--scope workspace requires --workspace <name>");
|
|
125
|
+
}
|
|
126
|
+
if (scope !== "workspace" && workspaceName) {
|
|
127
|
+
throw new ClawChefError("--workspace is only allowed when --scope workspace");
|
|
128
|
+
}
|
|
99
129
|
const options = {
|
|
100
130
|
vars: parseVarFlags(opts.var),
|
|
101
131
|
plugins: parsePluginFlags(opts.plugin),
|
|
132
|
+
scope,
|
|
133
|
+
workspaceName,
|
|
102
134
|
dryRun: Boolean(opts.dryRun),
|
|
103
135
|
allowMissing: Boolean(opts.allowMissing),
|
|
104
136
|
verbose: Boolean(opts.verbose),
|
|
105
137
|
silent: Boolean(opts.silent),
|
|
106
|
-
keepOpenClawState: Boolean(opts.keepOpenclawState),
|
|
107
138
|
provider,
|
|
108
139
|
remote: {
|
|
109
140
|
base_url: opts.remoteBaseUrl ?? readEnv("CLAWCHEF_REMOTE_BASE_URL"),
|
|
@@ -2,7 +2,7 @@ import type { AgentDef, ChannelDef, ConversationDef, OpenClawSection } from "../
|
|
|
2
2
|
import type { EnsureVersionResult, OpenClawProvider, ResolvedWorkspaceDef } from "./provider.js";
|
|
3
3
|
export declare class CommandOpenClawProvider implements OpenClawProvider {
|
|
4
4
|
private readonly stagedMessages;
|
|
5
|
-
ensureVersion(config: OpenClawSection, dryRun: boolean, silent: boolean,
|
|
5
|
+
ensureVersion(config: OpenClawSection, dryRun: boolean, silent: boolean, preserveExistingState: boolean): Promise<EnsureVersionResult>;
|
|
6
6
|
factoryReset(config: OpenClawSection, dryRun: boolean): Promise<void>;
|
|
7
7
|
installPlugin(config: OpenClawSection, pluginSpec: string, dryRun: boolean): Promise<void>;
|
|
8
8
|
startGateway(config: OpenClawSection, dryRun: boolean): Promise<void>;
|
|
@@ -21,14 +21,25 @@ const DEFAULT_COMMANDS = {
|
|
|
21
21
|
run_agent: "${bin} agent --local --agent ${agent} --message ${prompt_q} --json",
|
|
22
22
|
};
|
|
23
23
|
const SECRET_FLAG_RE = /(--[A-Za-z0-9-]*(?:api-key|token|password|secret)[A-Za-z0-9-]*\s+)(?:'[^']*'|"[^"]*"|\S+)/g;
|
|
24
|
+
const AUTH_CHOICE_TO_LLM_FLAG = {
|
|
25
|
+
"openai-api-key": "--openai-api-key",
|
|
26
|
+
"anthropic-api-key": "--anthropic-api-key",
|
|
27
|
+
"openrouter-api-key": "--openrouter-api-key",
|
|
28
|
+
"xai-api-key": "--xai-api-key",
|
|
29
|
+
"gemini-api-key": "--gemini-api-key",
|
|
30
|
+
"ai-gateway-api-key": "--ai-gateway-api-key",
|
|
31
|
+
"cloudflare-ai-gateway-api-key": "--cloudflare-ai-gateway-api-key",
|
|
32
|
+
};
|
|
33
|
+
const AUTH_CHOICE_TO_LLM_ENV = {
|
|
34
|
+
"openai-api-key": "OPENAI_API_KEY",
|
|
35
|
+
"anthropic-api-key": "ANTHROPIC_API_KEY",
|
|
36
|
+
"openrouter-api-key": "OPENROUTER_API_KEY",
|
|
37
|
+
"xai-api-key": "XAI_API_KEY",
|
|
38
|
+
"gemini-api-key": "GEMINI_API_KEY",
|
|
39
|
+
"ai-gateway-api-key": "AI_GATEWAY_API_KEY",
|
|
40
|
+
"cloudflare-ai-gateway-api-key": "CLOUDFLARE_AI_GATEWAY_API_KEY",
|
|
41
|
+
};
|
|
24
42
|
const BOOTSTRAP_STRING_FLAGS = [
|
|
25
|
-
["openai_api_key", "--openai-api-key"],
|
|
26
|
-
["anthropic_api_key", "--anthropic-api-key"],
|
|
27
|
-
["openrouter_api_key", "--openrouter-api-key"],
|
|
28
|
-
["xai_api_key", "--xai-api-key"],
|
|
29
|
-
["gemini_api_key", "--gemini-api-key"],
|
|
30
|
-
["ai_gateway_api_key", "--ai-gateway-api-key"],
|
|
31
|
-
["cloudflare_ai_gateway_api_key", "--cloudflare-ai-gateway-api-key"],
|
|
32
43
|
["cloudflare_ai_gateway_account_id", "--cloudflare-ai-gateway-account-id"],
|
|
33
44
|
["cloudflare_ai_gateway_gateway_id", "--cloudflare-ai-gateway-gateway-id"],
|
|
34
45
|
["token", "--token"],
|
|
@@ -187,6 +198,12 @@ function buildBootstrapCommand(bin, bootstrap, workspacePath) {
|
|
|
187
198
|
else if (cfg.install_daemon === false) {
|
|
188
199
|
flags.push("--no-install-daemon");
|
|
189
200
|
}
|
|
201
|
+
if (cfg.llm_api_key?.trim()) {
|
|
202
|
+
const llmFlag = AUTH_CHOICE_TO_LLM_FLAG[cfg.auth_choice ?? ""];
|
|
203
|
+
if (llmFlag) {
|
|
204
|
+
flags.push(`${llmFlag} ${shellQuote(cfg.llm_api_key)}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
190
207
|
for (const [field, flag] of BOOTSTRAP_STRING_FLAGS) {
|
|
191
208
|
const value = cfg[field];
|
|
192
209
|
if (value && value.trim()) {
|
|
@@ -202,26 +219,17 @@ function bootstrapRuntimeEnv(bootstrap) {
|
|
|
202
219
|
return {};
|
|
203
220
|
}
|
|
204
221
|
const env = {};
|
|
205
|
-
if (bootstrap.
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
env.OPENROUTER_API_KEY = bootstrap.openrouter_api_key;
|
|
211
|
-
if (bootstrap.xai_api_key)
|
|
212
|
-
env.XAI_API_KEY = bootstrap.xai_api_key;
|
|
213
|
-
if (bootstrap.gemini_api_key)
|
|
214
|
-
env.GEMINI_API_KEY = bootstrap.gemini_api_key;
|
|
215
|
-
if (bootstrap.ai_gateway_api_key)
|
|
216
|
-
env.AI_GATEWAY_API_KEY = bootstrap.ai_gateway_api_key;
|
|
217
|
-
if (bootstrap.cloudflare_ai_gateway_api_key) {
|
|
218
|
-
env.CLOUDFLARE_AI_GATEWAY_API_KEY = bootstrap.cloudflare_ai_gateway_api_key;
|
|
222
|
+
if (bootstrap.llm_api_key?.trim()) {
|
|
223
|
+
const envKey = AUTH_CHOICE_TO_LLM_ENV[bootstrap.auth_choice ?? ""];
|
|
224
|
+
if (envKey) {
|
|
225
|
+
env[envKey] = bootstrap.llm_api_key;
|
|
226
|
+
}
|
|
219
227
|
}
|
|
220
228
|
return env;
|
|
221
229
|
}
|
|
222
230
|
export class CommandOpenClawProvider {
|
|
223
231
|
stagedMessages = new Map();
|
|
224
|
-
async ensureVersion(config, dryRun, silent,
|
|
232
|
+
async ensureVersion(config, dryRun, silent, preserveExistingState) {
|
|
225
233
|
const bin = config.bin ?? "openclaw";
|
|
226
234
|
const installPolicy = config.install ?? "auto";
|
|
227
235
|
const useCmd = commandFor(config, "use_version", { bin, version: config.version });
|
|
@@ -272,7 +280,7 @@ export class CommandOpenClawProvider {
|
|
|
272
280
|
if (installedThisRun) {
|
|
273
281
|
throw new ClawChefError(`OpenClaw version mismatch after install: current ${currentVersion}, expected ${config.version}`);
|
|
274
282
|
}
|
|
275
|
-
if (
|
|
283
|
+
if (preserveExistingState) {
|
|
276
284
|
return { installedThisRun: false };
|
|
277
285
|
}
|
|
278
286
|
const choice = await chooseVersionMismatchAction(currentVersion, config.version, silent);
|
|
@@ -2,7 +2,7 @@ import type { AgentDef, ChannelDef, ConversationDef, OpenClawSection } from "../
|
|
|
2
2
|
import type { EnsureVersionResult, OpenClawProvider, ResolvedWorkspaceDef } from "./provider.js";
|
|
3
3
|
export declare class MockOpenClawProvider implements OpenClawProvider {
|
|
4
4
|
private state;
|
|
5
|
-
ensureVersion(config: OpenClawSection, _dryRun: boolean, _silent: boolean,
|
|
5
|
+
ensureVersion(config: OpenClawSection, _dryRun: boolean, _silent: boolean, _preserveExistingState: boolean): Promise<EnsureVersionResult>;
|
|
6
6
|
installPlugin(_config: OpenClawSection, _pluginSpec: string, _dryRun: boolean): Promise<void>;
|
|
7
7
|
factoryReset(_config: OpenClawSection, _dryRun: boolean): Promise<void>;
|
|
8
8
|
startGateway(_config: OpenClawSection, _dryRun: boolean): Promise<void>;
|
|
@@ -7,7 +7,7 @@ export class MockOpenClawProvider {
|
|
|
7
7
|
skills: new Set(),
|
|
8
8
|
messages: new Map(),
|
|
9
9
|
};
|
|
10
|
-
async ensureVersion(config, _dryRun, _silent,
|
|
10
|
+
async ensureVersion(config, _dryRun, _silent, _preserveExistingState) {
|
|
11
11
|
const policy = config.install ?? "auto";
|
|
12
12
|
const installed = this.state.installedVersions.has(config.version);
|
|
13
13
|
let installedThisRun = false;
|
|
@@ -6,7 +6,7 @@ export interface EnsureVersionResult {
|
|
|
6
6
|
installedThisRun: boolean;
|
|
7
7
|
}
|
|
8
8
|
export interface OpenClawProvider {
|
|
9
|
-
ensureVersion(config: OpenClawSection, dryRun: boolean, silent: boolean,
|
|
9
|
+
ensureVersion(config: OpenClawSection, dryRun: boolean, silent: boolean, preserveExistingState: boolean): Promise<EnsureVersionResult>;
|
|
10
10
|
installPlugin(config: OpenClawSection, pluginSpec: string, dryRun: boolean): Promise<void>;
|
|
11
11
|
factoryReset(config: OpenClawSection, dryRun: boolean): Promise<void>;
|
|
12
12
|
startGateway(config: OpenClawSection, dryRun: boolean): Promise<void>;
|
|
@@ -5,7 +5,7 @@ export declare class RemoteOpenClawProvider implements OpenClawProvider {
|
|
|
5
5
|
private readonly remoteConfig;
|
|
6
6
|
constructor(remoteConfig: Partial<OpenClawRemoteConfig>);
|
|
7
7
|
private perform;
|
|
8
|
-
ensureVersion(config: OpenClawSection, dryRun: boolean, _silent: boolean,
|
|
8
|
+
ensureVersion(config: OpenClawSection, dryRun: boolean, _silent: boolean, _preserveExistingState: boolean): Promise<EnsureVersionResult>;
|
|
9
9
|
installPlugin(config: OpenClawSection, pluginSpec: string, dryRun: boolean): Promise<void>;
|
|
10
10
|
factoryReset(config: OpenClawSection, dryRun: boolean): Promise<void>;
|
|
11
11
|
startGateway(config: OpenClawSection, dryRun: boolean): Promise<void>;
|
|
@@ -94,7 +94,7 @@ export class RemoteOpenClawProvider {
|
|
|
94
94
|
clearTimeout(timeout);
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
|
-
async ensureVersion(config, dryRun, _silent,
|
|
97
|
+
async ensureVersion(config, dryRun, _silent, _preserveExistingState) {
|
|
98
98
|
const result = await this.perform(config, "ensure_version", {
|
|
99
99
|
install: config.install,
|
|
100
100
|
}, dryRun);
|
package/dist/orchestrator.js
CHANGED
|
@@ -22,6 +22,25 @@ function truncateForLog(text, maxLength = 500) {
|
|
|
22
22
|
}
|
|
23
23
|
return `${text.slice(0, maxLength)}... [truncated ${text.length - maxLength} chars]`;
|
|
24
24
|
}
|
|
25
|
+
function renderTemplateString(input, vars, allowMissing) {
|
|
26
|
+
return input.replace(/\$\{([^}]+)\}/g, (_match, rawKey) => {
|
|
27
|
+
const key = String(rawKey).trim();
|
|
28
|
+
if (!key) {
|
|
29
|
+
return "";
|
|
30
|
+
}
|
|
31
|
+
if (Object.prototype.hasOwnProperty.call(vars, key)) {
|
|
32
|
+
return vars[key] ?? "";
|
|
33
|
+
}
|
|
34
|
+
const lowerKey = key.toLowerCase();
|
|
35
|
+
if (Object.prototype.hasOwnProperty.call(vars, lowerKey)) {
|
|
36
|
+
return vars[lowerKey] ?? "";
|
|
37
|
+
}
|
|
38
|
+
if (allowMissing) {
|
|
39
|
+
return `\${${key}}`;
|
|
40
|
+
}
|
|
41
|
+
throw new ClawChefError(`Missing template variable in file content: ${key}`);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
25
44
|
function resolveWorkspacePath(recipeOrigin, name, configuredPath) {
|
|
26
45
|
if (configuredPath?.trim()) {
|
|
27
46
|
if (path.isAbsolute(configuredPath)) {
|
|
@@ -121,13 +140,14 @@ export async function runRecipe(recipe, recipeOrigin, options, logger) {
|
|
|
121
140
|
const provider = createProvider(options);
|
|
122
141
|
const remoteMode = options.provider === "remote";
|
|
123
142
|
const workspacePaths = new Map();
|
|
143
|
+
const preserveExistingState = options.scope !== "full";
|
|
124
144
|
logger.info(`Running recipe: ${recipe.name}`);
|
|
125
|
-
const versionResult = await provider.ensureVersion(recipe.openclaw, options.dryRun, options.silent,
|
|
145
|
+
const versionResult = await provider.ensureVersion(recipe.openclaw, options.dryRun, options.silent, preserveExistingState);
|
|
126
146
|
logger.info(`OpenClaw version ready: ${recipe.openclaw.version}`);
|
|
127
147
|
if (versionResult.installedThisRun) {
|
|
128
148
|
logger.info("OpenClaw was installed in this run; skipping factory reset");
|
|
129
149
|
}
|
|
130
|
-
else if (
|
|
150
|
+
else if (preserveExistingState) {
|
|
131
151
|
logger.info("Keeping existing OpenClaw state; skipping factory reset");
|
|
132
152
|
}
|
|
133
153
|
else {
|
|
@@ -198,65 +218,69 @@ export async function runRecipe(recipe, recipeOrigin, options, logger) {
|
|
|
198
218
|
await provider.configureChannel(recipe.openclaw, channel, options.dryRun);
|
|
199
219
|
logger.info(`Channel configured: ${channel.channel}${channel.account ? `/${channel.account}` : ""}`);
|
|
200
220
|
}
|
|
201
|
-
for (const
|
|
202
|
-
const wsPath = workspacePaths.get(
|
|
221
|
+
for (const workspace of recipe.workspaces ?? []) {
|
|
222
|
+
const wsPath = workspacePaths.get(workspace.name);
|
|
203
223
|
if (!wsPath) {
|
|
204
|
-
throw new ClawChefError(`
|
|
224
|
+
throw new ClawChefError(`Workspace does not exist for files: ${workspace.name}`);
|
|
205
225
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
if (
|
|
210
|
-
|
|
226
|
+
for (const file of workspace.files ?? []) {
|
|
227
|
+
if (provider.materializeFile) {
|
|
228
|
+
let content = file.content;
|
|
229
|
+
if (content === undefined && file.content_from) {
|
|
230
|
+
if (!options.dryRun) {
|
|
231
|
+
const rawContent = await readTextFromRef(recipeOrigin, file.content_from);
|
|
232
|
+
content = renderTemplateString(rawContent, options.vars, options.allowMissing);
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
const resolved = resolveFileRef(recipeOrigin, file.content_from);
|
|
236
|
+
content = `__dry_run_content_from__:${resolved.value}`;
|
|
237
|
+
}
|
|
211
238
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
239
|
+
if (content === undefined && file.source) {
|
|
240
|
+
if (!options.dryRun) {
|
|
241
|
+
content = await readTextFromRef(recipeOrigin, file.source);
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
const resolved = resolveFileRef(recipeOrigin, file.source);
|
|
245
|
+
content = `__dry_run_source__:${resolved.value}`;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (content === undefined) {
|
|
249
|
+
throw new ClawChefError(`File ${file.path} requires content, content_from, or source`);
|
|
215
250
|
}
|
|
251
|
+
await provider.materializeFile(recipe.openclaw, workspace.name, file.path, content, file.overwrite, options.dryRun);
|
|
252
|
+
logger.info(`File materialized: ${workspace.name}/${file.path}`);
|
|
253
|
+
continue;
|
|
216
254
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
255
|
+
const target = path.resolve(wsPath, file.path);
|
|
256
|
+
const targetDir = path.dirname(target);
|
|
257
|
+
if (!options.dryRun) {
|
|
258
|
+
await mkdir(targetDir, { recursive: true });
|
|
259
|
+
const alreadyExists = await exists(target);
|
|
260
|
+
if (alreadyExists && file.overwrite === false) {
|
|
261
|
+
logger.warn(`Skipping existing file: ${target}`);
|
|
220
262
|
}
|
|
221
|
-
else {
|
|
222
|
-
|
|
223
|
-
content = `__dry_run_source__:${resolved.value}`;
|
|
263
|
+
else if (file.content !== undefined) {
|
|
264
|
+
await writeFile(target, file.content, "utf8");
|
|
224
265
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
await provider.materializeFile(recipe.openclaw, file.workspace, file.path, content, file.overwrite, options.dryRun);
|
|
230
|
-
logger.info(`File materialized: ${file.workspace}/${file.path}`);
|
|
231
|
-
continue;
|
|
232
|
-
}
|
|
233
|
-
const target = path.resolve(wsPath, file.path);
|
|
234
|
-
const targetDir = path.dirname(target);
|
|
235
|
-
if (!options.dryRun) {
|
|
236
|
-
await mkdir(targetDir, { recursive: true });
|
|
237
|
-
const alreadyExists = await exists(target);
|
|
238
|
-
if (alreadyExists && file.overwrite === false) {
|
|
239
|
-
logger.warn(`Skipping existing file: ${target}`);
|
|
240
|
-
}
|
|
241
|
-
else if (file.content !== undefined) {
|
|
242
|
-
await writeFile(target, file.content, "utf8");
|
|
243
|
-
}
|
|
244
|
-
else if (file.content_from) {
|
|
245
|
-
const content = await readTextFromRef(recipeOrigin, file.content_from);
|
|
246
|
-
await writeFile(target, content, "utf8");
|
|
247
|
-
}
|
|
248
|
-
else if (file.source) {
|
|
249
|
-
const resolved = resolveFileRef(recipeOrigin, file.source);
|
|
250
|
-
if (resolved.kind === "local") {
|
|
251
|
-
await copyFile(resolved.value, target);
|
|
266
|
+
else if (file.content_from) {
|
|
267
|
+
const rawContent = await readTextFromRef(recipeOrigin, file.content_from);
|
|
268
|
+
const content = renderTemplateString(rawContent, options.vars, options.allowMissing);
|
|
269
|
+
await writeFile(target, content, "utf8");
|
|
252
270
|
}
|
|
253
|
-
else {
|
|
254
|
-
const
|
|
255
|
-
|
|
271
|
+
else if (file.source) {
|
|
272
|
+
const resolved = resolveFileRef(recipeOrigin, file.source);
|
|
273
|
+
if (resolved.kind === "local") {
|
|
274
|
+
await copyFile(resolved.value, target);
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
const content = await readBinaryFromRef(recipeOrigin, file.source);
|
|
278
|
+
await writeFile(target, content);
|
|
279
|
+
}
|
|
256
280
|
}
|
|
257
281
|
}
|
|
282
|
+
logger.info(`File materialized: ${workspace.name}/${file.path}`);
|
|
258
283
|
}
|
|
259
|
-
logger.info(`File materialized: ${file.workspace}/${file.path}`);
|
|
260
284
|
}
|
|
261
285
|
for (const agent of recipe.agents ?? []) {
|
|
262
286
|
for (const skill of agent.skills ?? []) {
|