@bachbntdev/devcmd 0.0.0-stage → 2.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +849 -2
  3. package/dev +33 -0
  4. package/package.json +47 -4
  5. package/src/cli.js +217 -0
  6. package/src/commands/completion.js +97 -0
  7. package/src/commands/devices.js +26 -0
  8. package/src/commands/docker.js +35 -0
  9. package/src/commands/env.js +52 -0
  10. package/src/commands/frameworks.js +36 -0
  11. package/src/commands/history.js +59 -0
  12. package/src/commands/inspect.js +48 -0
  13. package/src/commands/lifecycle.js +70 -0
  14. package/src/config/index.js +35 -0
  15. package/src/config/user.js +101 -0
  16. package/src/constants.js +23 -0
  17. package/src/devices/android.js +89 -0
  18. package/src/devices/index.js +14 -0
  19. package/src/devices/ios.js +47 -0
  20. package/src/generators/clean-project.js +52 -0
  21. package/src/generators/python-project.js +175 -0
  22. package/src/generators/reset-project.js +16 -0
  23. package/src/handlers/clone.js +73 -0
  24. package/src/handlers/devices.js +78 -0
  25. package/src/handlers/history.js +75 -0
  26. package/src/handlers/json.js +68 -0
  27. package/src/handlers/lifecycle.js +119 -0
  28. package/src/handlers/mcp.js +10 -0
  29. package/src/handlers/scaffold.js +166 -0
  30. package/src/handlers/system.js +135 -0
  31. package/src/handlers/tools.js +66 -0
  32. package/src/handlers/workflow.js +121 -0
  33. package/src/mcp/processes.js +164 -0
  34. package/src/mcp/server.js +32 -0
  35. package/src/mcp/tools.js +389 -0
  36. package/src/openers/built-ins.json +80 -0
  37. package/src/openers/engine.js +51 -0
  38. package/src/openers/index.js +26 -0
  39. package/src/openers/loader.js +89 -0
  40. package/src/openers/schema.js +81 -0
  41. package/src/projects/adapters/android.js +24 -0
  42. package/src/projects/adapters/common.js +24 -0
  43. package/src/projects/adapters/flutter.js +29 -0
  44. package/src/projects/adapters/index.js +30 -0
  45. package/src/projects/adapters/ios.js +35 -0
  46. package/src/projects/adapters/node.js +112 -0
  47. package/src/projects/adapters/python.js +90 -0
  48. package/src/projects/adapters/recipe.js +13 -0
  49. package/src/projects/detect.js +187 -0
  50. package/src/projects/profile.js +175 -0
  51. package/src/projects/recent.js +50 -0
  52. package/src/projects/workspaces.js +96 -0
  53. package/src/recipes/action-runner.js +24 -0
  54. package/src/recipes/built-ins.json +417 -0
  55. package/src/recipes/engine.js +159 -0
  56. package/src/recipes/index.js +34 -0
  57. package/src/recipes/loader.js +82 -0
  58. package/src/recipes/schema.js +225 -0
  59. package/src/runner/captured.js +125 -0
  60. package/src/runner/check.js +93 -0
  61. package/src/runner/command.js +43 -0
  62. package/src/runner/parallel.js +110 -0
  63. package/src/runner/process.js +80 -0
  64. package/src/runtime/execute.js +99 -0
  65. package/src/utils/args.js +219 -0
  66. package/src/utils/interpolate.js +10 -0
