clawchef 0.1.8 → 0.1.9
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 +37 -2
- package/dist/api.d.ts +2 -1
- package/dist/api.js +1 -0
- package/dist/cli.js +9 -0
- package/dist/openclaw/command-provider.d.ts +2 -2
- package/dist/openclaw/command-provider.js +7 -2
- package/dist/openclaw/mock-provider.d.ts +2 -2
- package/dist/openclaw/mock-provider.js +1 -1
- package/dist/openclaw/provider.d.ts +2 -2
- package/dist/openclaw/remote-provider.d.ts +2 -2
- package/dist/openclaw/remote-provider.js +5 -2
- package/dist/orchestrator.js +85 -2
- package/dist/recipe.js +12 -0
- package/dist/schema.d.ts +112 -6
- package/dist/schema.js +21 -0
- package/dist/types.d.ts +9 -0
- package/package.json +1 -1
- package/recipes/sample.yaml +7 -0
- package/src/api.ts +3 -1
- package/src/cli.ts +11 -1
- package/src/openclaw/command-provider.ts +9 -3
- package/src/openclaw/mock-provider.ts +2 -2
- package/src/openclaw/provider.ts +2 -2
- package/src/openclaw/remote-provider.ts +6 -2
- package/src/orchestrator.ts +89 -2
- package/src/recipe.ts +12 -0
- package/src/schema.ts +23 -0
- package/src/types.ts +10 -0
package/README.md
CHANGED
|
@@ -15,10 +15,11 @@ Recipe-driven OpenClaw environment orchestrator.
|
|
|
15
15
|
- Supports scoped execution via `--scope full|files|workspace`.
|
|
16
16
|
- `full` scope runs factory reset first (with confirmation prompt unless `-s/--silent` is used).
|
|
17
17
|
- If `openclaw` is missing, auto-installs the recipe version and skips factory reset.
|
|
18
|
-
- Starts OpenClaw gateway
|
|
18
|
+
- Starts OpenClaw gateway after each recipe execution based on `--gateway-mode`.
|
|
19
19
|
- Creates workspaces and agents (default workspace path: `~/.openclaw/workspace-<workspace-name>`).
|
|
20
20
|
- Supports workspace-level assets copy via `workspaces[].assets`.
|
|
21
21
|
- Materializes files into target workspaces.
|
|
22
|
+
- Supports OpenClaw root-level assets/files via `openclaw.root` (default root path: `~/.openclaw`).
|
|
22
23
|
- Installs skills.
|
|
23
24
|
- Supports plugin preinstall via `openclaw.plugins[]` and runtime `--plugin` flags.
|
|
24
25
|
- Configures channels with `openclaw channels add`.
|
|
@@ -90,6 +91,14 @@ Skip reset confirmation prompt:
|
|
|
90
91
|
clawchef cook recipes/sample.yaml -s
|
|
91
92
|
```
|
|
92
93
|
|
|
94
|
+
Control gateway startup mode:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
clawchef cook recipes/sample.yaml --gateway-mode service
|
|
98
|
+
clawchef cook recipes/sample.yaml --gateway-mode run
|
|
99
|
+
clawchef cook recipes/sample.yaml --gateway-mode none
|
|
100
|
+
```
|
|
101
|
+
|
|
93
102
|
Warning: `-s/--silent` suppresses the factory-reset confirmation and auto-chooses force reinstall on version mismatch.
|
|
94
103
|
Use it only in CI/non-interactive flows where destructive reset behavior is expected.
|
|
95
104
|
|
|
@@ -204,6 +213,7 @@ await scaffold("./my-recipe-project", {
|
|
|
204
213
|
- `plugins`: plugin npm specs to preinstall for this run (`string[]`)
|
|
205
214
|
- `scope`: `full | files | workspace` (default: `full`)
|
|
206
215
|
- `workspaceName`: required when `scope: "workspace"`
|
|
216
|
+
- `gatewayMode`: `service | run | none` (default: `service`)
|
|
207
217
|
- `provider`: `command | remote | mock`
|
|
208
218
|
- `remote`: remote provider config (same fields as CLI remote flags)
|
|
209
219
|
- `envFile`: custom env file path/URL; when set, default cwd `.env` loading is skipped
|
|
@@ -307,6 +317,8 @@ Supported operation values sent by clawchef:
|
|
|
307
317
|
- `configure_channel`, `bind_channel_agent`, `login_channel`
|
|
308
318
|
- `run_agent`
|
|
309
319
|
|
|
320
|
+
For `start_gateway`, clawchef sends `{ mode: "service" | "run" }` in payload when gateway mode is enabled.
|
|
321
|
+
|
|
310
322
|
For `run_agent`, clawchef expects `output` in response for assertions.
|
|
311
323
|
|
|
312
324
|
`command` provider now defaults to the current OpenClaw CLI shape (`openclaw 2026.x`), including:
|
|
@@ -352,6 +364,7 @@ For `command` provider, default command templates are:
|
|
|
352
364
|
- `install_plugin`: `${bin} plugins install ${plugin_spec_q}`
|
|
353
365
|
- `factory_reset`: `${bin} reset --scope full --yes --non-interactive`
|
|
354
366
|
- `start_gateway`: `${bin} gateway start`
|
|
367
|
+
- `run_gateway`: `${bin} gateway run`
|
|
355
368
|
- `bind_channel_agent`: built-in `openclaw config get/set bindings` upsert (override with `openclaw.commands.bind_channel_agent`)
|
|
356
369
|
- `login_channel`: `${bin} channels login --channel ${channel_q}${account_arg}`
|
|
357
370
|
- `create_workspace`: generated from `openclaw.bootstrap` (override with `openclaw.commands.create_workspace`)
|
|
@@ -426,9 +439,31 @@ workspaces:
|
|
|
426
439
|
assets: "./meetingbot-assets"
|
|
427
440
|
```
|
|
428
441
|
|
|
442
|
+
## OpenClaw root files
|
|
443
|
+
|
|
444
|
+
- `openclaw.root.path` is optional.
|
|
445
|
+
- If omitted, clawchef uses `~/.openclaw`.
|
|
446
|
+
- `openclaw.root.assets` is optional and recursively copied into the root directory.
|
|
447
|
+
- `openclaw.root.files[]` runs after assets copy, so explicit file entries can override copied assets.
|
|
448
|
+
- `openclaw.root.assets` is resolved relative to the recipe file path (unless absolute path is given).
|
|
449
|
+
- Direct URL recipes do not support `openclaw.root.assets` (assets must resolve to a local directory).
|
|
450
|
+
- `openclaw.root` currently supports `command` and `mock` providers; `remote` provider is not supported.
|
|
451
|
+
|
|
452
|
+
Example:
|
|
453
|
+
|
|
454
|
+
```yaml
|
|
455
|
+
openclaw:
|
|
456
|
+
version: "2026.2.9"
|
|
457
|
+
root:
|
|
458
|
+
assets: "./openclaw-root-assets"
|
|
459
|
+
files:
|
|
460
|
+
- path: "AGENTS.md"
|
|
461
|
+
content_from: "./snippets/agents-template.md"
|
|
462
|
+
```
|
|
463
|
+
|
|
429
464
|
## File content references
|
|
430
465
|
|
|
431
|
-
In `workspaces[].files[]`, set exactly one of:
|
|
466
|
+
In `workspaces[].files[]` and `openclaw.root.files[]`, set exactly one of:
|
|
432
467
|
|
|
433
468
|
- `content`: inline text in recipe
|
|
434
469
|
- `content_from`: load text from another file/URL (loaded content supports `${var}` template rendering)
|
package/dist/api.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { OpenClawProvider, OpenClawRemoteConfig, RunScope } from "./types.js";
|
|
1
|
+
import type { GatewayMode, 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>;
|
|
@@ -9,6 +9,7 @@ export interface CookOptions {
|
|
|
9
9
|
silent?: boolean;
|
|
10
10
|
scope?: RunScope;
|
|
11
11
|
workspaceName?: string;
|
|
12
|
+
gatewayMode?: GatewayMode;
|
|
12
13
|
provider?: OpenClawProvider;
|
|
13
14
|
remote?: Partial<OpenClawRemoteConfig>;
|
|
14
15
|
envFile?: string;
|
package/dist/api.js
CHANGED
package/dist/cli.js
CHANGED
|
@@ -61,6 +61,12 @@ function parseScope(value) {
|
|
|
61
61
|
}
|
|
62
62
|
throw new ClawChefError(`Invalid --scope value: ${value}. Expected full, files, or workspace`);
|
|
63
63
|
}
|
|
64
|
+
function parseGatewayMode(value) {
|
|
65
|
+
if (value === "service" || value === "run" || value === "none") {
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
throw new ClawChefError(`Invalid --gateway-mode value: ${value}. Expected service, run, or none`);
|
|
69
|
+
}
|
|
64
70
|
function parseOptionalInt(value, fieldName) {
|
|
65
71
|
if (value === undefined) {
|
|
66
72
|
return undefined;
|
|
@@ -101,6 +107,7 @@ export function buildCli() {
|
|
|
101
107
|
.option("-s, --silent", "Skip reset confirmation prompt", false)
|
|
102
108
|
.option("--scope <scope>", "Run scope: full | files | workspace", "full")
|
|
103
109
|
.option("--workspace <name>", "Workspace name (required when --scope workspace)")
|
|
110
|
+
.option("--gateway-mode <mode>", "Gateway mode: service | run | none", "service")
|
|
104
111
|
.option("--dotenv-ref <path-or-url>", "Load env vars from local file or HTTP URL")
|
|
105
112
|
.option("--provider <provider>", "Execution provider: command | remote | mock")
|
|
106
113
|
.option("--plugin <npm-spec>", "Preinstall plugin package (repeatable)", (v, p) => p.concat([v]), [])
|
|
@@ -119,6 +126,7 @@ export function buildCli() {
|
|
|
119
126
|
}
|
|
120
127
|
const provider = parseProvider(opts.provider ?? readEnv("CLAWCHEF_PROVIDER") ?? "command");
|
|
121
128
|
const scope = parseScope(String(opts.scope ?? "full"));
|
|
129
|
+
const gatewayMode = parseGatewayMode(String(opts.gatewayMode ?? "service"));
|
|
122
130
|
const workspaceName = opts.workspace?.trim() ? String(opts.workspace).trim() : undefined;
|
|
123
131
|
if (scope === "workspace" && !workspaceName) {
|
|
124
132
|
throw new ClawChefError("--scope workspace requires --workspace <name>");
|
|
@@ -131,6 +139,7 @@ export function buildCli() {
|
|
|
131
139
|
plugins: parsePluginFlags(opts.plugin),
|
|
132
140
|
scope,
|
|
133
141
|
workspaceName,
|
|
142
|
+
gatewayMode,
|
|
134
143
|
dryRun: Boolean(opts.dryRun),
|
|
135
144
|
allowMissing: Boolean(opts.allowMissing),
|
|
136
145
|
verbose: Boolean(opts.verbose),
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type { AgentDef, ChannelDef, ConversationDef, OpenClawSection } from "../types.js";
|
|
1
|
+
import type { AgentDef, ChannelDef, ConversationDef, GatewayMode, OpenClawSection } from "../types.js";
|
|
2
2
|
import type { EnsureVersionResult, OpenClawProvider, ResolvedWorkspaceDef } from "./provider.js";
|
|
3
3
|
export declare class CommandOpenClawProvider implements OpenClawProvider {
|
|
4
4
|
private readonly stagedMessages;
|
|
5
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
|
-
startGateway(config: OpenClawSection, dryRun: boolean): Promise<void>;
|
|
8
|
+
startGateway(config: OpenClawSection, mode: GatewayMode, dryRun: boolean): Promise<void>;
|
|
9
9
|
createWorkspace(config: OpenClawSection, workspace: ResolvedWorkspaceDef, dryRun: boolean): Promise<void>;
|
|
10
10
|
configureChannel(config: OpenClawSection, channel: ChannelDef, dryRun: boolean): Promise<void>;
|
|
11
11
|
bindChannelAgent(config: OpenClawSection, channel: ChannelDef, agent: string, dryRun: boolean): Promise<void>;
|
|
@@ -13,6 +13,7 @@ const DEFAULT_COMMANDS = {
|
|
|
13
13
|
install_plugin: "${bin} plugins install ${plugin_spec_q}",
|
|
14
14
|
factory_reset: "${bin} reset --scope full --yes --non-interactive",
|
|
15
15
|
start_gateway: "${bin} gateway start",
|
|
16
|
+
run_gateway: "${bin} gateway run",
|
|
16
17
|
enable_plugin: "",
|
|
17
18
|
bind_channel_agent: "",
|
|
18
19
|
login_channel: "${bin} channels login --channel ${channel_q}${account_arg}",
|
|
@@ -362,9 +363,13 @@ export class CommandOpenClawProvider {
|
|
|
362
363
|
}
|
|
363
364
|
await runShell(cmd, dryRun);
|
|
364
365
|
}
|
|
365
|
-
async startGateway(config, dryRun) {
|
|
366
|
+
async startGateway(config, mode, dryRun) {
|
|
367
|
+
if (mode === "none") {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
366
370
|
const bin = config.bin ?? "openclaw";
|
|
367
|
-
const
|
|
371
|
+
const key = mode === "run" ? "run_gateway" : "start_gateway";
|
|
372
|
+
const startCmd = commandFor(config, key, { bin, version: config.version });
|
|
368
373
|
if (!startCmd.trim()) {
|
|
369
374
|
return;
|
|
370
375
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type { AgentDef, ChannelDef, ConversationDef, OpenClawSection } from "../types.js";
|
|
1
|
+
import type { AgentDef, ChannelDef, ConversationDef, GatewayMode, OpenClawSection } from "../types.js";
|
|
2
2
|
import type { EnsureVersionResult, OpenClawProvider, ResolvedWorkspaceDef } from "./provider.js";
|
|
3
3
|
export declare class MockOpenClawProvider implements OpenClawProvider {
|
|
4
4
|
private state;
|
|
5
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
|
-
startGateway(_config: OpenClawSection, _dryRun: boolean): Promise<void>;
|
|
8
|
+
startGateway(_config: OpenClawSection, _mode: GatewayMode, _dryRun: boolean): Promise<void>;
|
|
9
9
|
createWorkspace(_config: OpenClawSection, workspace: ResolvedWorkspaceDef, _dryRun: boolean): Promise<void>;
|
|
10
10
|
configureChannel(_config: OpenClawSection, channel: ChannelDef, _dryRun: boolean): Promise<void>;
|
|
11
11
|
bindChannelAgent(_config: OpenClawSection, _channel: ChannelDef, _agent: string, _dryRun: boolean): Promise<void>;
|
|
@@ -35,7 +35,7 @@ export class MockOpenClawProvider {
|
|
|
35
35
|
this.state.skills.clear();
|
|
36
36
|
this.state.messages.clear();
|
|
37
37
|
}
|
|
38
|
-
async startGateway(_config, _dryRun) {
|
|
38
|
+
async startGateway(_config, _mode, _dryRun) {
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
41
41
|
async createWorkspace(_config, workspace, _dryRun) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentDef, ChannelDef, ConversationDef, OpenClawSection, WorkspaceDef } from "../types.js";
|
|
1
|
+
import type { AgentDef, ChannelDef, ConversationDef, GatewayMode, OpenClawSection, WorkspaceDef } from "../types.js";
|
|
2
2
|
export type ResolvedWorkspaceDef = WorkspaceDef & {
|
|
3
3
|
path: string;
|
|
4
4
|
};
|
|
@@ -9,7 +9,7 @@ export interface OpenClawProvider {
|
|
|
9
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
|
-
startGateway(config: OpenClawSection, dryRun: boolean): Promise<void>;
|
|
12
|
+
startGateway(config: OpenClawSection, mode: GatewayMode, dryRun: boolean): Promise<void>;
|
|
13
13
|
createWorkspace(config: OpenClawSection, workspace: ResolvedWorkspaceDef, dryRun: boolean): Promise<void>;
|
|
14
14
|
configureChannel(config: OpenClawSection, channel: ChannelDef, dryRun: boolean): Promise<void>;
|
|
15
15
|
bindChannelAgent(config: OpenClawSection, channel: ChannelDef, agent: string, dryRun: boolean): Promise<void>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentDef, ChannelDef, ConversationDef, OpenClawRemoteConfig, OpenClawSection } from "../types.js";
|
|
1
|
+
import type { AgentDef, ChannelDef, ConversationDef, GatewayMode, OpenClawRemoteConfig, OpenClawSection } from "../types.js";
|
|
2
2
|
import type { EnsureVersionResult, OpenClawProvider, ResolvedWorkspaceDef } from "./provider.js";
|
|
3
3
|
export declare class RemoteOpenClawProvider implements OpenClawProvider {
|
|
4
4
|
private readonly stagedMessages;
|
|
@@ -8,7 +8,7 @@ export declare class RemoteOpenClawProvider implements OpenClawProvider {
|
|
|
8
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
|
-
startGateway(config: OpenClawSection, dryRun: boolean): Promise<void>;
|
|
11
|
+
startGateway(config: OpenClawSection, mode: GatewayMode, dryRun: boolean): Promise<void>;
|
|
12
12
|
createWorkspace(config: OpenClawSection, workspace: ResolvedWorkspaceDef, dryRun: boolean): Promise<void>;
|
|
13
13
|
configureChannel(config: OpenClawSection, channel: ChannelDef, dryRun: boolean): Promise<void>;
|
|
14
14
|
bindChannelAgent(config: OpenClawSection, channel: ChannelDef, agent: string, dryRun: boolean): Promise<void>;
|
|
@@ -110,8 +110,11 @@ export class RemoteOpenClawProvider {
|
|
|
110
110
|
async factoryReset(config, dryRun) {
|
|
111
111
|
await this.perform(config, "factory_reset", undefined, dryRun);
|
|
112
112
|
}
|
|
113
|
-
async startGateway(config, dryRun) {
|
|
114
|
-
|
|
113
|
+
async startGateway(config, mode, dryRun) {
|
|
114
|
+
if (mode === "none") {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
await this.perform(config, "start_gateway", { mode }, dryRun);
|
|
115
118
|
}
|
|
116
119
|
async createWorkspace(config, workspace, dryRun) {
|
|
117
120
|
await this.perform(config, "create_workspace", { workspace }, dryRun);
|
package/dist/orchestrator.js
CHANGED
|
@@ -55,6 +55,18 @@ function resolveWorkspacePath(recipeOrigin, name, configuredPath) {
|
|
|
55
55
|
const workspaceName = trimmedName.startsWith("workspace-") ? trimmedName : `workspace-${trimmedName}`;
|
|
56
56
|
return path.join(homedir(), ".openclaw", workspaceName);
|
|
57
57
|
}
|
|
58
|
+
function resolveOpenClawRootPath(recipeOrigin, configuredPath) {
|
|
59
|
+
if (configuredPath?.trim()) {
|
|
60
|
+
if (path.isAbsolute(configuredPath)) {
|
|
61
|
+
return configuredPath;
|
|
62
|
+
}
|
|
63
|
+
if (recipeOrigin.kind === "local") {
|
|
64
|
+
return path.resolve(recipeOrigin.recipeDir, configuredPath);
|
|
65
|
+
}
|
|
66
|
+
return path.resolve(configuredPath);
|
|
67
|
+
}
|
|
68
|
+
return path.join(homedir(), ".openclaw");
|
|
69
|
+
}
|
|
58
70
|
function isHttpUrl(value) {
|
|
59
71
|
try {
|
|
60
72
|
const url = new URL(value);
|
|
@@ -164,6 +176,72 @@ export async function runRecipe(recipe, recipeOrigin, options, logger) {
|
|
|
164
176
|
await provider.installPlugin(recipe.openclaw, pluginSpec, options.dryRun);
|
|
165
177
|
logger.info(`Plugin preinstalled: ${pluginSpec}`);
|
|
166
178
|
}
|
|
179
|
+
const root = recipe.openclaw.root;
|
|
180
|
+
if (root && (root.assets?.trim() || (root.files?.length ?? 0) > 0)) {
|
|
181
|
+
if (remoteMode) {
|
|
182
|
+
throw new ClawChefError("openclaw.root assets/files are not supported with --provider remote");
|
|
183
|
+
}
|
|
184
|
+
const openclawRootPath = resolveOpenClawRootPath(recipeOrigin, root.path);
|
|
185
|
+
if (!options.dryRun) {
|
|
186
|
+
await mkdir(openclawRootPath, { recursive: true });
|
|
187
|
+
}
|
|
188
|
+
if (root.assets?.trim()) {
|
|
189
|
+
const resolvedAssets = resolveFileRef(recipeOrigin, root.assets);
|
|
190
|
+
if (resolvedAssets.kind !== "local") {
|
|
191
|
+
throw new ClawChefError(`openclaw.root.assets must resolve to a local directory: ${root.assets}. Direct URL recipes cannot use openclaw.root.assets.`);
|
|
192
|
+
}
|
|
193
|
+
let assetDirStat;
|
|
194
|
+
try {
|
|
195
|
+
assetDirStat = await stat(resolvedAssets.value);
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
199
|
+
throw new ClawChefError(`openclaw.root.assets path is not accessible: ${resolvedAssets.value} (${message})`);
|
|
200
|
+
}
|
|
201
|
+
if (!assetDirStat.isDirectory()) {
|
|
202
|
+
throw new ClawChefError(`openclaw.root.assets must be a directory: ${resolvedAssets.value}`);
|
|
203
|
+
}
|
|
204
|
+
const assetFiles = await collectLocalAssetFiles(resolvedAssets.value);
|
|
205
|
+
for (const assetFile of assetFiles) {
|
|
206
|
+
const target = path.resolve(openclawRootPath, assetFile.relativePath);
|
|
207
|
+
if (!options.dryRun) {
|
|
208
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
209
|
+
await copyFile(assetFile.absolutePath, target);
|
|
210
|
+
}
|
|
211
|
+
logger.info(`OpenClaw root asset copied: ${assetFile.relativePath}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
for (const file of root.files ?? []) {
|
|
215
|
+
const target = path.resolve(openclawRootPath, file.path);
|
|
216
|
+
const targetDir = path.dirname(target);
|
|
217
|
+
if (!options.dryRun) {
|
|
218
|
+
await mkdir(targetDir, { recursive: true });
|
|
219
|
+
const alreadyExists = await exists(target);
|
|
220
|
+
if (alreadyExists && file.overwrite === false) {
|
|
221
|
+
logger.warn(`Skipping existing file: ${target}`);
|
|
222
|
+
}
|
|
223
|
+
else if (file.content !== undefined) {
|
|
224
|
+
await writeFile(target, file.content, "utf8");
|
|
225
|
+
}
|
|
226
|
+
else if (file.content_from) {
|
|
227
|
+
const rawContent = await readTextFromRef(recipeOrigin, file.content_from);
|
|
228
|
+
const content = renderTemplateString(rawContent, options.vars, options.allowMissing);
|
|
229
|
+
await writeFile(target, content, "utf8");
|
|
230
|
+
}
|
|
231
|
+
else if (file.source) {
|
|
232
|
+
const resolved = resolveFileRef(recipeOrigin, file.source);
|
|
233
|
+
if (resolved.kind === "local") {
|
|
234
|
+
await copyFile(resolved.value, target);
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
const content = await readBinaryFromRef(recipeOrigin, file.source);
|
|
238
|
+
await writeFile(target, content);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
logger.info(`OpenClaw root file materialized: ${file.path}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
167
245
|
for (const ws of recipe.workspaces ?? []) {
|
|
168
246
|
const absPath = resolveWorkspacePath(recipeOrigin, ws.name, ws.path);
|
|
169
247
|
workspacePaths.set(ws.name, absPath);
|
|
@@ -323,8 +401,13 @@ export async function runRecipe(recipe, recipeOrigin, options, logger) {
|
|
|
323
401
|
}
|
|
324
402
|
logger.info(`Preset messages sent: ${conv.workspace}/${conv.agent}`);
|
|
325
403
|
}
|
|
326
|
-
await provider.startGateway(recipe.openclaw, options.dryRun);
|
|
327
|
-
|
|
404
|
+
await provider.startGateway(recipe.openclaw, options.gatewayMode, options.dryRun);
|
|
405
|
+
if (options.gatewayMode === "none") {
|
|
406
|
+
logger.info("Gateway start skipped by gateway mode: none");
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
logger.info(`Gateway started (${options.gatewayMode})`);
|
|
410
|
+
}
|
|
328
411
|
for (const channel of recipe.channels ?? []) {
|
|
329
412
|
const effectiveChannel = channel.agent?.trim() && !channel.account?.trim()
|
|
330
413
|
? { ...channel, account: channel.agent.trim() }
|
package/dist/recipe.js
CHANGED
|
@@ -146,6 +146,18 @@ function filterRecipeByWorkspaceName(recipe, workspaceName) {
|
|
|
146
146
|
function semanticValidate(recipe) {
|
|
147
147
|
const ws = new Set((recipe.workspaces ?? []).map((w) => w.name));
|
|
148
148
|
const agentNameCounts = new Map();
|
|
149
|
+
const root = recipe.openclaw.root;
|
|
150
|
+
if (root?.path !== undefined && !root.path.trim()) {
|
|
151
|
+
throw new ClawChefError("openclaw.root.path cannot be empty");
|
|
152
|
+
}
|
|
153
|
+
if (root?.assets !== undefined && !root.assets.trim()) {
|
|
154
|
+
throw new ClawChefError("openclaw.root.assets cannot be empty");
|
|
155
|
+
}
|
|
156
|
+
for (const file of root?.files ?? []) {
|
|
157
|
+
if (!file.path.trim()) {
|
|
158
|
+
throw new ClawChefError("openclaw.root.files[] has file with empty path");
|
|
159
|
+
}
|
|
160
|
+
}
|
|
149
161
|
for (const workspace of recipe.workspaces ?? []) {
|
|
150
162
|
if (workspace.assets !== undefined && !workspace.assets.trim()) {
|
|
151
163
|
throw new ClawChefError(`Workspace ${workspace.name} has empty assets path`);
|
package/dist/schema.d.ts
CHANGED
|
@@ -20,6 +20,61 @@ export declare const recipeSchema: z.ZodObject<{
|
|
|
20
20
|
version: z.ZodString;
|
|
21
21
|
install: z.ZodOptional<z.ZodEnum<["auto", "always", "never"]>>;
|
|
22
22
|
plugins: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
23
|
+
root: z.ZodOptional<z.ZodObject<{
|
|
24
|
+
path: z.ZodOptional<z.ZodString>;
|
|
25
|
+
assets: z.ZodOptional<z.ZodString>;
|
|
26
|
+
files: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodObject<{
|
|
27
|
+
path: z.ZodString;
|
|
28
|
+
content: z.ZodOptional<z.ZodString>;
|
|
29
|
+
content_from: z.ZodOptional<z.ZodString>;
|
|
30
|
+
source: z.ZodOptional<z.ZodString>;
|
|
31
|
+
overwrite: z.ZodOptional<z.ZodBoolean>;
|
|
32
|
+
}, "strict", z.ZodTypeAny, {
|
|
33
|
+
path: string;
|
|
34
|
+
content?: string | undefined;
|
|
35
|
+
overwrite?: boolean | undefined;
|
|
36
|
+
content_from?: string | undefined;
|
|
37
|
+
source?: string | undefined;
|
|
38
|
+
}, {
|
|
39
|
+
path: string;
|
|
40
|
+
content?: string | undefined;
|
|
41
|
+
overwrite?: boolean | undefined;
|
|
42
|
+
content_from?: string | undefined;
|
|
43
|
+
source?: string | undefined;
|
|
44
|
+
}>, {
|
|
45
|
+
path: string;
|
|
46
|
+
content?: string | undefined;
|
|
47
|
+
overwrite?: boolean | undefined;
|
|
48
|
+
content_from?: string | undefined;
|
|
49
|
+
source?: string | undefined;
|
|
50
|
+
}, {
|
|
51
|
+
path: string;
|
|
52
|
+
content?: string | undefined;
|
|
53
|
+
overwrite?: boolean | undefined;
|
|
54
|
+
content_from?: string | undefined;
|
|
55
|
+
source?: string | undefined;
|
|
56
|
+
}>, "many">>;
|
|
57
|
+
}, "strict", z.ZodTypeAny, {
|
|
58
|
+
path?: string | undefined;
|
|
59
|
+
files?: {
|
|
60
|
+
path: string;
|
|
61
|
+
content?: string | undefined;
|
|
62
|
+
overwrite?: boolean | undefined;
|
|
63
|
+
content_from?: string | undefined;
|
|
64
|
+
source?: string | undefined;
|
|
65
|
+
}[] | undefined;
|
|
66
|
+
assets?: string | undefined;
|
|
67
|
+
}, {
|
|
68
|
+
path?: string | undefined;
|
|
69
|
+
files?: {
|
|
70
|
+
path: string;
|
|
71
|
+
content?: string | undefined;
|
|
72
|
+
overwrite?: boolean | undefined;
|
|
73
|
+
content_from?: string | undefined;
|
|
74
|
+
source?: string | undefined;
|
|
75
|
+
}[] | undefined;
|
|
76
|
+
assets?: string | undefined;
|
|
77
|
+
}>>;
|
|
23
78
|
bootstrap: z.ZodOptional<z.ZodObject<{
|
|
24
79
|
non_interactive: z.ZodOptional<z.ZodBoolean>;
|
|
25
80
|
accept_risk: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -48,9 +103,9 @@ export declare const recipeSchema: z.ZodObject<{
|
|
|
48
103
|
token_provider?: string | undefined;
|
|
49
104
|
token_profile_id?: string | undefined;
|
|
50
105
|
llm_api_key?: string | undefined;
|
|
106
|
+
mode?: "remote" | "local" | undefined;
|
|
51
107
|
non_interactive?: boolean | undefined;
|
|
52
108
|
accept_risk?: boolean | undefined;
|
|
53
|
-
mode?: "remote" | "local" | undefined;
|
|
54
109
|
flow?: "quickstart" | "advanced" | "manual" | undefined;
|
|
55
110
|
auth_choice?: string | undefined;
|
|
56
111
|
reset?: boolean | undefined;
|
|
@@ -68,9 +123,9 @@ export declare const recipeSchema: z.ZodObject<{
|
|
|
68
123
|
token_provider?: string | undefined;
|
|
69
124
|
token_profile_id?: string | undefined;
|
|
70
125
|
llm_api_key?: string | undefined;
|
|
126
|
+
mode?: "remote" | "local" | undefined;
|
|
71
127
|
non_interactive?: boolean | undefined;
|
|
72
128
|
accept_risk?: boolean | undefined;
|
|
73
|
-
mode?: "remote" | "local" | undefined;
|
|
74
129
|
flow?: "quickstart" | "advanced" | "manual" | undefined;
|
|
75
130
|
auth_choice?: string | undefined;
|
|
76
131
|
reset?: boolean | undefined;
|
|
@@ -88,6 +143,7 @@ export declare const recipeSchema: z.ZodObject<{
|
|
|
88
143
|
install_plugin: z.ZodOptional<z.ZodString>;
|
|
89
144
|
factory_reset: z.ZodOptional<z.ZodString>;
|
|
90
145
|
start_gateway: z.ZodOptional<z.ZodString>;
|
|
146
|
+
run_gateway: z.ZodOptional<z.ZodString>;
|
|
91
147
|
enable_plugin: z.ZodOptional<z.ZodString>;
|
|
92
148
|
bind_channel_agent: z.ZodOptional<z.ZodString>;
|
|
93
149
|
login_channel: z.ZodOptional<z.ZodString>;
|
|
@@ -103,6 +159,7 @@ export declare const recipeSchema: z.ZodObject<{
|
|
|
103
159
|
install_plugin?: string | undefined;
|
|
104
160
|
factory_reset?: string | undefined;
|
|
105
161
|
start_gateway?: string | undefined;
|
|
162
|
+
run_gateway?: string | undefined;
|
|
106
163
|
enable_plugin?: string | undefined;
|
|
107
164
|
bind_channel_agent?: string | undefined;
|
|
108
165
|
login_channel?: string | undefined;
|
|
@@ -118,6 +175,7 @@ export declare const recipeSchema: z.ZodObject<{
|
|
|
118
175
|
install_plugin?: string | undefined;
|
|
119
176
|
factory_reset?: string | undefined;
|
|
120
177
|
start_gateway?: string | undefined;
|
|
178
|
+
run_gateway?: string | undefined;
|
|
121
179
|
enable_plugin?: string | undefined;
|
|
122
180
|
bind_channel_agent?: string | undefined;
|
|
123
181
|
login_channel?: string | undefined;
|
|
@@ -132,6 +190,17 @@ export declare const recipeSchema: z.ZodObject<{
|
|
|
132
190
|
bin?: string | undefined;
|
|
133
191
|
install?: "auto" | "always" | "never" | undefined;
|
|
134
192
|
plugins?: string[] | undefined;
|
|
193
|
+
root?: {
|
|
194
|
+
path?: string | undefined;
|
|
195
|
+
files?: {
|
|
196
|
+
path: string;
|
|
197
|
+
content?: string | undefined;
|
|
198
|
+
overwrite?: boolean | undefined;
|
|
199
|
+
content_from?: string | undefined;
|
|
200
|
+
source?: string | undefined;
|
|
201
|
+
}[] | undefined;
|
|
202
|
+
assets?: string | undefined;
|
|
203
|
+
} | undefined;
|
|
135
204
|
bootstrap?: {
|
|
136
205
|
workspace?: string | undefined;
|
|
137
206
|
cloudflare_ai_gateway_account_id?: string | undefined;
|
|
@@ -140,9 +209,9 @@ export declare const recipeSchema: z.ZodObject<{
|
|
|
140
209
|
token_provider?: string | undefined;
|
|
141
210
|
token_profile_id?: string | undefined;
|
|
142
211
|
llm_api_key?: string | undefined;
|
|
212
|
+
mode?: "remote" | "local" | undefined;
|
|
143
213
|
non_interactive?: boolean | undefined;
|
|
144
214
|
accept_risk?: boolean | undefined;
|
|
145
|
-
mode?: "remote" | "local" | undefined;
|
|
146
215
|
flow?: "quickstart" | "advanced" | "manual" | undefined;
|
|
147
216
|
auth_choice?: string | undefined;
|
|
148
217
|
reset?: boolean | undefined;
|
|
@@ -160,6 +229,7 @@ export declare const recipeSchema: z.ZodObject<{
|
|
|
160
229
|
install_plugin?: string | undefined;
|
|
161
230
|
factory_reset?: string | undefined;
|
|
162
231
|
start_gateway?: string | undefined;
|
|
232
|
+
run_gateway?: string | undefined;
|
|
163
233
|
enable_plugin?: string | undefined;
|
|
164
234
|
bind_channel_agent?: string | undefined;
|
|
165
235
|
login_channel?: string | undefined;
|
|
@@ -174,6 +244,17 @@ export declare const recipeSchema: z.ZodObject<{
|
|
|
174
244
|
bin?: string | undefined;
|
|
175
245
|
install?: "auto" | "always" | "never" | undefined;
|
|
176
246
|
plugins?: string[] | undefined;
|
|
247
|
+
root?: {
|
|
248
|
+
path?: string | undefined;
|
|
249
|
+
files?: {
|
|
250
|
+
path: string;
|
|
251
|
+
content?: string | undefined;
|
|
252
|
+
overwrite?: boolean | undefined;
|
|
253
|
+
content_from?: string | undefined;
|
|
254
|
+
source?: string | undefined;
|
|
255
|
+
}[] | undefined;
|
|
256
|
+
assets?: string | undefined;
|
|
257
|
+
} | undefined;
|
|
177
258
|
bootstrap?: {
|
|
178
259
|
workspace?: string | undefined;
|
|
179
260
|
cloudflare_ai_gateway_account_id?: string | undefined;
|
|
@@ -182,9 +263,9 @@ export declare const recipeSchema: z.ZodObject<{
|
|
|
182
263
|
token_provider?: string | undefined;
|
|
183
264
|
token_profile_id?: string | undefined;
|
|
184
265
|
llm_api_key?: string | undefined;
|
|
266
|
+
mode?: "remote" | "local" | undefined;
|
|
185
267
|
non_interactive?: boolean | undefined;
|
|
186
268
|
accept_risk?: boolean | undefined;
|
|
187
|
-
mode?: "remote" | "local" | undefined;
|
|
188
269
|
flow?: "quickstart" | "advanced" | "manual" | undefined;
|
|
189
270
|
auth_choice?: string | undefined;
|
|
190
271
|
reset?: boolean | undefined;
|
|
@@ -202,6 +283,7 @@ export declare const recipeSchema: z.ZodObject<{
|
|
|
202
283
|
install_plugin?: string | undefined;
|
|
203
284
|
factory_reset?: string | undefined;
|
|
204
285
|
start_gateway?: string | undefined;
|
|
286
|
+
run_gateway?: string | undefined;
|
|
205
287
|
enable_plugin?: string | undefined;
|
|
206
288
|
bind_channel_agent?: string | undefined;
|
|
207
289
|
login_channel?: string | undefined;
|
|
@@ -416,6 +498,17 @@ export declare const recipeSchema: z.ZodObject<{
|
|
|
416
498
|
bin?: string | undefined;
|
|
417
499
|
install?: "auto" | "always" | "never" | undefined;
|
|
418
500
|
plugins?: string[] | undefined;
|
|
501
|
+
root?: {
|
|
502
|
+
path?: string | undefined;
|
|
503
|
+
files?: {
|
|
504
|
+
path: string;
|
|
505
|
+
content?: string | undefined;
|
|
506
|
+
overwrite?: boolean | undefined;
|
|
507
|
+
content_from?: string | undefined;
|
|
508
|
+
source?: string | undefined;
|
|
509
|
+
}[] | undefined;
|
|
510
|
+
assets?: string | undefined;
|
|
511
|
+
} | undefined;
|
|
419
512
|
bootstrap?: {
|
|
420
513
|
workspace?: string | undefined;
|
|
421
514
|
cloudflare_ai_gateway_account_id?: string | undefined;
|
|
@@ -424,9 +517,9 @@ export declare const recipeSchema: z.ZodObject<{
|
|
|
424
517
|
token_provider?: string | undefined;
|
|
425
518
|
token_profile_id?: string | undefined;
|
|
426
519
|
llm_api_key?: string | undefined;
|
|
520
|
+
mode?: "remote" | "local" | undefined;
|
|
427
521
|
non_interactive?: boolean | undefined;
|
|
428
522
|
accept_risk?: boolean | undefined;
|
|
429
|
-
mode?: "remote" | "local" | undefined;
|
|
430
523
|
flow?: "quickstart" | "advanced" | "manual" | undefined;
|
|
431
524
|
auth_choice?: string | undefined;
|
|
432
525
|
reset?: boolean | undefined;
|
|
@@ -444,6 +537,7 @@ export declare const recipeSchema: z.ZodObject<{
|
|
|
444
537
|
install_plugin?: string | undefined;
|
|
445
538
|
factory_reset?: string | undefined;
|
|
446
539
|
start_gateway?: string | undefined;
|
|
540
|
+
run_gateway?: string | undefined;
|
|
447
541
|
enable_plugin?: string | undefined;
|
|
448
542
|
bind_channel_agent?: string | undefined;
|
|
449
543
|
login_channel?: string | undefined;
|
|
@@ -519,6 +613,17 @@ export declare const recipeSchema: z.ZodObject<{
|
|
|
519
613
|
bin?: string | undefined;
|
|
520
614
|
install?: "auto" | "always" | "never" | undefined;
|
|
521
615
|
plugins?: string[] | undefined;
|
|
616
|
+
root?: {
|
|
617
|
+
path?: string | undefined;
|
|
618
|
+
files?: {
|
|
619
|
+
path: string;
|
|
620
|
+
content?: string | undefined;
|
|
621
|
+
overwrite?: boolean | undefined;
|
|
622
|
+
content_from?: string | undefined;
|
|
623
|
+
source?: string | undefined;
|
|
624
|
+
}[] | undefined;
|
|
625
|
+
assets?: string | undefined;
|
|
626
|
+
} | undefined;
|
|
522
627
|
bootstrap?: {
|
|
523
628
|
workspace?: string | undefined;
|
|
524
629
|
cloudflare_ai_gateway_account_id?: string | undefined;
|
|
@@ -527,9 +632,9 @@ export declare const recipeSchema: z.ZodObject<{
|
|
|
527
632
|
token_provider?: string | undefined;
|
|
528
633
|
token_profile_id?: string | undefined;
|
|
529
634
|
llm_api_key?: string | undefined;
|
|
635
|
+
mode?: "remote" | "local" | undefined;
|
|
530
636
|
non_interactive?: boolean | undefined;
|
|
531
637
|
accept_risk?: boolean | undefined;
|
|
532
|
-
mode?: "remote" | "local" | undefined;
|
|
533
638
|
flow?: "quickstart" | "advanced" | "manual" | undefined;
|
|
534
639
|
auth_choice?: string | undefined;
|
|
535
640
|
reset?: boolean | undefined;
|
|
@@ -547,6 +652,7 @@ export declare const recipeSchema: z.ZodObject<{
|
|
|
547
652
|
install_plugin?: string | undefined;
|
|
548
653
|
factory_reset?: string | undefined;
|
|
549
654
|
start_gateway?: string | undefined;
|
|
655
|
+
run_gateway?: string | undefined;
|
|
550
656
|
enable_plugin?: string | undefined;
|
|
551
657
|
bind_channel_agent?: string | undefined;
|
|
552
658
|
login_channel?: string | undefined;
|
package/dist/schema.js
CHANGED
|
@@ -12,6 +12,7 @@ const openClawCommandsSchema = z
|
|
|
12
12
|
install_plugin: z.string().optional(),
|
|
13
13
|
factory_reset: z.string().optional(),
|
|
14
14
|
start_gateway: z.string().optional(),
|
|
15
|
+
run_gateway: z.string().optional(),
|
|
15
16
|
enable_plugin: z.string().optional(),
|
|
16
17
|
bind_channel_agent: z.string().optional(),
|
|
17
18
|
login_channel: z.string().optional(),
|
|
@@ -45,12 +46,32 @@ const openClawBootstrapSchema = z
|
|
|
45
46
|
token_profile_id: z.string().optional(),
|
|
46
47
|
})
|
|
47
48
|
.strict();
|
|
49
|
+
const rootFileSchema = z
|
|
50
|
+
.object({
|
|
51
|
+
path: z.string().min(1),
|
|
52
|
+
content: z.string().optional(),
|
|
53
|
+
content_from: z.string().min(1).optional(),
|
|
54
|
+
source: z.string().optional(),
|
|
55
|
+
overwrite: z.boolean().optional(),
|
|
56
|
+
})
|
|
57
|
+
.strict()
|
|
58
|
+
.refine((v) => [v.content, v.content_from, v.source].filter((item) => item !== undefined).length === 1, {
|
|
59
|
+
message: "openclaw.root.files[] requires exactly one of content, content_from, or source",
|
|
60
|
+
});
|
|
61
|
+
const openClawRootSchema = z
|
|
62
|
+
.object({
|
|
63
|
+
path: z.string().min(1).optional(),
|
|
64
|
+
assets: z.string().min(1).optional(),
|
|
65
|
+
files: z.array(rootFileSchema).optional(),
|
|
66
|
+
})
|
|
67
|
+
.strict();
|
|
48
68
|
const openClawSchema = z
|
|
49
69
|
.object({
|
|
50
70
|
bin: z.string().optional(),
|
|
51
71
|
version: z.string(),
|
|
52
72
|
install: z.enum(["auto", "always", "never"]).optional(),
|
|
53
73
|
plugins: z.array(z.string().min(1)).optional(),
|
|
74
|
+
root: openClawRootSchema.optional(),
|
|
54
75
|
bootstrap: openClawBootstrapSchema.optional(),
|
|
55
76
|
commands: openClawCommandsSchema.optional(),
|
|
56
77
|
})
|
package/dist/types.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export type InstallPolicy = "auto" | "always" | "never";
|
|
2
2
|
export type OpenClawProvider = "command" | "mock" | "remote";
|
|
3
3
|
export type RunScope = "full" | "files" | "workspace";
|
|
4
|
+
export type GatewayMode = "service" | "run" | "none";
|
|
4
5
|
export interface OpenClawRemoteConfig {
|
|
5
6
|
base_url: string;
|
|
6
7
|
api_key?: string;
|
|
@@ -21,6 +22,7 @@ export interface OpenClawCommandOverrides {
|
|
|
21
22
|
install_plugin?: string;
|
|
22
23
|
factory_reset?: string;
|
|
23
24
|
start_gateway?: string;
|
|
25
|
+
run_gateway?: string;
|
|
24
26
|
enable_plugin?: string;
|
|
25
27
|
bind_channel_agent?: string;
|
|
26
28
|
login_channel?: string;
|
|
@@ -56,9 +58,15 @@ export interface OpenClawSection {
|
|
|
56
58
|
version: string;
|
|
57
59
|
install?: InstallPolicy;
|
|
58
60
|
plugins?: string[];
|
|
61
|
+
root?: OpenClawRootDef;
|
|
59
62
|
bootstrap?: OpenClawBootstrap;
|
|
60
63
|
commands?: OpenClawCommandOverrides;
|
|
61
64
|
}
|
|
65
|
+
export interface OpenClawRootDef {
|
|
66
|
+
path?: string;
|
|
67
|
+
assets?: string;
|
|
68
|
+
files?: WorkspaceFileDef[];
|
|
69
|
+
}
|
|
62
70
|
export interface WorkspaceDef {
|
|
63
71
|
name: string;
|
|
64
72
|
path?: string;
|
|
@@ -129,6 +137,7 @@ export interface RunOptions {
|
|
|
129
137
|
plugins: string[];
|
|
130
138
|
scope: RunScope;
|
|
131
139
|
workspaceName?: string;
|
|
140
|
+
gatewayMode: GatewayMode;
|
|
132
141
|
dryRun: boolean;
|
|
133
142
|
allowMissing: boolean;
|
|
134
143
|
verbose: boolean;
|
package/package.json
CHANGED
package/recipes/sample.yaml
CHANGED
package/src/api.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { runRecipe } from "./orchestrator.js";
|
|
|
6
6
|
import { loadRecipe, loadRecipeText } from "./recipe.js";
|
|
7
7
|
import { scaffoldProject } from "./scaffold.js";
|
|
8
8
|
import { recipeSchema } from "./schema.js";
|
|
9
|
-
import type { OpenClawProvider, OpenClawRemoteConfig, RunOptions, RunScope } from "./types.js";
|
|
9
|
+
import type { GatewayMode, OpenClawProvider, OpenClawRemoteConfig, RunOptions, RunScope } from "./types.js";
|
|
10
10
|
import type { ScaffoldOptions, ScaffoldResult } from "./scaffold.js";
|
|
11
11
|
|
|
12
12
|
export interface CookOptions {
|
|
@@ -18,6 +18,7 @@ export interface CookOptions {
|
|
|
18
18
|
silent?: boolean;
|
|
19
19
|
scope?: RunScope;
|
|
20
20
|
workspaceName?: string;
|
|
21
|
+
gatewayMode?: GatewayMode;
|
|
21
22
|
provider?: OpenClawProvider;
|
|
22
23
|
remote?: Partial<OpenClawRemoteConfig>;
|
|
23
24
|
envFile?: string;
|
|
@@ -39,6 +40,7 @@ function normalizeCookOptions(options: CookOptions): RunOptions {
|
|
|
39
40
|
plugins,
|
|
40
41
|
scope,
|
|
41
42
|
workspaceName,
|
|
43
|
+
gatewayMode: options.gatewayMode ?? "service",
|
|
42
44
|
dryRun: Boolean(options.dryRun),
|
|
43
45
|
allowMissing: Boolean(options.allowMissing),
|
|
44
46
|
verbose: Boolean(options.verbose),
|
package/src/cli.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { runRecipe } from "./orchestrator.js";
|
|
|
6
6
|
import { loadRecipe, loadRecipeText } from "./recipe.js";
|
|
7
7
|
import { recipeSchema } from "./schema.js";
|
|
8
8
|
import { scaffoldProject } from "./scaffold.js";
|
|
9
|
-
import type { RunOptions, RunScope } from "./types.js";
|
|
9
|
+
import type { GatewayMode, RunOptions, RunScope } from "./types.js";
|
|
10
10
|
import YAML from "js-yaml";
|
|
11
11
|
import path from "node:path";
|
|
12
12
|
import { readFileSync } from "node:fs";
|
|
@@ -68,6 +68,13 @@ function parseScope(value: string): RunScope {
|
|
|
68
68
|
throw new ClawChefError(`Invalid --scope value: ${value}. Expected full, files, or workspace`);
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
function parseGatewayMode(value: string): GatewayMode {
|
|
72
|
+
if (value === "service" || value === "run" || value === "none") {
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
throw new ClawChefError(`Invalid --gateway-mode value: ${value}. Expected service, run, or none`);
|
|
76
|
+
}
|
|
77
|
+
|
|
71
78
|
function parseOptionalInt(value: string | undefined, fieldName: string): number | undefined {
|
|
72
79
|
if (value === undefined) {
|
|
73
80
|
return undefined;
|
|
@@ -112,6 +119,7 @@ export function buildCli(): Command {
|
|
|
112
119
|
.option("-s, --silent", "Skip reset confirmation prompt", false)
|
|
113
120
|
.option("--scope <scope>", "Run scope: full | files | workspace", "full")
|
|
114
121
|
.option("--workspace <name>", "Workspace name (required when --scope workspace)")
|
|
122
|
+
.option("--gateway-mode <mode>", "Gateway mode: service | run | none", "service")
|
|
115
123
|
.option("--dotenv-ref <path-or-url>", "Load env vars from local file or HTTP URL")
|
|
116
124
|
.option("--provider <provider>", "Execution provider: command | remote | mock")
|
|
117
125
|
.option("--plugin <npm-spec>", "Preinstall plugin package (repeatable)", (v, p: string[]) => p.concat([v]), [])
|
|
@@ -130,6 +138,7 @@ export function buildCli(): Command {
|
|
|
130
138
|
|
|
131
139
|
const provider = parseProvider(opts.provider ?? readEnv("CLAWCHEF_PROVIDER") ?? "command");
|
|
132
140
|
const scope = parseScope(String(opts.scope ?? "full"));
|
|
141
|
+
const gatewayMode = parseGatewayMode(String(opts.gatewayMode ?? "service"));
|
|
133
142
|
const workspaceName = opts.workspace?.trim() ? String(opts.workspace).trim() : undefined;
|
|
134
143
|
if (scope === "workspace" && !workspaceName) {
|
|
135
144
|
throw new ClawChefError("--scope workspace requires --workspace <name>");
|
|
@@ -142,6 +151,7 @@ export function buildCli(): Command {
|
|
|
142
151
|
plugins: parsePluginFlags(opts.plugin),
|
|
143
152
|
scope,
|
|
144
153
|
workspaceName,
|
|
154
|
+
gatewayMode,
|
|
145
155
|
dryRun: Boolean(opts.dryRun),
|
|
146
156
|
allowMissing: Boolean(opts.allowMissing),
|
|
147
157
|
verbose: Boolean(opts.verbose),
|
|
@@ -6,7 +6,7 @@ import { spawn } from "node:child_process";
|
|
|
6
6
|
import { createInterface } from "node:readline/promises";
|
|
7
7
|
import { stdin as input, stdout as output } from "node:process";
|
|
8
8
|
import { ClawChefError } from "../errors.js";
|
|
9
|
-
import type { AgentDef, ChannelDef, ConversationDef, OpenClawBootstrap, OpenClawSection } from "../types.js";
|
|
9
|
+
import type { AgentDef, ChannelDef, ConversationDef, GatewayMode, OpenClawBootstrap, OpenClawSection } from "../types.js";
|
|
10
10
|
import type { EnsureVersionResult, OpenClawProvider, ResolvedWorkspaceDef } from "./provider.js";
|
|
11
11
|
|
|
12
12
|
const DEFAULT_COMMANDS = {
|
|
@@ -16,6 +16,7 @@ const DEFAULT_COMMANDS = {
|
|
|
16
16
|
install_plugin: "${bin} plugins install ${plugin_spec_q}",
|
|
17
17
|
factory_reset: "${bin} reset --scope full --yes --non-interactive",
|
|
18
18
|
start_gateway: "${bin} gateway start",
|
|
19
|
+
run_gateway: "${bin} gateway run",
|
|
19
20
|
enable_plugin: "",
|
|
20
21
|
bind_channel_agent: "",
|
|
21
22
|
login_channel: "${bin} channels login --channel ${channel_q}${account_arg}",
|
|
@@ -464,9 +465,14 @@ export class CommandOpenClawProvider implements OpenClawProvider {
|
|
|
464
465
|
await runShell(cmd, dryRun);
|
|
465
466
|
}
|
|
466
467
|
|
|
467
|
-
async startGateway(config: OpenClawSection, dryRun: boolean): Promise<void> {
|
|
468
|
+
async startGateway(config: OpenClawSection, mode: GatewayMode, dryRun: boolean): Promise<void> {
|
|
469
|
+
if (mode === "none") {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
|
|
468
473
|
const bin = config.bin ?? "openclaw";
|
|
469
|
-
const
|
|
474
|
+
const key = mode === "run" ? "run_gateway" : "start_gateway";
|
|
475
|
+
const startCmd = commandFor(config, key, { bin, version: config.version });
|
|
470
476
|
if (!startCmd.trim()) {
|
|
471
477
|
return;
|
|
472
478
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentDef, ChannelDef, ConversationDef, OpenClawSection } from "../types.js";
|
|
1
|
+
import type { AgentDef, ChannelDef, ConversationDef, GatewayMode, OpenClawSection } from "../types.js";
|
|
2
2
|
import type { EnsureVersionResult, OpenClawProvider, ResolvedWorkspaceDef } from "./provider.js";
|
|
3
3
|
|
|
4
4
|
interface MockState {
|
|
@@ -57,7 +57,7 @@ export class MockOpenClawProvider implements OpenClawProvider {
|
|
|
57
57
|
this.state.messages.clear();
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
async startGateway(_config: OpenClawSection, _dryRun: boolean): Promise<void> {
|
|
60
|
+
async startGateway(_config: OpenClawSection, _mode: GatewayMode, _dryRun: boolean): Promise<void> {
|
|
61
61
|
return;
|
|
62
62
|
}
|
|
63
63
|
|
package/src/openclaw/provider.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentDef, ChannelDef, ConversationDef, OpenClawSection, WorkspaceDef } from "../types.js";
|
|
1
|
+
import type { AgentDef, ChannelDef, ConversationDef, GatewayMode, OpenClawSection, WorkspaceDef } from "../types.js";
|
|
2
2
|
|
|
3
3
|
export type ResolvedWorkspaceDef = WorkspaceDef & { path: string };
|
|
4
4
|
|
|
@@ -15,7 +15,7 @@ export interface OpenClawProvider {
|
|
|
15
15
|
): Promise<EnsureVersionResult>;
|
|
16
16
|
installPlugin(config: OpenClawSection, pluginSpec: string, dryRun: boolean): Promise<void>;
|
|
17
17
|
factoryReset(config: OpenClawSection, dryRun: boolean): Promise<void>;
|
|
18
|
-
startGateway(config: OpenClawSection, dryRun: boolean): Promise<void>;
|
|
18
|
+
startGateway(config: OpenClawSection, mode: GatewayMode, dryRun: boolean): Promise<void>;
|
|
19
19
|
createWorkspace(config: OpenClawSection, workspace: ResolvedWorkspaceDef, dryRun: boolean): Promise<void>;
|
|
20
20
|
configureChannel(config: OpenClawSection, channel: ChannelDef, dryRun: boolean): Promise<void>;
|
|
21
21
|
bindChannelAgent(config: OpenClawSection, channel: ChannelDef, agent: string, dryRun: boolean): Promise<void>;
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
AgentDef,
|
|
4
4
|
ChannelDef,
|
|
5
5
|
ConversationDef,
|
|
6
|
+
GatewayMode,
|
|
6
7
|
OpenClawRemoteConfig,
|
|
7
8
|
OpenClawSection,
|
|
8
9
|
} from "../types.js";
|
|
@@ -176,8 +177,11 @@ export class RemoteOpenClawProvider implements OpenClawProvider {
|
|
|
176
177
|
await this.perform(config, "factory_reset", undefined, dryRun);
|
|
177
178
|
}
|
|
178
179
|
|
|
179
|
-
async startGateway(config: OpenClawSection, dryRun: boolean): Promise<void> {
|
|
180
|
-
|
|
180
|
+
async startGateway(config: OpenClawSection, mode: GatewayMode, dryRun: boolean): Promise<void> {
|
|
181
|
+
if (mode === "none") {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
await this.perform(config, "start_gateway", { mode }, dryRun);
|
|
181
185
|
}
|
|
182
186
|
|
|
183
187
|
async createWorkspace(config: OpenClawSection, workspace: ResolvedWorkspaceDef, dryRun: boolean): Promise<void> {
|
package/src/orchestrator.ts
CHANGED
|
@@ -62,6 +62,19 @@ function resolveWorkspacePath(recipeOrigin: RecipeOrigin, name: string, configur
|
|
|
62
62
|
return path.join(homedir(), ".openclaw", workspaceName);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
function resolveOpenClawRootPath(recipeOrigin: RecipeOrigin, configuredPath?: string): string {
|
|
66
|
+
if (configuredPath?.trim()) {
|
|
67
|
+
if (path.isAbsolute(configuredPath)) {
|
|
68
|
+
return configuredPath;
|
|
69
|
+
}
|
|
70
|
+
if (recipeOrigin.kind === "local") {
|
|
71
|
+
return path.resolve(recipeOrigin.recipeDir, configuredPath);
|
|
72
|
+
}
|
|
73
|
+
return path.resolve(configuredPath);
|
|
74
|
+
}
|
|
75
|
+
return path.join(homedir(), ".openclaw");
|
|
76
|
+
}
|
|
77
|
+
|
|
65
78
|
function isHttpUrl(value: string): boolean {
|
|
66
79
|
try {
|
|
67
80
|
const url = new URL(value);
|
|
@@ -197,6 +210,76 @@ export async function runRecipe(
|
|
|
197
210
|
logger.info(`Plugin preinstalled: ${pluginSpec}`);
|
|
198
211
|
}
|
|
199
212
|
|
|
213
|
+
const root = recipe.openclaw.root;
|
|
214
|
+
if (root && (root.assets?.trim() || (root.files?.length ?? 0) > 0)) {
|
|
215
|
+
if (remoteMode) {
|
|
216
|
+
throw new ClawChefError("openclaw.root assets/files are not supported with --provider remote");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const openclawRootPath = resolveOpenClawRootPath(recipeOrigin, root.path);
|
|
220
|
+
if (!options.dryRun) {
|
|
221
|
+
await mkdir(openclawRootPath, { recursive: true });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (root.assets?.trim()) {
|
|
225
|
+
const resolvedAssets = resolveFileRef(recipeOrigin, root.assets);
|
|
226
|
+
if (resolvedAssets.kind !== "local") {
|
|
227
|
+
throw new ClawChefError(
|
|
228
|
+
`openclaw.root.assets must resolve to a local directory: ${root.assets}. Direct URL recipes cannot use openclaw.root.assets.`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
let assetDirStat;
|
|
233
|
+
try {
|
|
234
|
+
assetDirStat = await stat(resolvedAssets.value);
|
|
235
|
+
} catch (err) {
|
|
236
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
237
|
+
throw new ClawChefError(`openclaw.root.assets path is not accessible: ${resolvedAssets.value} (${message})`);
|
|
238
|
+
}
|
|
239
|
+
if (!assetDirStat.isDirectory()) {
|
|
240
|
+
throw new ClawChefError(`openclaw.root.assets must be a directory: ${resolvedAssets.value}`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const assetFiles = await collectLocalAssetFiles(resolvedAssets.value);
|
|
244
|
+
for (const assetFile of assetFiles) {
|
|
245
|
+
const target = path.resolve(openclawRootPath, assetFile.relativePath);
|
|
246
|
+
if (!options.dryRun) {
|
|
247
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
248
|
+
await copyFile(assetFile.absolutePath, target);
|
|
249
|
+
}
|
|
250
|
+
logger.info(`OpenClaw root asset copied: ${assetFile.relativePath}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
for (const file of root.files ?? []) {
|
|
255
|
+
const target = path.resolve(openclawRootPath, file.path);
|
|
256
|
+
const targetDir = path.dirname(target);
|
|
257
|
+
|
|
258
|
+
if (!options.dryRun) {
|
|
259
|
+
await mkdir(targetDir, { recursive: true });
|
|
260
|
+
const alreadyExists = await exists(target);
|
|
261
|
+
if (alreadyExists && file.overwrite === false) {
|
|
262
|
+
logger.warn(`Skipping existing file: ${target}`);
|
|
263
|
+
} else if (file.content !== undefined) {
|
|
264
|
+
await writeFile(target, file.content, "utf8");
|
|
265
|
+
} else if (file.content_from) {
|
|
266
|
+
const rawContent = await readTextFromRef(recipeOrigin, file.content_from);
|
|
267
|
+
const content = renderTemplateString(rawContent, options.vars, options.allowMissing);
|
|
268
|
+
await writeFile(target, content, "utf8");
|
|
269
|
+
} else if (file.source) {
|
|
270
|
+
const resolved = resolveFileRef(recipeOrigin, file.source);
|
|
271
|
+
if (resolved.kind === "local") {
|
|
272
|
+
await copyFile(resolved.value, target);
|
|
273
|
+
} else {
|
|
274
|
+
const content = await readBinaryFromRef(recipeOrigin, file.source);
|
|
275
|
+
await writeFile(target, content);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
logger.info(`OpenClaw root file materialized: ${file.path}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
200
283
|
for (const ws of recipe.workspaces ?? []) {
|
|
201
284
|
const absPath = resolveWorkspacePath(recipeOrigin, ws.name, ws.path);
|
|
202
285
|
workspacePaths.set(ws.name, absPath);
|
|
@@ -375,8 +458,12 @@ export async function runRecipe(
|
|
|
375
458
|
logger.info(`Preset messages sent: ${conv.workspace}/${conv.agent}`);
|
|
376
459
|
}
|
|
377
460
|
|
|
378
|
-
await provider.startGateway(recipe.openclaw, options.dryRun);
|
|
379
|
-
|
|
461
|
+
await provider.startGateway(recipe.openclaw, options.gatewayMode, options.dryRun);
|
|
462
|
+
if (options.gatewayMode === "none") {
|
|
463
|
+
logger.info("Gateway start skipped by gateway mode: none");
|
|
464
|
+
} else {
|
|
465
|
+
logger.info(`Gateway started (${options.gatewayMode})`);
|
|
466
|
+
}
|
|
380
467
|
|
|
381
468
|
for (const channel of recipe.channels ?? []) {
|
|
382
469
|
const effectiveChannel = channel.agent?.trim() && !channel.account?.trim()
|
package/src/recipe.ts
CHANGED
|
@@ -194,6 +194,18 @@ function filterRecipeByWorkspaceName(recipe: Recipe, workspaceName: string): Rec
|
|
|
194
194
|
function semanticValidate(recipe: Recipe): void {
|
|
195
195
|
const ws = new Set((recipe.workspaces ?? []).map((w) => w.name));
|
|
196
196
|
const agentNameCounts = new Map<string, number>();
|
|
197
|
+
const root = recipe.openclaw.root;
|
|
198
|
+
if (root?.path !== undefined && !root.path.trim()) {
|
|
199
|
+
throw new ClawChefError("openclaw.root.path cannot be empty");
|
|
200
|
+
}
|
|
201
|
+
if (root?.assets !== undefined && !root.assets.trim()) {
|
|
202
|
+
throw new ClawChefError("openclaw.root.assets cannot be empty");
|
|
203
|
+
}
|
|
204
|
+
for (const file of root?.files ?? []) {
|
|
205
|
+
if (!file.path.trim()) {
|
|
206
|
+
throw new ClawChefError("openclaw.root.files[] has file with empty path");
|
|
207
|
+
}
|
|
208
|
+
}
|
|
197
209
|
for (const workspace of recipe.workspaces ?? []) {
|
|
198
210
|
if (workspace.assets !== undefined && !workspace.assets.trim()) {
|
|
199
211
|
throw new ClawChefError(`Workspace ${workspace.name} has empty assets path`);
|
package/src/schema.ts
CHANGED
|
@@ -14,6 +14,7 @@ const openClawCommandsSchema = z
|
|
|
14
14
|
install_plugin: z.string().optional(),
|
|
15
15
|
factory_reset: z.string().optional(),
|
|
16
16
|
start_gateway: z.string().optional(),
|
|
17
|
+
run_gateway: z.string().optional(),
|
|
17
18
|
enable_plugin: z.string().optional(),
|
|
18
19
|
bind_channel_agent: z.string().optional(),
|
|
19
20
|
login_channel: z.string().optional(),
|
|
@@ -49,12 +50,34 @@ const openClawBootstrapSchema = z
|
|
|
49
50
|
})
|
|
50
51
|
.strict();
|
|
51
52
|
|
|
53
|
+
const rootFileSchema = z
|
|
54
|
+
.object({
|
|
55
|
+
path: z.string().min(1),
|
|
56
|
+
content: z.string().optional(),
|
|
57
|
+
content_from: z.string().min(1).optional(),
|
|
58
|
+
source: z.string().optional(),
|
|
59
|
+
overwrite: z.boolean().optional(),
|
|
60
|
+
})
|
|
61
|
+
.strict()
|
|
62
|
+
.refine((v) => [v.content, v.content_from, v.source].filter((item) => item !== undefined).length === 1, {
|
|
63
|
+
message: "openclaw.root.files[] requires exactly one of content, content_from, or source",
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const openClawRootSchema = z
|
|
67
|
+
.object({
|
|
68
|
+
path: z.string().min(1).optional(),
|
|
69
|
+
assets: z.string().min(1).optional(),
|
|
70
|
+
files: z.array(rootFileSchema).optional(),
|
|
71
|
+
})
|
|
72
|
+
.strict();
|
|
73
|
+
|
|
52
74
|
const openClawSchema = z
|
|
53
75
|
.object({
|
|
54
76
|
bin: z.string().optional(),
|
|
55
77
|
version: z.string(),
|
|
56
78
|
install: z.enum(["auto", "always", "never"]).optional(),
|
|
57
79
|
plugins: z.array(z.string().min(1)).optional(),
|
|
80
|
+
root: openClawRootSchema.optional(),
|
|
58
81
|
bootstrap: openClawBootstrapSchema.optional(),
|
|
59
82
|
commands: openClawCommandsSchema.optional(),
|
|
60
83
|
})
|
package/src/types.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export type InstallPolicy = "auto" | "always" | "never";
|
|
2
2
|
export type OpenClawProvider = "command" | "mock" | "remote";
|
|
3
3
|
export type RunScope = "full" | "files" | "workspace";
|
|
4
|
+
export type GatewayMode = "service" | "run" | "none";
|
|
4
5
|
|
|
5
6
|
export interface OpenClawRemoteConfig {
|
|
6
7
|
base_url: string;
|
|
@@ -24,6 +25,7 @@ export interface OpenClawCommandOverrides {
|
|
|
24
25
|
install_plugin?: string;
|
|
25
26
|
factory_reset?: string;
|
|
26
27
|
start_gateway?: string;
|
|
28
|
+
run_gateway?: string;
|
|
27
29
|
enable_plugin?: string;
|
|
28
30
|
bind_channel_agent?: string;
|
|
29
31
|
login_channel?: string;
|
|
@@ -61,10 +63,17 @@ export interface OpenClawSection {
|
|
|
61
63
|
version: string;
|
|
62
64
|
install?: InstallPolicy;
|
|
63
65
|
plugins?: string[];
|
|
66
|
+
root?: OpenClawRootDef;
|
|
64
67
|
bootstrap?: OpenClawBootstrap;
|
|
65
68
|
commands?: OpenClawCommandOverrides;
|
|
66
69
|
}
|
|
67
70
|
|
|
71
|
+
export interface OpenClawRootDef {
|
|
72
|
+
path?: string;
|
|
73
|
+
assets?: string;
|
|
74
|
+
files?: WorkspaceFileDef[];
|
|
75
|
+
}
|
|
76
|
+
|
|
68
77
|
export interface WorkspaceDef {
|
|
69
78
|
name: string;
|
|
70
79
|
path?: string;
|
|
@@ -143,6 +152,7 @@ export interface RunOptions {
|
|
|
143
152
|
plugins: string[];
|
|
144
153
|
scope: RunScope;
|
|
145
154
|
workspaceName?: string;
|
|
155
|
+
gatewayMode: GatewayMode;
|
|
146
156
|
dryRun: boolean;
|
|
147
157
|
allowMissing: boolean;
|
|
148
158
|
verbose: boolean;
|