@aiwg/cli 2026.8.0 → 2026.8.2

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 (70) hide show
  1. package/README.md +33 -0
  2. package/agentic/code/providers/capability-matrix.yaml +511 -0
  3. package/agentic/code/providers/model-capabilities.v1.json +120 -0
  4. package/agentic/code/providers/model-catalog.v1.json +96 -0
  5. package/agentic/code/providers/model-policy-evaluations.v1.json +50 -0
  6. package/agentic/code/providers/premium-model-allowlist.v1.json +36 -0
  7. package/bin/aiwg.mjs +14 -10
  8. package/dist/src/api/index.d.ts +1 -0
  9. package/dist/src/api/index.js +1 -0
  10. package/dist/src/artifacts/cli.js +2 -0
  11. package/dist/src/artifacts/types.js +4 -0
  12. package/dist/src/auth/client.js +209 -0
  13. package/dist/src/auth/config.js +38 -0
  14. package/dist/src/auth/credential-store.js +141 -0
  15. package/dist/src/auth/resource-credentials.js +25 -0
  16. package/dist/src/auth/types.js +2 -0
  17. package/dist/src/channel/manager.mjs +5 -5
  18. package/dist/src/cli/handlers/auth.js +125 -0
  19. package/dist/src/cli/handlers/help.js +1 -0
  20. package/dist/src/cli/handlers/index.js +3 -1
  21. package/dist/src/cli/handlers/install.js +42 -4
  22. package/dist/src/cli/handlers/marketplace.js +375 -122
  23. package/dist/src/cli/handlers/resource-versions.js +2 -0
  24. package/dist/src/cli/handlers/sessions.js +23 -5
  25. package/dist/src/cli/handlers/subcommands.js +10 -1
  26. package/dist/src/cli/handlers/use.js +342 -43
  27. package/dist/src/config/gitignore.js +1 -0
  28. package/dist/src/extensions/commands/definitions.js +19 -0
  29. package/dist/src/marketplace/exchange.js +602 -0
  30. package/dist/src/marketplace/provenance-types.js +19 -0
  31. package/dist/src/marketplace/provenance.js +834 -0
  32. package/dist/src/memory/canonical-context.js +342 -0
  33. package/dist/src/memory/context-pack.js +282 -0
  34. package/dist/src/memory/index.js +4 -0
  35. package/dist/src/memory/intake.js +118 -0
  36. package/dist/src/packages/adapters/git.js +79 -29
  37. package/dist/src/packages/package-discovery.js +81 -0
  38. package/dist/src/packages/package-registry.js +2 -0
  39. package/dist/src/packages/registry.js +119 -20
  40. package/dist/src/resources/resolver.js +1 -0
  41. package/dist/src/resources/web-release.d.ts +3 -1
  42. package/dist/src/resources/web-release.js +14 -6
  43. package/dist/src/serve/agentic-sandbox-fleet-client.js +213 -0
  44. package/dist/src/serve/fleet-mission-conductor.js +293 -0
  45. package/dist/src/sessions/index.js +1 -0
  46. package/dist/src/sessions/output-registration.js +338 -0
  47. package/dist/src/sessions/promotion.js +73 -2
  48. package/dist/src/sessions/repository.js +2 -1
  49. package/dist/src/update/notifier.mjs +13 -2
  50. package/package.json +8 -1
  51. package/tools/_resolve-impl.mjs +74 -0
  52. package/tools/agents/deploy-agents.mjs +962 -0
  53. package/tools/agents/providers/base.mjs +2954 -0
  54. package/tools/agents/providers/claude.mjs +711 -0
  55. package/tools/agents/providers/codex.mjs +699 -0
  56. package/tools/agents/providers/copilot.mjs +659 -0
  57. package/tools/agents/providers/cursor.mjs +714 -0
  58. package/tools/agents/providers/factory.mjs +1130 -0
  59. package/tools/agents/providers/hermes.mjs +663 -0
  60. package/tools/agents/providers/hook-capabilities.mjs +85 -0
  61. package/tools/agents/providers/model-role.mjs +56 -0
  62. package/tools/agents/providers/openclaw-translator.mjs +348 -0
  63. package/tools/agents/providers/openclaw.mjs +680 -0
  64. package/tools/agents/providers/opencode.mjs +675 -0
  65. package/tools/agents/providers/openhuman.mjs +292 -0
  66. package/tools/agents/providers/warp.mjs +413 -0
  67. package/tools/agents/providers/windsurf.mjs +748 -0
  68. package/tools/commands/deploy-prompts-codex.mjs +336 -0
  69. package/tools/plugin/package-plugins.mjs +1013 -0
  70. package/tools/skills/deploy-skills-codex.mjs +571 -0
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Provider Hook Capabilities
3
+ *
4
+ * Defines @-link support, hook file names, and directives for each provider.
5
+ * Used by postDeploy to write hook files and wire directives.
6
+ *
7
+ * @issue #444
8
+ */
9
+
10
+ export const PROVIDER_CAPABILITIES = {
11
+ claude: {
12
+ atLinkSupport: true,
13
+ hookFileName: 'AIWG.md',
14
+ hookDirective: '@AIWG.md',
15
+ contextFile: 'CLAUDE.md',
16
+ },
17
+ warp: {
18
+ atLinkSupport: true,
19
+ hookFileName: 'AIWG-warp.md',
20
+ hookDirective: '@AIWG-warp.md',
21
+ contextFile: 'WARP.md',
22
+ },
23
+ cursor: {
24
+ atLinkSupport: true,
25
+ hookFileName: 'AIWG-cursor.md',
26
+ hookDirective: '@AIWG-cursor.md',
27
+ // PUW-037 (#1138): .cursorrules is deprecated by Cursor in favor of
28
+ // AGENTS.md + .cursor/rules/*.mdc with MDC frontmatter. AGENTS.md is
29
+ // the canonical context file going forward.
30
+ contextFile: 'AGENTS.md',
31
+ },
32
+ copilot: {
33
+ atLinkSupport: false,
34
+ hookFileName: 'AIWG-copilot.md',
35
+ hookDirective: null,
36
+ contextFile: '.github/copilot-instructions.md',
37
+ fallback: 'full-inject',
38
+ },
39
+ windsurf: {
40
+ atLinkSupport: false,
41
+ hookFileName: 'AIWG-windsurf.md',
42
+ hookDirective: null,
43
+ contextFile: 'AGENTS.md',
44
+ fallback: 'full-inject',
45
+ },
46
+ factory: {
47
+ atLinkSupport: false,
48
+ hookFileName: 'AIWG-factory.md',
49
+ hookDirective: null,
50
+ contextFile: 'AGENTS.md',
51
+ fallback: 'full-inject',
52
+ },
53
+ opencode: {
54
+ atLinkSupport: true,
55
+ hookFileName: 'AIWG-opencode.md',
56
+ hookDirective: '@AIWG-opencode.md',
57
+ contextFile: '.opencode/context.md',
58
+ },
59
+ codex: {
60
+ atLinkSupport: false,
61
+ hookFileName: 'AIWG-codex.md',
62
+ hookDirective: null,
63
+ contextFile: 'CODEX.md',
64
+ fallback: 'full-inject',
65
+ },
66
+ };
67
+
68
+ /**
69
+ * Get hook capability for a provider.
70
+ * @param {string} provider
71
+ * @returns {object|null}
72
+ */
73
+ export function getHookCapability(provider) {
74
+ return PROVIDER_CAPABILITIES[provider] || null;
75
+ }
76
+
77
+ /**
78
+ * Check if a provider supports @-link hook files.
79
+ * @param {string} provider
80
+ * @returns {boolean}
81
+ */
82
+ export function supportsAtLink(provider) {
83
+ const cap = PROVIDER_CAPABILITIES[provider];
84
+ return cap ? cap.atLinkSupport : false;
85
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Provider-neutral model-role classification.
3
+ *
4
+ * Canonical agents historically used Claude family aliases, then moved to
5
+ * pinned and provider-qualified identifiers. Deployment and filtering must
6
+ * classify the family consistently without treating an unknown identifier as
7
+ * coding.
8
+ *
9
+ * @implements #1801
10
+ */
11
+
12
+ const ROLE_PATTERNS = [
13
+ ['reasoning', /(?:^|[/:._-])opus(?:$|[/:._-]|\d)/i],
14
+ ['coding', /(?:^|[/:._-])sonnet(?:$|[/:._-]|\d)/i],
15
+ ['efficiency', /(?:^|[/:._-])haiku(?:$|[/:._-]|\d)/i],
16
+ ];
17
+
18
+ /**
19
+ * @typedef {'reasoning' | 'coding' | 'efficiency' | 'unknown'} ModelRole
20
+ */
21
+
22
+ /**
23
+ * Classify aliases, pinned family IDs, and provider-qualified IDs.
24
+ *
25
+ * Missing model metadata is handled separately from an explicit unknown pin:
26
+ * callers may supply `defaultRole` for legacy artifacts that omit the field.
27
+ *
28
+ * @param {unknown} model
29
+ * @param {{ defaultRole?: Exclude<ModelRole, 'unknown'> }} [options]
30
+ * @returns {ModelRole}
31
+ */
32
+ export function classifyModelRole(model, options = {}) {
33
+ if (model === undefined || model === null || String(model).trim() === '') {
34
+ return options.defaultRole ?? 'unknown';
35
+ }
36
+
37
+ const normalized = String(model).trim().replace(/^['"]|['"]$/g, '');
38
+ for (const [role, pattern] of ROLE_PATTERNS) {
39
+ if (pattern.test(normalized)) return /** @type {ModelRole} */ (role);
40
+ }
41
+
42
+ return 'unknown';
43
+ }
44
+
45
+ /**
46
+ * Select a role-specific model without manufacturing a coding classification.
47
+ *
48
+ * @param {unknown} originalModel
49
+ * @param {{reasoning: string, coding: string, efficiency: string}} models
50
+ * @param {{ defaultRole?: 'reasoning' | 'coding' | 'efficiency' }} [options]
51
+ * @returns {string | null}
52
+ */
53
+ export function modelForRole(originalModel, models, options = {}) {
54
+ const role = classifyModelRole(originalModel, options);
55
+ return role === 'unknown' ? null : models[role];
56
+ }
@@ -0,0 +1,348 @@
1
+ /**
2
+ * OpenClaw behavior → hook translator (PUW-008 / #1109, PUW-009 / #1110).
3
+ *
4
+ * Per ADR-3 §1, AIWG hook source (BEHAVIOR.md frontmatter + scripts/) is
5
+ * translated to OpenClaw-native hook artifacts (HOOK.md + handler.js) at
6
+ * deploy time. The original BEHAVIOR.md continues to deploy under
7
+ * ~/.openclaw/behaviors/<name>/ per ADR-1 §0.6 always-deploy invariant
8
+ * (operator visibility); this translator additionally emits to
9
+ * ~/.openclaw/hooks/<name>/ which is the path OpenClaw natively scans.
10
+ *
11
+ * AIWG behavior YAML frontmatter has a `hooks:` field shaped like:
12
+ *
13
+ * hooks:
14
+ * on_file_write:
15
+ * - filter: '**\/*.{ts,js}'
16
+ * action: run_script
17
+ * script: scripts/scan.sh
18
+ * on_deploy:
19
+ * - action: run_script
20
+ * script: scripts/post-deploy.sh
21
+ *
22
+ * The translator emits one HOOK.md per behavior carrying frontmatter that
23
+ * lists the events the hook subscribes to, plus a handler.js that
24
+ * dispatches to the right script based on the event the loader passes in.
25
+ *
26
+ * @implements ADR-3 (.aiwg/architecture/adr-hook-deployment-generalization.md)
27
+ * @issue #1109 #1110
28
+ */
29
+
30
+ import * as fs from 'node:fs';
31
+ import * as path from 'node:path';
32
+
33
+ /**
34
+ * Parse YAML frontmatter from BEHAVIOR.md. Returns null when no frontmatter
35
+ * is present or the parse fails — the caller skips translation in that case.
36
+ *
37
+ * Lightweight parser that handles the subset of YAML used in BEHAVIOR.md
38
+ * frontmatter (key:value, lists, nested maps). For broader YAML support a
39
+ * library would be needed, but the format is constrained.
40
+ */
41
+ function parseBehaviorFrontmatter(content) {
42
+ const match = /^---\s*\n([\s\S]*?)\n---/.exec(content);
43
+ if (!match) return null;
44
+
45
+ const fmRaw = match[1];
46
+ const lines = fmRaw.split('\n');
47
+ const frontmatter = {};
48
+
49
+ // Top-level fields we care about
50
+ let inHooks = false;
51
+ let currentEvent = null;
52
+ let currentEntry = null;
53
+
54
+ for (let i = 0; i < lines.length; i++) {
55
+ const line = lines[i];
56
+ if (!line.trim()) continue;
57
+
58
+ // Top-level fields (no indentation)
59
+ if (!/^\s/.test(line)) {
60
+ inHooks = false;
61
+ currentEvent = null;
62
+ currentEntry = null;
63
+
64
+ const nameMatch = /^name:\s*(.+)/.exec(line);
65
+ if (nameMatch) { frontmatter.name = nameMatch[1].trim(); continue; }
66
+
67
+ const versionMatch = /^version:\s*(.+)/.exec(line);
68
+ if (versionMatch) { frontmatter.version = versionMatch[1].trim(); continue; }
69
+
70
+ const descMatch = /^description:\s*(.+)?/.exec(line);
71
+ if (descMatch) {
72
+ // Multi-line descriptions continue until next top-level key.
73
+ let desc = (descMatch[1] || '').trim();
74
+ let j = i + 1;
75
+ while (j < lines.length && /^\s+/.test(lines[j]) && !/^\s+\w+:/.test(lines[j])) {
76
+ desc += ' ' + lines[j].trim();
77
+ j++;
78
+ }
79
+ frontmatter.description = desc;
80
+ i = j - 1;
81
+ continue;
82
+ }
83
+
84
+ if (/^hooks:/.test(line)) {
85
+ inHooks = true;
86
+ frontmatter.hooks = {};
87
+ continue;
88
+ }
89
+ continue;
90
+ }
91
+
92
+ // Inside `hooks:` block
93
+ if (inHooks) {
94
+ // Event key (one level of indentation, ends with colon, no list dash)
95
+ const eventMatch = /^\s{2,4}([a-z_][a-z0-9_]*):\s*$/i.exec(line);
96
+ if (eventMatch) {
97
+ currentEvent = eventMatch[1];
98
+ frontmatter.hooks[currentEvent] = [];
99
+ currentEntry = null;
100
+ continue;
101
+ }
102
+
103
+ // List entry start (dash with field)
104
+ const listStart = /^\s+-\s*(\w+):\s*(.+)?/.exec(line);
105
+ if (listStart && currentEvent) {
106
+ currentEntry = { [listStart[1]]: (listStart[2] || '').trim().replace(/^['"]|['"]$/g, '') };
107
+ frontmatter.hooks[currentEvent].push(currentEntry);
108
+ continue;
109
+ }
110
+
111
+ // Continuation field within a list entry
112
+ const fieldMatch = /^\s+(\w+):\s*(.+)/.exec(line);
113
+ if (fieldMatch && currentEntry) {
114
+ currentEntry[fieldMatch[1]] = fieldMatch[2].trim().replace(/^['"]|['"]$/g, '');
115
+ continue;
116
+ }
117
+ }
118
+ }
119
+
120
+ return frontmatter;
121
+ }
122
+
123
+ /**
124
+ * Render the HOOK.md content for OpenClaw's hook loader.
125
+ *
126
+ * Frontmatter fields:
127
+ * - name: behavior name
128
+ * - description: short description
129
+ * - events: list of event names this hook subscribes to
130
+ * - handler: relative path to the JS handler
131
+ * - aiwg_managed: provenance signature per ADR-3
132
+ */
133
+ export function renderHookMd(behavior) {
134
+ const events = Object.keys(behavior.hooks || {});
135
+ const eventsList = events.length > 0
136
+ ? events.map((e) => ` - ${e}`).join('\n')
137
+ : ' []';
138
+
139
+ const description = (behavior.description || '').replace(/"/g, '\\"').slice(0, 200);
140
+
141
+ return [
142
+ '---',
143
+ `name: ${behavior.name}`,
144
+ `description: "${description}"`,
145
+ 'events:',
146
+ eventsList,
147
+ 'handler: ./handler.js',
148
+ 'aiwg_managed: true',
149
+ '---',
150
+ '',
151
+ `# ${behavior.name}`,
152
+ '',
153
+ behavior.description || '',
154
+ '',
155
+ `Generated by AIWG from \`agentic/code/behaviors/${behavior.name}/BEHAVIOR.md\`.`,
156
+ `Edits to this file are overwritten on \`aiwg use\`. Edit the source BEHAVIOR.md instead.`,
157
+ '',
158
+ ].join('\n');
159
+ }
160
+
161
+ /**
162
+ * Render the handler.js dispatcher for one behavior.
163
+ *
164
+ * The handler receives an event payload from OpenClaw's hook loader and
165
+ * dispatches to the correct script based on the event name. Per ADR-3 §3,
166
+ * the stdin schema is normalized to the AIWG canonical shape:
167
+ *
168
+ * { event, tool, args, project_dir, session_id }
169
+ *
170
+ * Per ADR-3 §4, the handler exits 0 (allow), 1 (block), or 2 (warn).
171
+ *
172
+ * The handler is intentionally minimal — it forwards execution to the
173
+ * behavior's existing scripts/*.sh files. AIWG never invents new logic;
174
+ * the translator is a transport-layer change only.
175
+ */
176
+ export function renderHandlerJs(behavior, scriptsRelDir = './scripts') {
177
+ // Build an event → scripts mapping from the hooks: frontmatter.
178
+ const dispatchTable = {};
179
+ for (const [event, entries] of Object.entries(behavior.hooks || {})) {
180
+ dispatchTable[event] = (entries || [])
181
+ .filter((e) => e.action === 'run_script' && e.script)
182
+ .map((e) => ({
183
+ script: e.script.startsWith('scripts/')
184
+ ? e.script.slice('scripts/'.length)
185
+ : e.script,
186
+ filter: e.filter || null,
187
+ }));
188
+ }
189
+
190
+ // Stable JSON for the dispatch table embedded in the handler source.
191
+ const dispatchJson = JSON.stringify(dispatchTable, null, 2)
192
+ .split('\n').map((l) => ' ' + l).join('\n').trim();
193
+
194
+ return `#!/usr/bin/env node
195
+ // Generated by AIWG (PUW-008/PUW-009). Do not edit; regenerated on \`aiwg use\`.
196
+ // Handler for OpenClaw hook \`${behavior.name}\`. Dispatches to scripts in
197
+ // ./scripts/ based on the event delivered by OpenClaw's hook loader.
198
+ //
199
+ // Stdin contract (AIWG canonical, per ADR-3 §3):
200
+ // { event, tool, args, project_dir, session_id }
201
+ // Exit codes (per ADR-3 §4):
202
+ // 0 = allow, 1 = block, 2 = warn-and-continue
203
+
204
+ import { spawnSync } from 'node:child_process';
205
+ import { readFileSync } from 'node:fs';
206
+ import { dirname, join, resolve } from 'node:path';
207
+ import { fileURLToPath } from 'node:url';
208
+ import { minimatch } from 'minimatch';
209
+
210
+ const __dirname = dirname(fileURLToPath(import.meta.url));
211
+ const SCRIPTS_DIR = resolve(__dirname, ${JSON.stringify(scriptsRelDir)});
212
+
213
+ const DISPATCH = ${dispatchJson};
214
+
215
+ function readStdin() {
216
+ try {
217
+ const data = readFileSync(0, 'utf8');
218
+ return data ? JSON.parse(data) : {};
219
+ } catch {
220
+ return {};
221
+ }
222
+ }
223
+
224
+ function pathMatches(filter, candidate) {
225
+ if (!filter) return true;
226
+ if (!candidate) return false;
227
+ try {
228
+ return minimatch(candidate, filter, { matchBase: true });
229
+ } catch {
230
+ return true;
231
+ }
232
+ }
233
+
234
+ const payload = readStdin();
235
+ const event = process.env.AIWG_HOOK_EVENT || payload.event;
236
+ const targetPath = payload.path || payload.file || (payload.args && payload.args[0]) || null;
237
+
238
+ const handlers = DISPATCH[event] || [];
239
+ if (handlers.length === 0) {
240
+ process.exit(0);
241
+ }
242
+
243
+ let worstExit = 0;
244
+ for (const h of handlers) {
245
+ if (!pathMatches(h.filter, targetPath)) continue;
246
+ const scriptPath = join(SCRIPTS_DIR, h.script);
247
+ const result = spawnSync(scriptPath, [], {
248
+ stdio: ['pipe', 'inherit', 'inherit'],
249
+ input: JSON.stringify(payload),
250
+ env: {
251
+ ...process.env,
252
+ AIWG_HOOK_EVENT: event,
253
+ AIWG_PROJECT_DIR: payload.project_dir || process.env.AIWG_PROJECT_DIR || '',
254
+ AIWG_TOOL_NAME: payload.tool || '',
255
+ },
256
+ });
257
+ if (result.status !== null && result.status > worstExit) {
258
+ worstExit = result.status;
259
+ }
260
+ }
261
+ process.exit(worstExit > 2 ? 1 : worstExit);
262
+ `;
263
+ }
264
+
265
+ /**
266
+ * Translate one BEHAVIOR.md into HOOK.md + handler.js artifacts.
267
+ *
268
+ * @param {string} behaviorDir absolute path of source behavior directory
269
+ * @param {string} hookDestDir absolute path of destination hook directory
270
+ * @param {object} opts deploy options (dryRun supported)
271
+ * @returns {{ ok: boolean, reason?: string, hookMdPath?: string, handlerPath?: string }}
272
+ */
273
+ export function translateBehaviorToHook(behaviorDir, hookDestDir, opts = {}) {
274
+ const behaviorMdPath = path.join(behaviorDir, 'BEHAVIOR.md');
275
+ if (!fs.existsSync(behaviorMdPath)) {
276
+ return { ok: false, reason: 'BEHAVIOR.md not found' };
277
+ }
278
+
279
+ const content = fs.readFileSync(behaviorMdPath, 'utf8');
280
+ const fm = parseBehaviorFrontmatter(content);
281
+ if (!fm || !fm.name) {
282
+ return { ok: false, reason: 'BEHAVIOR.md frontmatter missing or unparseable' };
283
+ }
284
+
285
+ // Behaviors without a `hooks:` block don't have anything for OpenClaw's
286
+ // hook loader to subscribe to (e.g. `mode: agent` behaviors). Skip them.
287
+ if (!fm.hooks || Object.keys(fm.hooks).length === 0) {
288
+ return { ok: false, reason: 'no hooks declared in frontmatter' };
289
+ }
290
+
291
+ const hookMd = renderHookMd(fm);
292
+ const handlerJs = renderHandlerJs(fm);
293
+
294
+ if (opts.dryRun) {
295
+ return { ok: true, hookMdPath: '<dry-run>', handlerPath: '<dry-run>' };
296
+ }
297
+
298
+ fs.mkdirSync(hookDestDir, { recursive: true });
299
+ const hookMdPath = path.join(hookDestDir, 'HOOK.md');
300
+ const handlerPath = path.join(hookDestDir, 'handler.js');
301
+ fs.writeFileSync(hookMdPath, hookMd, 'utf8');
302
+ fs.writeFileSync(handlerPath, handlerJs, 'utf8');
303
+ try { fs.chmodSync(handlerPath, 0o755); } catch { /* non-POSIX */ }
304
+
305
+ // Also copy scripts/ alongside the handler so it can find them by
306
+ // relative path. The behavior's BEHAVIOR.md references scripts/<name>.sh
307
+ // and the handler's spawnSync uses the same convention.
308
+ const sourceScripts = path.join(behaviorDir, 'scripts');
309
+ if (fs.existsSync(sourceScripts)) {
310
+ const destScripts = path.join(hookDestDir, 'scripts');
311
+ fs.mkdirSync(destScripts, { recursive: true });
312
+ for (const entry of fs.readdirSync(sourceScripts)) {
313
+ const src = path.join(sourceScripts, entry);
314
+ const dst = path.join(destScripts, entry);
315
+ if (fs.statSync(src).isFile()) {
316
+ fs.copyFileSync(src, dst);
317
+ try { fs.chmodSync(dst, 0o755); } catch { /* non-POSIX */ }
318
+ }
319
+ }
320
+ }
321
+
322
+ return { ok: true, hookMdPath, handlerPath };
323
+ }
324
+
325
+ /**
326
+ * Translate every behavior under behaviorsRoot into hooks under hooksRoot.
327
+ *
328
+ * @returns {{ translated: number, skipped: Array<{name: string, reason: string}> }}
329
+ */
330
+ export function translateAllBehaviors(behaviorsRoot, hooksRoot, opts = {}) {
331
+ const result = { translated: 0, skipped: [] };
332
+ if (!fs.existsSync(behaviorsRoot)) return result;
333
+
334
+ const entries = fs.readdirSync(behaviorsRoot, { withFileTypes: true });
335
+ for (const entry of entries) {
336
+ if (!entry.isDirectory()) continue;
337
+ const behaviorDir = path.join(behaviorsRoot, entry.name);
338
+ const hookDir = path.join(hooksRoot, entry.name);
339
+ const r = translateBehaviorToHook(behaviorDir, hookDir, opts);
340
+ if (r.ok) {
341
+ result.translated++;
342
+ } else {
343
+ result.skipped.push({ name: entry.name, reason: r.reason });
344
+ }
345
+ }
346
+
347
+ return result;
348
+ }