@@ -0,0 +1,417 @@
1
+ [
2
+ {
3
+ "name": "flutter",
4
+ "description": "Flutter CLI",
5
+ "capabilities": { "git": true, "noInstall": true },
6
+ "inputs": {},
7
+ "detect": { "priority": 70, "rules": [{ "files": ["pubspec.yaml"] }] },
8
+ "lifecycle": { "adapter": "flutter" },
9
+ "requirements": [
10
+ "node",
11
+ "flutter",
12
+ { "value": "git", "when": { "option": "git", "equals": true } }
13
+ ],
14
+ "actions": [
15
+ {
16
+ "type": "run",
17
+ "executable": "flutter",
18
+ "args": [
19
+ "create",
20
+ "{target}",
21
+ { "value": "--no-pub", "when": { "option": "noInstall", "equals": true } }
22
+ ]
23
+ },
24
+ {
25
+ "type": "run",
26
+ "executable": "git",
27
+ "args": ["-C", "{target}", "init"],
28
+ "when": { "option": "git", "equals": true }
29
+ }
30
+ ]
31
+ },
32
+ {
33
+ "name": "react",
34
+ "description": "React app from scratch, powered by Vite",
35
+ "capabilities": { "language": true, "packageManager": true, "git": true, "noInstall": true },
36
+ "inputs": {},
37
+ "detect": { "priority": 20, "rules": [{ "dependencies": ["react"] }] },
38
+ "lifecycle": { "adapter": "node" },
39
+ "requirements": [
40
+ "node",
41
+ {
42
+ "option": "packageManager",
43
+ "map": { "npm": "npm", "pnpm": "pnpm", "yarn": "yarn" }
44
+ },
45
+ { "value": "git", "when": { "option": "git", "equals": true } }
46
+ ],
47
+ "actions": [
48
+ {
49
+ "type": "run",
50
+ "executable": {
51
+ "option": "packageManager",
52
+ "map": { "npm": "npm", "pnpm": "pnpm", "yarn": "yarn" }
53
+ },
54
+ "args": [
55
+ {
56
+ "option": "packageManager",
57
+ "map": {
58
+ "npm": ["create", "vite@latest", "{target}", "--"],
59
+ "pnpm": ["create", "vite", "{target}"],
60
+ "yarn": ["create", "vite", "{target}"]
61
+ }
62
+ },
63
+ "--template",
64
+ { "option": "typescript", "map": { "true": "react-ts", "false": "react" } },
65
+ "--no-interactive"
66
+ ]
67
+ },
68
+ {
69
+ "type": "run",
70
+ "executable": "git",
71
+ "args": ["-C", "{target}", "init"],
72
+ "when": { "option": "git", "equals": true }
73
+ }
74
+ ]
75
+ },
76
+ {
77
+ "name": "react_native",
78
+ "description": "React Native app powered by Expo (recommended)",
79
+ "capabilities": { "language": true, "packageManager": true, "git": true, "noInstall": true },
80
+ "inputs": {},
81
+ "detect": { "priority": 70, "rules": [{ "dependencies": ["expo"] }] },
82
+ "lifecycle": { "adapter": "node" },
83
+ "requirements": [
84
+ "node",
85
+ {
86
+ "option": "packageManager",
87
+ "map": { "npm": "npx", "pnpm": "pnpm", "yarn": "yarn" }
88
+ },
89
+ { "value": "git", "when": { "option": "git", "equals": true } }
90
+ ],
91
+ "actions": [
92
+ {
93
+ "type": "run",
94
+ "executable": {
95
+ "option": "packageManager",
96
+ "map": { "npm": "npx", "pnpm": "pnpm", "yarn": "yarn" }
97
+ },
98
+ "args": [
99
+ {
100
+ "option": "packageManager",
101
+ "map": {
102
+ "npm": ["create-expo-app@latest", "{target}"],
103
+ "pnpm": ["create", "expo-app", "{target}"],
104
+ "yarn": ["create", "expo-app", "{target}"]
105
+ }
106
+ },
107
+ "--template",
108
+ { "option": "typescript", "map": { "true": "default", "false": "blank" } },
109
+ "--yes",
110
+ { "value": "--no-install", "when": { "option": "noInstall", "equals": true } }
111
+ ]
112
+ },
113
+ {
114
+ "type": "run",
115
+ "executable": "git",
116
+ "args": ["-C", "{target}", "init"],
117
+ "when": { "option": "git", "equals": true }
118
+ }
119
+ ]
120
+ },
121
+ {
122
+ "name": "react_native_cli",
123
+ "description": "React Native app via Community CLI without a framework",
124
+ "capabilities": { "packageManager": ["npm", "yarn"], "git": true, "noInstall": true },
125
+ "inputs": {},
126
+ "detect": { "priority": 60, "rules": [{ "dependencies": ["react-native"] }] },
127
+ "lifecycle": { "adapter": "node" },
128
+ "requirements": [
129
+ "node",
130
+ "npx",
131
+ { "value": "git", "when": { "option": "git", "equals": true } }
132
+ ],
133
+ "actions": [
134
+ {
135
+ "type": "run",
136
+ "executable": "npx",
137
+ "args": [
138
+ "@react-native-community/cli@latest",
139
+ "init",
140
+ "{target}",
141
+ "--pm",
142
+ "{packageManager}",
143
+ { "value": "--skip-git-init", "when": { "option": "git", "equals": false } },
144
+ { "value": "--skip-install", "when": { "option": "noInstall", "equals": true } }
145
+ ]
146
+ }
147
+ ]
148
+ },
149
+ {
150
+ "name": "next",
151
+ "description": "Next.js create-next-app",
152
+ "capabilities": { "language": true, "packageManager": true, "git": true, "eslint": true, "noInstall": true },
153
+ "inputs": {},
154
+ "detect": { "priority": 100, "rules": [{ "dependencies": ["next"] }] },
155
+ "lifecycle": { "adapter": "node" },
156
+ "requirements": [
157
+ "node",
158
+ {
159
+ "option": "packageManager",
160
+ "map": { "npm": "npx", "pnpm": "pnpm", "yarn": "yarn" }
161
+ },
162
+ { "value": "git", "when": { "option": "git", "equals": true } }
163
+ ],
164
+ "actions": [
165
+ {
166
+ "type": "run",
167
+ "executable": {
168
+ "option": "packageManager",
169
+ "map": { "npm": "npx", "pnpm": "pnpm", "yarn": "yarn" }
170
+ },
171
+ "args": [
172
+ {
173
+ "option": "packageManager",
174
+ "map": {
175
+ "npm": ["create-next-app@latest", "{target}"],
176
+ "pnpm": ["create", "next-app@latest", "{target}"],
177
+ "yarn": ["create", "next-app", "{target}"]
178
+ }
179
+ },
180
+ { "option": "typescript", "map": { "true": "--ts", "false": "--js" } },
181
+ { "option": "eslint", "map": { "true": "--eslint", "false": "--no-linter" } },
182
+ "--use-{packageManager}",
183
+ { "value": "--disable-git", "when": { "option": "git", "equals": false } },
184
+ { "value": "--skip-install", "when": { "option": "noInstall", "equals": true } },
185
+ "--yes"
186
+ ]
187
+ }
188
+ ]
189
+ },
190
+ {
191
+ "name": "vue",
192
+ "description": "Official create-vue scaffolder",
193
+ "capabilities": { "language": true, "packageManager": true, "git": true, "eslint": true, "noInstall": true },
194
+ "inputs": {},
195
+ "detect": { "priority": 40, "rules": [{ "dependencies": ["vue"] }] },
196
+ "lifecycle": { "adapter": "node" },
197
+ "requirements": [
198
+ "node",
199
+ {
200
+ "option": "packageManager",
201
+ "map": { "npm": "npm", "pnpm": "pnpm", "yarn": "yarn" }
202
+ },
203
+ { "value": "git", "when": { "option": "git", "equals": true } }
204
+ ],
205
+ "actions": [
206
+ {
207
+ "type": "run",
208
+ "executable": {
209
+ "option": "packageManager",
210
+ "map": { "npm": "npm", "pnpm": "pnpm", "yarn": "yarn" }
211
+ },
212
+ "args": [
213
+ {
214
+ "option": "packageManager",
215
+ "map": {
216
+ "npm": ["create", "vue@latest", "{target}", "--"],
217
+ "pnpm": ["create", "vue@latest", "{target}"],
218
+ "yarn": ["create", "vue@latest", "{target}"]
219
+ }
220
+ },
221
+ { "value": "--typescript", "when": { "option": "typescript", "equals": true } },
222
+ { "value": "--eslint", "when": { "option": "eslint", "equals": true } },
223
+ { "value": "--default", "when": { "option": "vueDefault", "equals": true } }
224
+ ]
225
+ },
226
+ {
227
+ "type": "run",
228
+ "executable": "git",
229
+ "args": ["-C", "{target}", "init"],
230
+ "when": { "option": "git", "equals": true }
231
+ }
232
+ ]
233
+ },
234
+ {
235
+ "name": "angular",
236
+ "description": "Angular CLI (ng new)",
237
+ "capabilities": { "packageManager": true, "git": true, "noInstall": true },
238
+ "inputs": {},
239
+ "detect": { "priority": 85, "rules": [{ "dependencies": ["@angular/core"] }] },
240
+ "lifecycle": { "adapter": "node" },
241
+ "requirements": [
242
+ "node",
243
+ "npx",
244
+ { "value": "git", "when": { "option": "git", "equals": true } }
245
+ ],
246
+ "actions": [
247
+ {
248
+ "type": "run",
249
+ "executable": "npx",
250
+ "args": [
251
+ "@angular/cli@latest",
252
+ "new",
253
+ "{target}",
254
+ "--package-manager",
255
+ "{packageManager}",
256
+ { "value": "--skip-git", "when": { "option": "git", "equals": false } },
257
+ { "value": "--skip-install", "when": { "option": "noInstall", "equals": true } }
258
+ ]
259
+ }
260
+ ]
261
+ },
262
+ {
263
+ "name": "express",
264
+ "description": "Official Express generator",
265
+ "capabilities": { "git": true, "noInstall": true },
266
+ "inputs": {},
267
+ "detect": { "priority": 30, "rules": [{ "dependencies": ["express"] }] },
268
+ "lifecycle": { "adapter": "node" },
269
+ "requirements": [
270
+ "node",
271
+ "npx",
272
+ { "value": "git", "when": { "option": "git", "equals": true } }
273
+ ],
274
+ "actions": [
275
+ {
276
+ "type": "run",
277
+ "executable": "npx",
278
+ "args": ["express-generator@latest", "{target}", "--no-view"]
279
+ },
280
+ {
281
+ "type": "run",
282
+ "executable": "git",
283
+ "args": ["-C", "{target}", "init"],
284
+ "when": { "option": "git", "equals": true }
285
+ }
286
+ ]
287
+ },
288
+ {
289
+ "name": "nest",
290
+ "description": "Nest CLI",
291
+ "capabilities": { "language": true, "packageManager": true, "git": true, "noInstall": true },
292
+ "inputs": {},
293
+ "detect": { "priority": 80, "rules": [{ "dependencies": ["@nestjs/core"] }] },
294
+ "lifecycle": { "adapter": "node" },
295
+ "requirements": [
296
+ "node",
297
+ "npx",
298
+ { "value": "git", "when": { "option": "git", "equals": true } }
299
+ ],
300
+ "actions": [
301
+ {
302
+ "type": "run",
303
+ "executable": "npx",
304
+ "args": [
305
+ "@nestjs/cli@latest",
306
+ "new",
307
+ "{target}",
308
+ "--package-manager",
309
+ "{packageManager}",
310
+ "--language",
311
+ { "option": "typescript", "map": { "true": "TypeScript", "false": "JavaScript" } },
312
+ { "value": "--skip-git", "when": { "option": "git", "equals": false } },
313
+ { "value": "--skip-install", "when": { "option": "noInstall", "equals": true } }
314
+ ]
315
+ }
316
+ ]
317
+ },
318
+ {
319
+ "name": "django",
320
+ "description": "Django full-stack Python framework",
321
+ "capabilities": { "git": true, "python": true },
322
+ "inputs": {},
323
+ "detect": {
324
+ "priority": 80,
325
+ "rules": [
326
+ { "files": ["manage.py"] },
327
+ { "contains": { "pyproject.toml": "django" } }
328
+ ]
329
+ },
330
+ "lifecycle": { "adapter": "python" },
331
+ "requirements": [
332
+ "node",
333
+ "{python}",
334
+ { "value": "git", "when": { "option": "git", "equals": true } }
335
+ ],
336
+ "actions": [
337
+ { "type": "run", "executable": "node", "args": ["{pythonGenerator}", "django", "{target}"] },
338
+ { "type": "run", "executable": "{python}", "args": ["-m", "venv", "{venvDirectory}"] },
339
+ { "type": "run", "executable": "{projectPython}", "args": ["-m", "pip", "install", "-e", "{target}[dev]"] },
340
+ { "type": "run", "executable": "{projectPython}", "args": ["-m", "django", "startproject", "config", "{target}"] },
341
+ {
342
+ "type": "run",
343
+ "executable": "git",
344
+ "args": ["-C", "{target}", "init"],
345
+ "when": { "option": "git", "equals": true }
346
+ }
347
+ ]
348
+ },
349
+ {
350
+ "name": "fastapi",
351
+ "description": "FastAPI Python API framework",
352
+ "capabilities": { "git": true, "python": true, "noInstall": true },
353
+ "inputs": {},
354
+ "detect": { "priority": 70, "rules": [{ "contains": { "pyproject.toml": "fastapi" } }] },
355
+ "lifecycle": { "adapter": "python" },
356
+ "requirements": [
357
+ "node",
358
+ { "value": "{python}", "when": { "option": "noInstall", "equals": false } },
359
+ { "value": "git", "when": { "option": "git", "equals": true } }
360
+ ],
361
+ "actions": [
362
+ { "type": "run", "executable": "node", "args": ["{pythonGenerator}", "fastapi", "{target}"] },
363
+ {
364
+ "type": "run",
365
+ "executable": "{python}",
366
+ "args": ["-m", "venv", "{venvDirectory}"],
367
+ "when": { "option": "noInstall", "equals": false }
368
+ },
369
+ {
370
+ "type": "run",
371
+ "executable": "{projectPython}",
372
+ "args": ["-m", "pip", "install", "-e", "{target}[dev]"],
373
+ "when": { "option": "noInstall", "equals": false }
374
+ },
375
+ {
376
+ "type": "run",
377
+ "executable": "git",
378
+ "args": ["-C", "{target}", "init"],
379
+ "when": { "option": "git", "equals": true }
380
+ }
381
+ ]
382
+ },
383
+ {
384
+ "name": "flask",
385
+ "description": "Flask lightweight Python web framework",
386
+ "capabilities": { "git": true, "python": true, "noInstall": true },
387
+ "inputs": {},
388
+ "detect": { "priority": 60, "rules": [{ "contains": { "pyproject.toml": "flask" } }] },
389
+ "lifecycle": { "adapter": "python" },
390
+ "requirements": [
391
+ "node",
392
+ { "value": "{python}", "when": { "option": "noInstall", "equals": false } },
393
+ { "value": "git", "when": { "option": "git", "equals": true } }
394
+ ],
395
+ "actions": [
396
+ { "type": "run", "executable": "node", "args": ["{pythonGenerator}", "flask", "{target}"] },
397
+ {
398
+ "type": "run",
399
+ "executable": "{python}",
400
+ "args": ["-m", "venv", "{venvDirectory}"],
401
+ "when": { "option": "noInstall", "equals": false }
402
+ },
403
+ {
404
+ "type": "run",
405
+ "executable": "{projectPython}",
406
+ "args": ["-m", "pip", "install", "-e", "{target}[dev]"],
407
+ "when": { "option": "noInstall", "equals": false }
408
+ },
409
+ {
410
+ "type": "run",
411
+ "executable": "git",
412
+ "args": ["-C", "{target}", "init"],
413
+ "when": { "option": "git", "equals": true }
414
+ }
415
+ ]
416
+ }
417
+ ]
@@ -0,0 +1,159 @@
1
+ // Copyright (c) 2026 bachbnt
2
+
3
+ const path = require('path');
4
+ const { createCommand } = require('../runner/command');
5
+ const { interpolate: _interpolate } = require('../utils/interpolate');
6
+
7
+ const ACTION_RUNNER = path.join(__dirname, 'action-runner.js');
8
+ const PYTHON_GENERATOR = path.join(__dirname, '..', 'generators', 'python-project.js');
9
+ const FORBIDDEN_SHELLS = new Set(['sh', 'bash', 'zsh', 'fish', 'cmd', 'cmd.exe', 'powershell', 'pwsh']);
10
+
11
+ function conditionMatches(condition, context) {
12
+ return !condition || context[condition.option] === condition.equals;
13
+ }
14
+
15
+ function interpolate(value, context) {
16
+ return _interpolate(value, context, 'recipe placeholder');
17
+ }
18
+
19
+ function resolveValue(spec, context) {
20
+ if (typeof spec === 'string') return interpolate(spec, context);
21
+ if (Array.isArray(spec)) return spec.flatMap((item) => resolveValue(item, context));
22
+ if (Object.hasOwn(spec, 'value')) {
23
+ return conditionMatches(spec.when, context) ? resolveValue(spec.value, context) : [];
24
+ }
25
+ const key = String(context[spec.option]);
26
+ if (!Object.hasOwn(spec.map, key)) {
27
+ throw new Error(`Recipe has no ${spec.option} mapping for: ${key}`);
28
+ }
29
+ return resolveValue(spec.map[key], context);
30
+ }
31
+
32
+ function scalarValue(spec, context, label) {
33
+ const values = resolveValue(spec, context);
34
+ const list = Array.isArray(values) ? values : [values];
35
+ if (list.length !== 1) throw new Error(`${label} must resolve to one value.`);
36
+ return list[0];
37
+ }
38
+
39
+ function resolveInputs(recipe, target, options = {}) {
40
+ const supplied = options.values || {};
41
+ const definitions = recipe.inputs || {};
42
+ const unknown = Object.keys(supplied).filter((name) => !Object.hasOwn(definitions, name));
43
+ if (unknown.length > 0) throw new Error(`${recipe.name} does not define input: ${unknown.join(', ')}`);
44
+ const values = {};
45
+ const base = { target };
46
+ for (const [name, definition] of Object.entries(definitions)) {
47
+ if (supplied[name] !== undefined) values[name] = supplied[name];
48
+ else if (definition.default !== undefined) values[name] = interpolate(definition.default, { ...base, ...values });
49
+ else if (definition.required && !options.skipRequiredInputs) {
50
+ throw new Error(`Missing required recipe input: ${name}. Pass --set ${name}=value.`);
51
+ }
52
+ }
53
+ return values;
54
+ }
55
+
56
+ function createContext(recipe, target, options = {}, extra = {}) {
57
+ const packageManager = options.packageManager || 'npm';
58
+ const python = options.python || (process.platform === 'win32' ? 'python' : 'python3');
59
+ const projectPython = process.platform === 'win32'
60
+ ? path.join(target, '.venv', 'Scripts', 'python.exe')
61
+ : path.join(target, '.venv', 'bin', 'python');
62
+ const typescript = options.typescript !== false;
63
+ const eslint = options.eslint !== false;
64
+ return {
65
+ target,
66
+ packageManager,
67
+ typescript,
68
+ git: options.git !== false,
69
+ eslint,
70
+ vueDefault: !typescript && !eslint,
71
+ noInstall: Boolean(options.noInstall),
72
+ python,
73
+ venvDirectory: path.join(target, '.venv'),
74
+ projectPython,
75
+ pythonGenerator: PYTHON_GENERATOR,
76
+ ...resolveInputs(recipe, target, options),
77
+ ...extra,
78
+ };
79
+ }
80
+
81
+ function ensureInside(root, candidate, label) {
82
+ const resolvedRoot = path.resolve(root);
83
+ const resolved = path.resolve(resolvedRoot, candidate);
84
+ if (resolved !== resolvedRoot && !resolved.startsWith(`${resolvedRoot}${path.sep}`)) {
85
+ throw new Error(`${label} must stay inside the target directory.`);
86
+ }
87
+ return resolved;
88
+ }
89
+
90
+ function buildFileAction(action, recipe, context) {
91
+ const targetRoot = path.resolve(context.target);
92
+ if (action.type === 'mkdir') {
93
+ const destination = ensureInside(targetRoot, scalarValue(action.path, context, 'mkdir path'), 'mkdir path');
94
+ return createCommand(process.execPath, [ACTION_RUNNER, 'mkdir', destination]);
95
+ }
96
+ if (action.type === 'write') {
97
+ const destination = ensureInside(targetRoot, scalarValue(action.path, context, 'write path'), 'write path');
98
+ const content = scalarValue(action.content, context, 'write content');
99
+ return createCommand(process.execPath, [ACTION_RUNNER, 'write', destination, Buffer.from(content).toString('base64')]);
100
+ }
101
+ const source = path.resolve(recipe.baseDir, scalarValue(action.source, context, 'copy source'));
102
+ if (source !== path.resolve(recipe.baseDir) && !source.startsWith(`${path.resolve(recipe.baseDir)}${path.sep}`)) {
103
+ throw new Error('copy source must stay inside the recipe directory.');
104
+ }
105
+ const destination = ensureInside(
106
+ targetRoot,
107
+ scalarValue(action.destination, context, 'copy destination'),
108
+ 'copy destination'
109
+ );
110
+ return createCommand(process.execPath, [ACTION_RUNNER, 'copy', source, destination]);
111
+ }
112
+
113
+ function buildRecipeActions(recipe, actions, target, options = {}, extra = {}) {
114
+ const context = createContext(recipe, target, options, extra);
115
+ return actions
116
+ .filter((action) => conditionMatches(action.when, context))
117
+ .map((action) => {
118
+ if (action.type !== 'run') return buildFileAction(action, recipe, context);
119
+ const executable = scalarValue(action.executable, context, 'run executable');
120
+ if (FORBIDDEN_SHELLS.has(path.basename(executable).toLowerCase())) {
121
+ throw new Error(`Recipe shell executables are not allowed: ${executable}`);
122
+ }
123
+ const args = action.args.flatMap((arg) => resolveValue(arg, context));
124
+ const cwd = action.cwd === undefined ? undefined : scalarValue(action.cwd, context, 'run cwd');
125
+ return createCommand(executable, args, { cwd });
126
+ });
127
+ }
128
+
129
+ function buildRecipeCommands(recipe, target, options = {}) {
130
+ return buildRecipeActions(recipe, recipe.actions, target, options);
131
+ }
132
+
133
+ function buildRecipeLifecycleCommands(recipe, action, project, options = {}) {
134
+ const actions = recipe.lifecycle?.[action];
135
+ if (!actions) throw new Error(`dev ${action} is not supported for detected project type: ${project.type}.`);
136
+ return buildRecipeActions(recipe, actions, project.root, {
137
+ packageManager: project.packageManager,
138
+ python: options.config?.python,
139
+ values: {},
140
+ skipRequiredInputs: true,
141
+ }, { root: project.root });
142
+ }
143
+
144
+ function getRecipeRequirements(recipe, target, options = {}) {
145
+ const context = createContext(recipe, target, options);
146
+ return recipe.requirements.flatMap((requirement) => resolveValue(requirement, context));
147
+ }
148
+
149
+ module.exports = {
150
+ buildRecipeCommands,
151
+ buildRecipeActions,
152
+ buildRecipeLifecycleCommands,
153
+ conditionMatches,
154
+ createContext,
155
+ getRecipeRequirements,
156
+ interpolate,
157
+ resolveInputs,
158
+ resolveValue,
159
+ };
@@ -0,0 +1,34 @@
1
+ // Copyright (c) 2026 bachbnt
2
+
3
+ const {
4
+ buildRecipeActions,
5
+ buildRecipeCommands,
6
+ buildRecipeLifecycleCommands,
7
+ getRecipeRequirements,
8
+ resolveInputs,
9
+ } = require('./engine');
10
+ const {
11
+ getRecipeDefinitions,
12
+ loadBuiltInRecipes,
13
+ loadRecipeDirectory,
14
+ loadRecipeFile,
15
+ loadRecipeRegistry,
16
+ } = require('./loader');
17
+ const { validateRecipe } = require('./schema');
18
+
19
+ const builtInRegistry = loadRecipeRegistry({ includeUser: false });
20
+
21
+ module.exports = {
22
+ builtInRegistry,
23
+ buildRecipeCommands,
24
+ buildRecipeActions,
25
+ buildRecipeLifecycleCommands,
26
+ getRecipeDefinitions,
27
+ getRecipeRequirements,
28
+ loadBuiltInRecipes,
29
+ loadRecipeDirectory,
30
+ loadRecipeFile,
31
+ loadRecipeRegistry,
32
+ resolveInputs,
33
+ validateRecipe,
34
+ };