clawchef 0.1.2 → 0.1.4
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 +18 -4
- package/dist/api.js +1 -0
- package/dist/cli.js +2 -0
- package/dist/openclaw/command-provider.d.ts +1 -1
- package/dist/openclaw/command-provider.js +4 -1
- 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 +4 -1
- package/dist/recipe.js +130 -31
- package/dist/scaffold.js +11 -7
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
- package/src/api.ts +1 -0
- package/src/cli.ts +2 -0
- package/src/openclaw/command-provider.ts +10 -1
- package/src/openclaw/mock-provider.ts +6 -1
- package/src/openclaw/provider.ts +6 -1
- package/src/openclaw/remote-provider.ts +6 -1
- package/src/orchestrator.ts +8 -1
- package/src/recipe.ts +169 -28
- package/src/scaffold.ts +11 -7
- package/src/types.ts +1 -0
package/README.md
CHANGED
|
@@ -19,6 +19,7 @@ Recipe-driven OpenClaw environment orchestrator.
|
|
|
19
19
|
- Materializes files into target workspaces.
|
|
20
20
|
- Installs skills.
|
|
21
21
|
- Supports plugin preinstall via `openclaw.plugins[]` and runtime `--plugin` flags.
|
|
22
|
+
- Supports preserving existing OpenClaw state via `--keep-openclaw-state` (skip factory reset).
|
|
22
23
|
- Configures channels with `openclaw channels add`.
|
|
23
24
|
- Supports interactive channel login at the end of execution (`channels[].login: true`) for channels that expose login.
|
|
24
25
|
- Supports remote HTTP orchestration via runtime flags (`--provider remote`) when OpenClaw is reachable via API.
|
|
@@ -38,6 +39,12 @@ Run recipe from URL:
|
|
|
38
39
|
clawchef cook https://example.com/recipes/sample.yaml --provider remote
|
|
39
40
|
```
|
|
40
41
|
|
|
42
|
+
Run recipe from GitHub repository root (`recipe.yaml` at repo root):
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
clawchef cook https://github.com/renorzr/loom-plus-recipe
|
|
46
|
+
```
|
|
47
|
+
|
|
41
48
|
Run recipe from archive (default `recipe.yaml`):
|
|
42
49
|
|
|
43
50
|
```bash
|
|
@@ -78,6 +85,12 @@ clawchef cook recipes/sample.yaml -s
|
|
|
78
85
|
Warning: `-s/--silent` suppresses the factory-reset confirmation and auto-chooses force reinstall on version mismatch.
|
|
79
86
|
Use it only in CI/non-interactive flows where destructive reset behavior is expected.
|
|
80
87
|
|
|
88
|
+
Keep existing OpenClaw state (skip reset and keep current version on mismatch):
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
clawchef cook recipes/sample.yaml --keep-openclaw-state
|
|
92
|
+
```
|
|
93
|
+
|
|
81
94
|
From-zero OpenClaw bootstrap (recommended):
|
|
82
95
|
|
|
83
96
|
```bash
|
|
@@ -142,9 +155,9 @@ clawchef scaffold ./my-recipe-project --name meetingbot
|
|
|
142
155
|
Scaffold output:
|
|
143
156
|
|
|
144
157
|
- `package.json` with `telegram-api-mock-server` in `devDependencies`
|
|
145
|
-
- `
|
|
146
|
-
- `
|
|
147
|
-
- `
|
|
158
|
+
- `recipe.yaml` with `telegram-mock` channel, plugin preinstall, and `workspaces[].assets`
|
|
159
|
+
- `assets/{AGENTS.md,IDENTITY.md,SOUL.md,TOOLS.md}`
|
|
160
|
+
- `assets/scripts/scheduling.mjs`
|
|
148
161
|
- `test/recipe-smoke.test.mjs`
|
|
149
162
|
|
|
150
163
|
By default scaffold only writes files; it does not run `npm install`.
|
|
@@ -188,7 +201,7 @@ Notes:
|
|
|
188
201
|
|
|
189
202
|
- `validate()` throws on invalid recipe.
|
|
190
203
|
- `cook()` throws on runtime/configuration errors.
|
|
191
|
-
- `scaffold()` creates `package.json`, `
|
|
204
|
+
- `scaffold()` creates `package.json`, `recipe.yaml`, `assets/`, and `test/`.
|
|
192
205
|
|
|
193
206
|
## Variable precedence
|
|
194
207
|
|
|
@@ -212,6 +225,7 @@ If `.env` exists in the directory where `clawchef` is executed, it is loaded bef
|
|
|
212
225
|
- `https://host/recipe.yaml`
|
|
213
226
|
- `https://host/archive.zip` (loads `recipe.yaml` from archive)
|
|
214
227
|
- `https://host/archive.zip:custom/recipe.yaml`
|
|
228
|
+
- `https://github.com/<owner>/<repo>` (loads `recipe.yaml` from repo root)
|
|
215
229
|
|
|
216
230
|
Supported archives: `.zip`, `.tar`, `.tar.gz`, `.tgz`.
|
|
217
231
|
|
package/dist/api.js
CHANGED
|
@@ -15,6 +15,7 @@ function normalizeCookOptions(options) {
|
|
|
15
15
|
allowMissing: Boolean(options.allowMissing),
|
|
16
16
|
verbose: Boolean(options.verbose),
|
|
17
17
|
silent: options.silent ?? true,
|
|
18
|
+
keepOpenClawState: false,
|
|
18
19
|
provider: options.provider ?? "command",
|
|
19
20
|
remote: options.remote ?? {},
|
|
20
21
|
};
|
package/dist/cli.js
CHANGED
|
@@ -77,6 +77,7 @@ export function buildCli() {
|
|
|
77
77
|
.option("--allow-missing", "Allow unresolved template variables", false)
|
|
78
78
|
.option("--verbose", "Verbose logging", false)
|
|
79
79
|
.option("-s, --silent", "Skip reset confirmation prompt", false)
|
|
80
|
+
.option("--keep-openclaw-state", "Preserve existing OpenClaw state (skip factory reset)", false)
|
|
80
81
|
.option("--provider <provider>", "Execution provider: command | remote | mock")
|
|
81
82
|
.option("--plugin <npm-spec>", "Preinstall plugin package (repeatable)", (v, p) => p.concat([v]), [])
|
|
82
83
|
.option("--remote-base-url <url>", "Remote OpenClaw API base URL")
|
|
@@ -94,6 +95,7 @@ export function buildCli() {
|
|
|
94
95
|
allowMissing: Boolean(opts.allowMissing),
|
|
95
96
|
verbose: Boolean(opts.verbose),
|
|
96
97
|
silent: Boolean(opts.silent),
|
|
98
|
+
keepOpenClawState: Boolean(opts.keepOpenclawState),
|
|
97
99
|
provider,
|
|
98
100
|
remote: {
|
|
99
101
|
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): Promise<EnsureVersionResult>;
|
|
5
|
+
ensureVersion(config: OpenClawSection, dryRun: boolean, silent: boolean, keepOpenClawState: 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>;
|
|
@@ -221,7 +221,7 @@ function bootstrapRuntimeEnv(bootstrap) {
|
|
|
221
221
|
}
|
|
222
222
|
export class CommandOpenClawProvider {
|
|
223
223
|
stagedMessages = new Map();
|
|
224
|
-
async ensureVersion(config, dryRun, silent) {
|
|
224
|
+
async ensureVersion(config, dryRun, silent, keepOpenClawState) {
|
|
225
225
|
const bin = config.bin ?? "openclaw";
|
|
226
226
|
const installPolicy = config.install ?? "auto";
|
|
227
227
|
const useCmd = commandFor(config, "use_version", { bin, version: config.version });
|
|
@@ -272,6 +272,9 @@ export class CommandOpenClawProvider {
|
|
|
272
272
|
if (installedThisRun) {
|
|
273
273
|
throw new ClawChefError(`OpenClaw version mismatch after install: current ${currentVersion}, expected ${config.version}`);
|
|
274
274
|
}
|
|
275
|
+
if (keepOpenClawState) {
|
|
276
|
+
return { installedThisRun: false };
|
|
277
|
+
}
|
|
275
278
|
const choice = await chooseVersionMismatchAction(currentVersion, config.version, silent);
|
|
276
279
|
if (choice === "ignore") {
|
|
277
280
|
return { installedThisRun: false };
|
|
@@ -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): Promise<EnsureVersionResult>;
|
|
5
|
+
ensureVersion(config: OpenClawSection, _dryRun: boolean, _silent: boolean, _keepOpenClawState: 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, _keepOpenClawState) {
|
|
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): Promise<EnsureVersionResult>;
|
|
9
|
+
ensureVersion(config: OpenClawSection, dryRun: boolean, silent: boolean, keepOpenClawState: 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): Promise<EnsureVersionResult>;
|
|
8
|
+
ensureVersion(config: OpenClawSection, dryRun: boolean, _silent: boolean, _keepOpenClawState: 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, _keepOpenClawState) {
|
|
98
98
|
const result = await this.perform(config, "ensure_version", {
|
|
99
99
|
install: config.install,
|
|
100
100
|
}, dryRun);
|
package/dist/orchestrator.js
CHANGED
|
@@ -122,11 +122,14 @@ export async function runRecipe(recipe, recipeOrigin, options, logger) {
|
|
|
122
122
|
const remoteMode = options.provider === "remote";
|
|
123
123
|
const workspacePaths = new Map();
|
|
124
124
|
logger.info(`Running recipe: ${recipe.name}`);
|
|
125
|
-
const versionResult = await provider.ensureVersion(recipe.openclaw, options.dryRun, options.silent);
|
|
125
|
+
const versionResult = await provider.ensureVersion(recipe.openclaw, options.dryRun, options.silent, options.keepOpenClawState);
|
|
126
126
|
logger.info(`OpenClaw version ready: ${recipe.openclaw.version}`);
|
|
127
127
|
if (versionResult.installedThisRun) {
|
|
128
128
|
logger.info("OpenClaw was installed in this run; skipping factory reset");
|
|
129
129
|
}
|
|
130
|
+
else if (options.keepOpenClawState) {
|
|
131
|
+
logger.info("Keeping existing OpenClaw state; skipping factory reset");
|
|
132
|
+
}
|
|
130
133
|
else {
|
|
131
134
|
const confirmed = await confirmFactoryReset(options);
|
|
132
135
|
if (!confirmed) {
|
package/dist/recipe.js
CHANGED
|
@@ -2,7 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import process from "node:process";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
|
-
import { readFile, mkdtemp, stat, writeFile, rm, mkdir } from "node:fs/promises";
|
|
5
|
+
import { readFile, mkdtemp, stat, writeFile, rm, mkdir, readdir } from "node:fs/promises";
|
|
6
6
|
import YAML from "js-yaml";
|
|
7
7
|
import { recipeSchema } from "./schema.js";
|
|
8
8
|
import { ClawChefError } from "./errors.js";
|
|
@@ -245,6 +245,128 @@ function parseUrlReference(input) {
|
|
|
245
245
|
directUrl: parsed.toString(),
|
|
246
246
|
};
|
|
247
247
|
}
|
|
248
|
+
function parseGitHubRepoReference(input) {
|
|
249
|
+
let parsed;
|
|
250
|
+
try {
|
|
251
|
+
parsed = new URL(input);
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
if (parsed.hostname.toLowerCase() !== "github.com") {
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
if (parsed.search || parsed.hash) {
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
const parts = parsed.pathname.split("/").filter((part) => part.length > 0);
|
|
266
|
+
if (parts.length !== 2) {
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
269
|
+
const owner = parts[0];
|
|
270
|
+
const repo = parts[1].endsWith(".git") ? parts[1].slice(0, -4) : parts[1];
|
|
271
|
+
if (!owner || !repo) {
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
return { owner, repo };
|
|
275
|
+
}
|
|
276
|
+
async function resolveRecipePathFromExtracted(extractDir, recipeInArchive, missingMessage) {
|
|
277
|
+
const directPath = path.join(extractDir, recipeInArchive);
|
|
278
|
+
try {
|
|
279
|
+
const directStat = await stat(directPath);
|
|
280
|
+
if (directStat.isFile()) {
|
|
281
|
+
return directPath;
|
|
282
|
+
}
|
|
283
|
+
throw new ClawChefError(`Recipe in archive is not a file: ${recipeInArchive}`);
|
|
284
|
+
}
|
|
285
|
+
catch (err) {
|
|
286
|
+
if (err instanceof ClawChefError) {
|
|
287
|
+
throw err;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
const entries = await readdir(extractDir, { withFileTypes: true });
|
|
291
|
+
const matched = [];
|
|
292
|
+
for (const entry of entries) {
|
|
293
|
+
if (!entry.isDirectory()) {
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
const candidate = path.join(extractDir, entry.name, recipeInArchive);
|
|
297
|
+
try {
|
|
298
|
+
const s = await stat(candidate);
|
|
299
|
+
if (s.isFile()) {
|
|
300
|
+
matched.push(candidate);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (matched.length === 1) {
|
|
308
|
+
return matched[0];
|
|
309
|
+
}
|
|
310
|
+
if (matched.length > 1) {
|
|
311
|
+
throw new ClawChefError(`Multiple recipe files found in extracted archive for ${recipeInArchive}; provide explicit selector`);
|
|
312
|
+
}
|
|
313
|
+
throw new ClawChefError(missingMessage);
|
|
314
|
+
}
|
|
315
|
+
async function resolveGitHubRepoReference(recipeInput) {
|
|
316
|
+
const repoRef = parseGitHubRepoReference(recipeInput);
|
|
317
|
+
if (!repoRef) {
|
|
318
|
+
return undefined;
|
|
319
|
+
}
|
|
320
|
+
const repoApiUrl = `https://api.github.com/repos/${repoRef.owner}/${repoRef.repo}`;
|
|
321
|
+
const repoHeaders = {
|
|
322
|
+
Accept: "application/vnd.github+json",
|
|
323
|
+
"User-Agent": "clawchef",
|
|
324
|
+
};
|
|
325
|
+
let repoResponse;
|
|
326
|
+
try {
|
|
327
|
+
repoResponse = await fetch(repoApiUrl, { headers: repoHeaders });
|
|
328
|
+
}
|
|
329
|
+
catch (err) {
|
|
330
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
331
|
+
throw new ClawChefError(`Failed to fetch GitHub repository metadata ${repoApiUrl}: ${message}`);
|
|
332
|
+
}
|
|
333
|
+
if (!repoResponse.ok) {
|
|
334
|
+
throw new ClawChefError(`Failed to fetch GitHub repository metadata ${repoApiUrl}: HTTP ${repoResponse.status}`);
|
|
335
|
+
}
|
|
336
|
+
const repoJson = (await repoResponse.json());
|
|
337
|
+
const defaultBranch = repoJson.default_branch?.trim();
|
|
338
|
+
if (!defaultBranch) {
|
|
339
|
+
throw new ClawChefError(`GitHub repository ${repoRef.owner}/${repoRef.repo} has no default branch`);
|
|
340
|
+
}
|
|
341
|
+
const tarballUrl = `https://api.github.com/repos/${repoRef.owner}/${repoRef.repo}/tarball/${encodeURIComponent(defaultBranch)}`;
|
|
342
|
+
let tarballResponse;
|
|
343
|
+
try {
|
|
344
|
+
tarballResponse = await fetch(tarballUrl, { headers: repoHeaders });
|
|
345
|
+
}
|
|
346
|
+
catch (err) {
|
|
347
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
348
|
+
throw new ClawChefError(`Failed to download GitHub repository tarball ${tarballUrl}: ${message}`);
|
|
349
|
+
}
|
|
350
|
+
if (!tarballResponse.ok) {
|
|
351
|
+
throw new ClawChefError(`Failed to download GitHub repository tarball ${tarballUrl}: HTTP ${tarballResponse.status}`);
|
|
352
|
+
}
|
|
353
|
+
const tempDir = await mkdtemp(path.join(tmpdir(), "clawchef-recipe-"));
|
|
354
|
+
const archivePath = path.join(tempDir, "repo.tar.gz");
|
|
355
|
+
await writeFile(archivePath, Buffer.from(await tarballResponse.arrayBuffer()));
|
|
356
|
+
const extractDir = path.join(tempDir, "extracted");
|
|
357
|
+
await mkdir(extractDir, { recursive: true });
|
|
358
|
+
await extractArchive(archivePath, extractDir);
|
|
359
|
+
const recipePath = await resolveRecipePathFromExtracted(extractDir, DEFAULT_RECIPE_FILE, `Recipe file not found in GitHub repository root: ${DEFAULT_RECIPE_FILE}`);
|
|
360
|
+
return {
|
|
361
|
+
recipePath,
|
|
362
|
+
origin: {
|
|
363
|
+
kind: "local",
|
|
364
|
+
recipePath,
|
|
365
|
+
recipeDir: path.dirname(recipePath),
|
|
366
|
+
},
|
|
367
|
+
cleanupPaths: [tempDir],
|
|
368
|
+
};
|
|
369
|
+
}
|
|
248
370
|
async function runCommand(command, args) {
|
|
249
371
|
await new Promise((resolve, reject) => {
|
|
250
372
|
const child = spawn(command, args, {
|
|
@@ -287,6 +409,10 @@ async function extractArchive(archivePath, extractDir) {
|
|
|
287
409
|
}
|
|
288
410
|
async function resolveRecipeReference(recipeInput) {
|
|
289
411
|
if (isHttpUrl(recipeInput)) {
|
|
412
|
+
const githubResolved = await resolveGitHubRepoReference(recipeInput);
|
|
413
|
+
if (githubResolved) {
|
|
414
|
+
return githubResolved;
|
|
415
|
+
}
|
|
290
416
|
const parsed = parseUrlReference(recipeInput);
|
|
291
417
|
if (parsed.directUrl) {
|
|
292
418
|
let response;
|
|
@@ -334,16 +460,7 @@ async function resolveRecipeReference(recipeInput) {
|
|
|
334
460
|
await rm(extractDir, { recursive: true, force: true });
|
|
335
461
|
await mkdir(extractDir, { recursive: true });
|
|
336
462
|
await extractArchive(downloadedArchivePath, extractDir);
|
|
337
|
-
const recipePath =
|
|
338
|
-
try {
|
|
339
|
-
const s = await stat(recipePath);
|
|
340
|
-
if (!s.isFile()) {
|
|
341
|
-
throw new ClawChefError(`Recipe in archive is not a file: ${recipeInArchive}`);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
catch {
|
|
345
|
-
throw new ClawChefError(`Recipe file not found in archive: ${recipeInArchive}`);
|
|
346
|
-
}
|
|
463
|
+
const recipePath = await resolveRecipePathFromExtracted(extractDir, recipeInArchive, `Recipe file not found in archive: ${recipeInArchive}`);
|
|
347
464
|
return {
|
|
348
465
|
recipePath,
|
|
349
466
|
origin: {
|
|
@@ -381,16 +498,7 @@ async function resolveRecipeReference(recipeInput) {
|
|
|
381
498
|
const extractDir = path.join(tempDir, "extracted");
|
|
382
499
|
await mkdir(extractDir, { recursive: true });
|
|
383
500
|
await extractArchive(localPath, extractDir);
|
|
384
|
-
const recipePath =
|
|
385
|
-
try {
|
|
386
|
-
const s = await stat(recipePath);
|
|
387
|
-
if (!s.isFile()) {
|
|
388
|
-
throw new ClawChefError(`Recipe in archive is not a file: ${DEFAULT_RECIPE_FILE}`);
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
catch {
|
|
392
|
-
throw new ClawChefError(`Recipe file not found in archive: ${DEFAULT_RECIPE_FILE}`);
|
|
393
|
-
}
|
|
501
|
+
const recipePath = await resolveRecipePathFromExtracted(extractDir, DEFAULT_RECIPE_FILE, `Recipe file not found in archive: ${DEFAULT_RECIPE_FILE}`);
|
|
394
502
|
return {
|
|
395
503
|
recipePath,
|
|
396
504
|
origin: {
|
|
@@ -445,16 +553,7 @@ async function resolveRecipeReference(recipeInput) {
|
|
|
445
553
|
const extractDir = path.join(tempDir, "extracted");
|
|
446
554
|
await mkdir(extractDir, { recursive: true });
|
|
447
555
|
await extractArchive(basePath, extractDir);
|
|
448
|
-
const recipePath =
|
|
449
|
-
try {
|
|
450
|
-
const s = await stat(recipePath);
|
|
451
|
-
if (!s.isFile()) {
|
|
452
|
-
throw new ClawChefError(`Recipe in archive is not a file: ${selector}`);
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
catch {
|
|
456
|
-
throw new ClawChefError(`Recipe file not found in archive: ${selector}`);
|
|
457
|
-
}
|
|
556
|
+
const recipePath = await resolveRecipePathFromExtracted(extractDir, selector, `Recipe file not found in archive: ${selector}`);
|
|
458
557
|
return {
|
|
459
558
|
recipePath,
|
|
460
559
|
origin: {
|
package/dist/scaffold.js
CHANGED
|
@@ -76,7 +76,7 @@ openclaw:
|
|
|
76
76
|
|
|
77
77
|
workspaces:
|
|
78
78
|
- name: "\${workspace_name}"
|
|
79
|
-
assets: "
|
|
79
|
+
assets: "./assets"
|
|
80
80
|
|
|
81
81
|
agents:
|
|
82
82
|
- workspace: "\${workspace_name}"
|
|
@@ -134,11 +134,17 @@ console.log("[scheduling] " + message);
|
|
|
134
134
|
function makeRecipeSmokeTest() {
|
|
135
135
|
return `import test from "node:test";
|
|
136
136
|
import assert from "node:assert/strict";
|
|
137
|
+
import path from "node:path";
|
|
138
|
+
import { fileURLToPath } from "node:url";
|
|
137
139
|
import { access } from "node:fs/promises";
|
|
138
140
|
|
|
141
|
+
const testDir = path.dirname(fileURLToPath(import.meta.url));
|
|
142
|
+
const projectRoot = path.resolve(testDir, "..");
|
|
143
|
+
|
|
139
144
|
test("recipe scaffold files exist", async () => {
|
|
140
|
-
await access("
|
|
141
|
-
await access("
|
|
145
|
+
await access(path.join(projectRoot, "recipe.yaml"));
|
|
146
|
+
await access(path.join(projectRoot, "assets", "AGENTS.md"));
|
|
147
|
+
await access(path.join(projectRoot, "package.json"));
|
|
142
148
|
assert.ok(true);
|
|
143
149
|
});
|
|
144
150
|
`;
|
|
@@ -149,16 +155,14 @@ export async function scaffoldProject(targetDirArg, options = {}) {
|
|
|
149
155
|
const defaultName = path.basename(targetDir);
|
|
150
156
|
const rawProjectName = options.projectName?.trim() || defaultName;
|
|
151
157
|
const projectName = normalizeProjectName(rawProjectName);
|
|
152
|
-
const
|
|
153
|
-
const assetsDir = path.join(srcDir, `${projectName}-assets`);
|
|
158
|
+
const assetsDir = path.join(targetDir, "assets");
|
|
154
159
|
const assetsScriptsDir = path.join(assetsDir, "scripts");
|
|
155
160
|
const testDir = path.join(targetDir, "test");
|
|
156
|
-
await mkdir(srcDir, { recursive: true });
|
|
157
161
|
await mkdir(assetsDir, { recursive: true });
|
|
158
162
|
await mkdir(assetsScriptsDir, { recursive: true });
|
|
159
163
|
await mkdir(testDir, { recursive: true });
|
|
160
164
|
await writeFile(path.join(targetDir, "package.json"), makePackageJson(projectName), "utf8");
|
|
161
|
-
await writeFile(path.join(
|
|
165
|
+
await writeFile(path.join(targetDir, "recipe.yaml"), makeRecipeYaml(projectName), "utf8");
|
|
162
166
|
await writeFile(path.join(assetsDir, "AGENTS.md"), makeAgentsDoc(projectName), "utf8");
|
|
163
167
|
await writeFile(path.join(assetsDir, "IDENTITY.md"), makeIdentityDoc(projectName), "utf8");
|
|
164
168
|
await writeFile(path.join(assetsDir, "SOUL.md"), makeSoulDoc(), "utf8");
|
package/dist/types.d.ts
CHANGED
package/package.json
CHANGED
package/src/api.ts
CHANGED
|
@@ -30,6 +30,7 @@ function normalizeCookOptions(options: CookOptions): RunOptions {
|
|
|
30
30
|
allowMissing: Boolean(options.allowMissing),
|
|
31
31
|
verbose: Boolean(options.verbose),
|
|
32
32
|
silent: options.silent ?? true,
|
|
33
|
+
keepOpenClawState: false,
|
|
33
34
|
provider: options.provider ?? "command",
|
|
34
35
|
remote: options.remote ?? {},
|
|
35
36
|
};
|
package/src/cli.ts
CHANGED
|
@@ -87,6 +87,7 @@ export function buildCli(): Command {
|
|
|
87
87
|
.option("--allow-missing", "Allow unresolved template variables", false)
|
|
88
88
|
.option("--verbose", "Verbose logging", false)
|
|
89
89
|
.option("-s, --silent", "Skip reset confirmation prompt", false)
|
|
90
|
+
.option("--keep-openclaw-state", "Preserve existing OpenClaw state (skip factory reset)", false)
|
|
90
91
|
.option("--provider <provider>", "Execution provider: command | remote | mock")
|
|
91
92
|
.option("--plugin <npm-spec>", "Preinstall plugin package (repeatable)", (v, p: string[]) => p.concat([v]), [])
|
|
92
93
|
.option("--remote-base-url <url>", "Remote OpenClaw API base URL")
|
|
@@ -104,6 +105,7 @@ export function buildCli(): Command {
|
|
|
104
105
|
allowMissing: Boolean(opts.allowMissing),
|
|
105
106
|
verbose: Boolean(opts.verbose),
|
|
106
107
|
silent: Boolean(opts.silent),
|
|
108
|
+
keepOpenClawState: Boolean(opts.keepOpenclawState),
|
|
107
109
|
provider,
|
|
108
110
|
remote: {
|
|
109
111
|
base_url: opts.remoteBaseUrl ?? readEnv("CLAWCHEF_REMOTE_BASE_URL"),
|
|
@@ -273,7 +273,12 @@ function bootstrapRuntimeEnv(bootstrap: OpenClawBootstrap | undefined): Record<s
|
|
|
273
273
|
export class CommandOpenClawProvider implements OpenClawProvider {
|
|
274
274
|
private readonly stagedMessages = new Map<string, StagedMessage[]>();
|
|
275
275
|
|
|
276
|
-
async ensureVersion(
|
|
276
|
+
async ensureVersion(
|
|
277
|
+
config: OpenClawSection,
|
|
278
|
+
dryRun: boolean,
|
|
279
|
+
silent: boolean,
|
|
280
|
+
keepOpenClawState: boolean,
|
|
281
|
+
): Promise<EnsureVersionResult> {
|
|
277
282
|
const bin = config.bin ?? "openclaw";
|
|
278
283
|
const installPolicy = config.install ?? "auto";
|
|
279
284
|
const useCmd = commandFor(config, "use_version", { bin, version: config.version });
|
|
@@ -338,6 +343,10 @@ export class CommandOpenClawProvider implements OpenClawProvider {
|
|
|
338
343
|
);
|
|
339
344
|
}
|
|
340
345
|
|
|
346
|
+
if (keepOpenClawState) {
|
|
347
|
+
return { installedThisRun: false };
|
|
348
|
+
}
|
|
349
|
+
|
|
341
350
|
const choice = await chooseVersionMismatchAction(currentVersion, config.version, silent);
|
|
342
351
|
|
|
343
352
|
if (choice === "ignore") {
|
|
@@ -21,7 +21,12 @@ export class MockOpenClawProvider implements OpenClawProvider {
|
|
|
21
21
|
messages: new Map(),
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
-
async ensureVersion(
|
|
24
|
+
async ensureVersion(
|
|
25
|
+
config: OpenClawSection,
|
|
26
|
+
_dryRun: boolean,
|
|
27
|
+
_silent: boolean,
|
|
28
|
+
_keepOpenClawState: boolean,
|
|
29
|
+
): Promise<EnsureVersionResult> {
|
|
25
30
|
const policy = config.install ?? "auto";
|
|
26
31
|
const installed = this.state.installedVersions.has(config.version);
|
|
27
32
|
let installedThisRun = false;
|
package/src/openclaw/provider.ts
CHANGED
|
@@ -7,7 +7,12 @@ export interface EnsureVersionResult {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
export interface OpenClawProvider {
|
|
10
|
-
ensureVersion(
|
|
10
|
+
ensureVersion(
|
|
11
|
+
config: OpenClawSection,
|
|
12
|
+
dryRun: boolean,
|
|
13
|
+
silent: boolean,
|
|
14
|
+
keepOpenClawState: boolean,
|
|
15
|
+
): Promise<EnsureVersionResult>;
|
|
11
16
|
installPlugin(config: OpenClawSection, pluginSpec: string, dryRun: boolean): Promise<void>;
|
|
12
17
|
factoryReset(config: OpenClawSection, dryRun: boolean): Promise<void>;
|
|
13
18
|
startGateway(config: OpenClawSection, dryRun: boolean): Promise<void>;
|
|
@@ -141,7 +141,12 @@ export class RemoteOpenClawProvider implements OpenClawProvider {
|
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
async ensureVersion(
|
|
144
|
+
async ensureVersion(
|
|
145
|
+
config: OpenClawSection,
|
|
146
|
+
dryRun: boolean,
|
|
147
|
+
_silent: boolean,
|
|
148
|
+
_keepOpenClawState: boolean,
|
|
149
|
+
): Promise<EnsureVersionResult> {
|
|
145
150
|
const result = await this.perform(
|
|
146
151
|
config,
|
|
147
152
|
"ensure_version",
|
package/src/orchestrator.ts
CHANGED
|
@@ -148,11 +148,18 @@ export async function runRecipe(
|
|
|
148
148
|
const workspacePaths = new Map<string, string>();
|
|
149
149
|
|
|
150
150
|
logger.info(`Running recipe: ${recipe.name}`);
|
|
151
|
-
const versionResult = await provider.ensureVersion(
|
|
151
|
+
const versionResult = await provider.ensureVersion(
|
|
152
|
+
recipe.openclaw,
|
|
153
|
+
options.dryRun,
|
|
154
|
+
options.silent,
|
|
155
|
+
options.keepOpenClawState,
|
|
156
|
+
);
|
|
152
157
|
logger.info(`OpenClaw version ready: ${recipe.openclaw.version}`);
|
|
153
158
|
|
|
154
159
|
if (versionResult.installedThisRun) {
|
|
155
160
|
logger.info("OpenClaw was installed in this run; skipping factory reset");
|
|
161
|
+
} else if (options.keepOpenClawState) {
|
|
162
|
+
logger.info("Keeping existing OpenClaw state; skipping factory reset");
|
|
156
163
|
} else {
|
|
157
164
|
const confirmed = await confirmFactoryReset(options);
|
|
158
165
|
if (!confirmed) {
|
package/src/recipe.ts
CHANGED
|
@@ -2,7 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import process from "node:process";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
|
-
import { readFile, mkdtemp, stat, writeFile, rm, mkdir } from "node:fs/promises";
|
|
5
|
+
import { readFile, mkdtemp, stat, writeFile, rm, mkdir, readdir } from "node:fs/promises";
|
|
6
6
|
import YAML from "js-yaml";
|
|
7
7
|
import { recipeSchema } from "./schema.js";
|
|
8
8
|
import { ClawChefError } from "./errors.js";
|
|
@@ -266,6 +266,11 @@ interface ResolvedRecipeReference {
|
|
|
266
266
|
cleanupPaths: string[];
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
+
interface GitHubRepoRef {
|
|
270
|
+
owner: string;
|
|
271
|
+
repo: string;
|
|
272
|
+
}
|
|
273
|
+
|
|
269
274
|
function isHttpUrl(value: string): boolean {
|
|
270
275
|
try {
|
|
271
276
|
const url = new URL(value);
|
|
@@ -323,6 +328,149 @@ function parseUrlReference(input: string): { archiveUrl: string; inner?: string;
|
|
|
323
328
|
};
|
|
324
329
|
}
|
|
325
330
|
|
|
331
|
+
function parseGitHubRepoReference(input: string): GitHubRepoRef | undefined {
|
|
332
|
+
let parsed: URL;
|
|
333
|
+
try {
|
|
334
|
+
parsed = new URL(input);
|
|
335
|
+
} catch {
|
|
336
|
+
return undefined;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
340
|
+
return undefined;
|
|
341
|
+
}
|
|
342
|
+
if (parsed.hostname.toLowerCase() !== "github.com") {
|
|
343
|
+
return undefined;
|
|
344
|
+
}
|
|
345
|
+
if (parsed.search || parsed.hash) {
|
|
346
|
+
return undefined;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const parts = parsed.pathname.split("/").filter((part) => part.length > 0);
|
|
350
|
+
if (parts.length !== 2) {
|
|
351
|
+
return undefined;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const owner = parts[0];
|
|
355
|
+
const repo = parts[1].endsWith(".git") ? parts[1].slice(0, -4) : parts[1];
|
|
356
|
+
if (!owner || !repo) {
|
|
357
|
+
return undefined;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return { owner, repo };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async function resolveRecipePathFromExtracted(
|
|
364
|
+
extractDir: string,
|
|
365
|
+
recipeInArchive: string,
|
|
366
|
+
missingMessage: string,
|
|
367
|
+
): Promise<string> {
|
|
368
|
+
const directPath = path.join(extractDir, recipeInArchive);
|
|
369
|
+
try {
|
|
370
|
+
const directStat = await stat(directPath);
|
|
371
|
+
if (directStat.isFile()) {
|
|
372
|
+
return directPath;
|
|
373
|
+
}
|
|
374
|
+
throw new ClawChefError(`Recipe in archive is not a file: ${recipeInArchive}`);
|
|
375
|
+
} catch (err) {
|
|
376
|
+
if (err instanceof ClawChefError) {
|
|
377
|
+
throw err;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const entries = await readdir(extractDir, { withFileTypes: true });
|
|
382
|
+
const matched: string[] = [];
|
|
383
|
+
for (const entry of entries) {
|
|
384
|
+
if (!entry.isDirectory()) {
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
const candidate = path.join(extractDir, entry.name, recipeInArchive);
|
|
388
|
+
try {
|
|
389
|
+
const s = await stat(candidate);
|
|
390
|
+
if (s.isFile()) {
|
|
391
|
+
matched.push(candidate);
|
|
392
|
+
}
|
|
393
|
+
} catch {
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (matched.length === 1) {
|
|
399
|
+
return matched[0];
|
|
400
|
+
}
|
|
401
|
+
if (matched.length > 1) {
|
|
402
|
+
throw new ClawChefError(`Multiple recipe files found in extracted archive for ${recipeInArchive}; provide explicit selector`);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
throw new ClawChefError(missingMessage);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async function resolveGitHubRepoReference(recipeInput: string): Promise<ResolvedRecipeReference | undefined> {
|
|
409
|
+
const repoRef = parseGitHubRepoReference(recipeInput);
|
|
410
|
+
if (!repoRef) {
|
|
411
|
+
return undefined;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const repoApiUrl = `https://api.github.com/repos/${repoRef.owner}/${repoRef.repo}`;
|
|
415
|
+
const repoHeaders = {
|
|
416
|
+
Accept: "application/vnd.github+json",
|
|
417
|
+
"User-Agent": "clawchef",
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
let repoResponse: Response;
|
|
421
|
+
try {
|
|
422
|
+
repoResponse = await fetch(repoApiUrl, { headers: repoHeaders });
|
|
423
|
+
} catch (err) {
|
|
424
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
425
|
+
throw new ClawChefError(`Failed to fetch GitHub repository metadata ${repoApiUrl}: ${message}`);
|
|
426
|
+
}
|
|
427
|
+
if (!repoResponse.ok) {
|
|
428
|
+
throw new ClawChefError(`Failed to fetch GitHub repository metadata ${repoApiUrl}: HTTP ${repoResponse.status}`);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const repoJson = (await repoResponse.json()) as { default_branch?: string };
|
|
432
|
+
const defaultBranch = repoJson.default_branch?.trim();
|
|
433
|
+
if (!defaultBranch) {
|
|
434
|
+
throw new ClawChefError(`GitHub repository ${repoRef.owner}/${repoRef.repo} has no default branch`);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const tarballUrl = `https://api.github.com/repos/${repoRef.owner}/${repoRef.repo}/tarball/${encodeURIComponent(defaultBranch)}`;
|
|
438
|
+
let tarballResponse: Response;
|
|
439
|
+
try {
|
|
440
|
+
tarballResponse = await fetch(tarballUrl, { headers: repoHeaders });
|
|
441
|
+
} catch (err) {
|
|
442
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
443
|
+
throw new ClawChefError(`Failed to download GitHub repository tarball ${tarballUrl}: ${message}`);
|
|
444
|
+
}
|
|
445
|
+
if (!tarballResponse.ok) {
|
|
446
|
+
throw new ClawChefError(`Failed to download GitHub repository tarball ${tarballUrl}: HTTP ${tarballResponse.status}`);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const tempDir = await mkdtemp(path.join(tmpdir(), "clawchef-recipe-"));
|
|
450
|
+
const archivePath = path.join(tempDir, "repo.tar.gz");
|
|
451
|
+
await writeFile(archivePath, Buffer.from(await tarballResponse.arrayBuffer()));
|
|
452
|
+
|
|
453
|
+
const extractDir = path.join(tempDir, "extracted");
|
|
454
|
+
await mkdir(extractDir, { recursive: true });
|
|
455
|
+
await extractArchive(archivePath, extractDir);
|
|
456
|
+
|
|
457
|
+
const recipePath = await resolveRecipePathFromExtracted(
|
|
458
|
+
extractDir,
|
|
459
|
+
DEFAULT_RECIPE_FILE,
|
|
460
|
+
`Recipe file not found in GitHub repository root: ${DEFAULT_RECIPE_FILE}`,
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
return {
|
|
464
|
+
recipePath,
|
|
465
|
+
origin: {
|
|
466
|
+
kind: "local",
|
|
467
|
+
recipePath,
|
|
468
|
+
recipeDir: path.dirname(recipePath),
|
|
469
|
+
},
|
|
470
|
+
cleanupPaths: [tempDir],
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
326
474
|
async function runCommand(command: string, args: string[]): Promise<void> {
|
|
327
475
|
await new Promise<void>((resolve, reject) => {
|
|
328
476
|
const child = spawn(command, args, {
|
|
@@ -370,6 +518,11 @@ async function extractArchive(archivePath: string, extractDir: string): Promise<
|
|
|
370
518
|
|
|
371
519
|
async function resolveRecipeReference(recipeInput: string): Promise<ResolvedRecipeReference> {
|
|
372
520
|
if (isHttpUrl(recipeInput)) {
|
|
521
|
+
const githubResolved = await resolveGitHubRepoReference(recipeInput);
|
|
522
|
+
if (githubResolved) {
|
|
523
|
+
return githubResolved;
|
|
524
|
+
}
|
|
525
|
+
|
|
373
526
|
const parsed = parseUrlReference(recipeInput);
|
|
374
527
|
if (parsed.directUrl) {
|
|
375
528
|
let response: Response;
|
|
@@ -421,15 +574,11 @@ async function resolveRecipeReference(recipeInput: string): Promise<ResolvedReci
|
|
|
421
574
|
await mkdir(extractDir, { recursive: true });
|
|
422
575
|
await extractArchive(downloadedArchivePath, extractDir);
|
|
423
576
|
|
|
424
|
-
const recipePath =
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
}
|
|
430
|
-
} catch {
|
|
431
|
-
throw new ClawChefError(`Recipe file not found in archive: ${recipeInArchive}`);
|
|
432
|
-
}
|
|
577
|
+
const recipePath = await resolveRecipePathFromExtracted(
|
|
578
|
+
extractDir,
|
|
579
|
+
recipeInArchive,
|
|
580
|
+
`Recipe file not found in archive: ${recipeInArchive}`,
|
|
581
|
+
);
|
|
433
582
|
|
|
434
583
|
return {
|
|
435
584
|
recipePath,
|
|
@@ -470,15 +619,11 @@ async function resolveRecipeReference(recipeInput: string): Promise<ResolvedReci
|
|
|
470
619
|
const extractDir = path.join(tempDir, "extracted");
|
|
471
620
|
await mkdir(extractDir, { recursive: true });
|
|
472
621
|
await extractArchive(localPath, extractDir);
|
|
473
|
-
const recipePath =
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
}
|
|
479
|
-
} catch {
|
|
480
|
-
throw new ClawChefError(`Recipe file not found in archive: ${DEFAULT_RECIPE_FILE}`);
|
|
481
|
-
}
|
|
622
|
+
const recipePath = await resolveRecipePathFromExtracted(
|
|
623
|
+
extractDir,
|
|
624
|
+
DEFAULT_RECIPE_FILE,
|
|
625
|
+
`Recipe file not found in archive: ${DEFAULT_RECIPE_FILE}`,
|
|
626
|
+
);
|
|
482
627
|
return {
|
|
483
628
|
recipePath,
|
|
484
629
|
origin: {
|
|
@@ -538,15 +683,11 @@ async function resolveRecipeReference(recipeInput: string): Promise<ResolvedReci
|
|
|
538
683
|
const extractDir = path.join(tempDir, "extracted");
|
|
539
684
|
await mkdir(extractDir, { recursive: true });
|
|
540
685
|
await extractArchive(basePath, extractDir);
|
|
541
|
-
const recipePath =
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
}
|
|
547
|
-
} catch {
|
|
548
|
-
throw new ClawChefError(`Recipe file not found in archive: ${selector}`);
|
|
549
|
-
}
|
|
686
|
+
const recipePath = await resolveRecipePathFromExtracted(
|
|
687
|
+
extractDir,
|
|
688
|
+
selector,
|
|
689
|
+
`Recipe file not found in archive: ${selector}`,
|
|
690
|
+
);
|
|
550
691
|
return {
|
|
551
692
|
recipePath,
|
|
552
693
|
origin: {
|
package/src/scaffold.ts
CHANGED
|
@@ -89,7 +89,7 @@ openclaw:
|
|
|
89
89
|
|
|
90
90
|
workspaces:
|
|
91
91
|
- name: "\${workspace_name}"
|
|
92
|
-
assets: "
|
|
92
|
+
assets: "./assets"
|
|
93
93
|
|
|
94
94
|
agents:
|
|
95
95
|
- workspace: "\${workspace_name}"
|
|
@@ -153,11 +153,17 @@ console.log("[scheduling] " + message);
|
|
|
153
153
|
function makeRecipeSmokeTest(): string {
|
|
154
154
|
return `import test from "node:test";
|
|
155
155
|
import assert from "node:assert/strict";
|
|
156
|
+
import path from "node:path";
|
|
157
|
+
import { fileURLToPath } from "node:url";
|
|
156
158
|
import { access } from "node:fs/promises";
|
|
157
159
|
|
|
160
|
+
const testDir = path.dirname(fileURLToPath(import.meta.url));
|
|
161
|
+
const projectRoot = path.resolve(testDir, "..");
|
|
162
|
+
|
|
158
163
|
test("recipe scaffold files exist", async () => {
|
|
159
|
-
await access("
|
|
160
|
-
await access("
|
|
164
|
+
await access(path.join(projectRoot, "recipe.yaml"));
|
|
165
|
+
await access(path.join(projectRoot, "assets", "AGENTS.md"));
|
|
166
|
+
await access(path.join(projectRoot, "package.json"));
|
|
161
167
|
assert.ok(true);
|
|
162
168
|
});
|
|
163
169
|
`;
|
|
@@ -171,18 +177,16 @@ export async function scaffoldProject(targetDirArg?: string, options: ScaffoldOp
|
|
|
171
177
|
const rawProjectName = options.projectName?.trim() || defaultName;
|
|
172
178
|
const projectName = normalizeProjectName(rawProjectName);
|
|
173
179
|
|
|
174
|
-
const
|
|
175
|
-
const assetsDir = path.join(srcDir, `${projectName}-assets`);
|
|
180
|
+
const assetsDir = path.join(targetDir, "assets");
|
|
176
181
|
const assetsScriptsDir = path.join(assetsDir, "scripts");
|
|
177
182
|
const testDir = path.join(targetDir, "test");
|
|
178
183
|
|
|
179
|
-
await mkdir(srcDir, { recursive: true });
|
|
180
184
|
await mkdir(assetsDir, { recursive: true });
|
|
181
185
|
await mkdir(assetsScriptsDir, { recursive: true });
|
|
182
186
|
await mkdir(testDir, { recursive: true });
|
|
183
187
|
|
|
184
188
|
await writeFile(path.join(targetDir, "package.json"), makePackageJson(projectName), "utf8");
|
|
185
|
-
await writeFile(path.join(
|
|
189
|
+
await writeFile(path.join(targetDir, "recipe.yaml"), makeRecipeYaml(projectName), "utf8");
|
|
186
190
|
await writeFile(path.join(assetsDir, "AGENTS.md"), makeAgentsDoc(projectName), "utf8");
|
|
187
191
|
await writeFile(path.join(assetsDir, "IDENTITY.md"), makeIdentityDoc(projectName), "utf8");
|
|
188
192
|
await writeFile(path.join(assetsDir, "SOUL.md"), makeSoulDoc(), "utf8");
|