@ghl-ai/aw 0.1.70-beta.2 → 0.1.70-beta.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/git.mjs CHANGED
@@ -5,14 +5,7 @@ import { mkdtempSync, existsSync, lstatSync, rmSync, readFileSync, symlinkSync,
5
5
  import { join, basename, dirname } from 'node:path';
6
6
  import { homedir, tmpdir } from 'node:os';
7
7
  import { promisify } from 'node:util';
8
- import {
9
- REGISTRY_BASE_BRANCH,
10
- REGISTRY_DIR,
11
- AW_REGISTRY_NAMESPACE_DIR,
12
- DOCS_SOURCE_DIR,
13
- AW_DOCS_DIR,
14
- RULES_SOURCE_DIR,
15
- } from './constants.mjs';
8
+ import { REGISTRY_BASE_BRANCH, REGISTRY_DIR, DOCS_SOURCE_DIR, AW_DOCS_DIR, RULES_SOURCE_DIR } from './constants.mjs';
16
9
 
17
10
  const exec = promisify(execCb);
18
11
 
@@ -162,7 +155,6 @@ export function includeToSparsePaths(paths) {
162
155
  }
163
156
  result.add(`${REGISTRY_DIR}/AW-PROTOCOL.md`);
164
157
  if (paths.includes('platform')) {
165
- result.add(AW_REGISTRY_NAMESPACE_DIR);
166
158
  result.add(DOCS_SOURCE_DIR);
167
159
  result.add(AW_DOCS_DIR);
168
160
  result.add(RULES_SOURCE_DIR);
@@ -397,9 +389,14 @@ export async function fetchAndMerge(awHome, { silent = true } = {}) {
397
389
  // git 2.46+ bug on blob:none + no-cone sparse-checkout repos that silently
398
390
  // drops bare-name patterns (e.g. "content", "CODEOWNERS") when HEAD advances.
399
391
  //
400
- // --autostash: AW can have local registry edits pending when sync runs.
401
- // Without autostash, git refuses to rebase and the pull silently aborts.
402
- // Autostash stashes dirty bytes, rebases cleanly, and reapplies them.
392
+ // --autostash: AW writes into the registry working tree from external
393
+ // sources (ensureAwRuntimeHook copies ~/.aw-ecc/.../session-start.sh into a
394
+ // tracked path; transformCursorAwRefs rewrites /aw: /aw- through Cursor
395
+ // skill directory symlinks that resolve into .aw_registry/). When those
396
+ // versions drift, the working tree is dirty at rebase time and rebase
397
+ // refuses to run, silently aborting the entire pull. Autostash stashes the
398
+ // dirty bytes, rebases cleanly, and reapplies them — so the registry stays
399
+ // in sync even when external sources have leaked into the tracked tree.
403
400
  try {
404
401
  await exec(`git -C "${awHome}" rebase --autostash origin/${REGISTRY_BASE_BRANCH}`);
405
402
  updated = true;
@@ -26,10 +26,10 @@ const CODEX_HOME_PHASE_BLUEPRINTS = {
26
26
  marker: this.scriptMarker,
27
27
  phase: 'SessionStart',
28
28
  targetCandidates: [
29
- '$HOME/.aw_registry/aw/skills/using-aw-skills/hooks/session-start.sh',
30
- '$HOME/.aw/.aw_registry/aw/skills/using-aw-skills/hooks/session-start.sh',
29
+ '$HOME/.aw_registry/platform/core/skills/using-aw-skills/hooks/session-start.sh',
30
+ '$HOME/.aw/.aw_registry/platform/core/skills/using-aw-skills/hooks/session-start.sh',
31
31
  ],
32
- warningMessage: 'WARNING: AW using-aw-skills hook not found in ~/.aw_registry/aw. Run aw init or aw pull platform.',
32
+ warningMessage: 'WARNING: AW using-aw-skills hook not found in ~/.aw_registry. Run aw init or aw pull platform.',
33
33
  telemetryHookPath: '$HOME/.aw-ecc/scripts/hooks/aw-usage-session-start.js',
34
34
  harnessEnv: 'codex',
35
35
  });
package/integrate.mjs CHANGED
@@ -1,9 +1,10 @@
1
1
  // integrate.mjs — Generate commands for all IDEs, instructions (CLAUDE.md, AGENTS.md)
2
2
 
3
- import { existsSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
3
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, rmSync } from 'node:fs';
4
4
  import { dirname, join } from 'node:path';
5
5
  import { homedir } from 'node:os';
6
6
  import * as fmt from './fmt.mjs';
7
+ import * as config from './config.mjs';
7
8
  import { getLocalRegistryDir } from './git.mjs';
8
9
  import {
9
10
  generateAgentsMdRulesSection,
@@ -173,8 +174,8 @@ function applyManagedInstructionSections(content, file, rulesSections = {}, opti
173
174
  }
174
175
 
175
176
  /**
176
- * Command slash adapters are deprecated. Keep this function as a compatibility
177
- * no-op for older call sites and clean the removed generated-command folder.
177
+ * Count hand-written commands already present in the registry.
178
+ * No CLI stub generation all commands come from the registry itself.
178
179
  */
179
180
  export function generateCommands(cwd, { silent = false } = {}) {
180
181
  const awDir = getLocalRegistryDir(cwd, join(homedir(), '.aw_registry'));
@@ -183,8 +184,47 @@ export function generateCommands(cwd, { silent = false } = {}) {
183
184
  const oldGenDir = join(awDir, '.generated-commands');
184
185
  if (existsSync(oldGenDir)) rmSync(oldGenDir, { recursive: true, force: true });
185
186
 
186
- if (!silent) fmt.logSuccess('Command adapters are disabled; use registry-backed AW skills');
187
- return 0;
187
+ // Count hand-written commands across all namespaces for reporting
188
+ let count = 0;
189
+ const namespaces = getTeamNamespaces(awDir);
190
+ for (const ns of namespaces) {
191
+ const nsDir = join(awDir, ns);
192
+ if (existsSync(nsDir)) {
193
+ const cmdFiles = findFiles(nsDir, 'commands');
194
+ count += cmdFiles.length;
195
+ }
196
+ }
197
+
198
+ if (count > 0 && !silent) {
199
+ fmt.logSuccess(`Generated ${count} aw commands`);
200
+ }
201
+
202
+ return count;
203
+ }
204
+
205
+ function findFiles(dir, typeName) {
206
+ const results = [];
207
+ function walk(d) {
208
+ let entries;
209
+ try {
210
+ entries = readdirSync(d, { withFileTypes: true });
211
+ } catch { return; } // not a directory — skip gracefully
212
+ for (const entry of entries) {
213
+ if (entry.name.startsWith('.')) continue;
214
+ const full = join(d, entry.name);
215
+ if (entry.isDirectory()) {
216
+ if (entry.name === typeName) {
217
+ for (const f of readdirSync(full)) {
218
+ if (f.endsWith('.md')) results.push(join(full, f));
219
+ }
220
+ } else {
221
+ walk(full);
222
+ }
223
+ }
224
+ }
225
+ }
226
+ walk(dir);
227
+ return results;
188
228
  }
189
229
 
190
230
  /**
@@ -671,3 +711,19 @@ No active tasks. Tasks are created during workflow execution.
671
711
  fmt.logSuccess('Orchestration state ready');
672
712
  }
673
713
  }
714
+
715
+ /**
716
+ * Return top-level team namespace names from config (excludes 'platform').
717
+ * cfg.include may contain full paths like 'mobile/core/backend/agents/dev.md'
718
+ * so we extract only the first path segment (the actual namespace folder).
719
+ * E.g. ['mobile/core/backend/agents/dev.md', 'revex'] → ['mobile', 'revex']
720
+ */
721
+ function getTeamNamespaces(awDir) {
722
+ const cfg = config.load(awDir);
723
+ if (!cfg || !cfg.include) return [];
724
+ return [...new Set(
725
+ cfg.include
726
+ .filter(p => p !== 'platform')
727
+ .map(p => p.split('/')[0]),
728
+ )];
729
+ }
package/integrations.mjs CHANGED
@@ -94,6 +94,7 @@ export const INTEGRATIONS = {
94
94
  label: 'Caveman',
95
95
  installCmd: 'caveman@caveman',
96
96
  marketplaceSource: 'JuliusBrussee/caveman',
97
+ requiresGitSubmodule: true,
97
98
  description: 'Token-efficient responses — ~75% fewer output tokens (lite / full / ultra / wenyan modes)',
98
99
  teams: [], // universal — every team benefits
99
100
  requiresAuth: false,
@@ -219,6 +220,15 @@ function recordSkipped(key, type, reason, options = {}) {
219
220
  writeManifest(manifest, options.home);
220
221
  }
221
222
 
223
+ function hasGitSubmoduleSupport() {
224
+ try {
225
+ execSync('git submodule -h', { stdio: 'ignore' });
226
+ return true;
227
+ } catch {
228
+ return false;
229
+ }
230
+ }
231
+
222
232
  function removeInstalled(key, options = {}) {
223
233
  if (options.dryRun) return;
224
234
  const manifest = readManifest(options.home);
@@ -651,7 +661,10 @@ async function runUniversalInstaller(integration, key, { silent = false, home =
651
661
  });
652
662
  }
653
663
 
654
- // Run post-install commands (e.g. rtk init -g to wire the agent hook)
664
+ // Run post-install commands (e.g. rtk init -g to wire the agent hook).
665
+ // A failed post-install means the integration is not fully configured, so
666
+ // do not record it as installed. The next `aw init` can retry it.
667
+ const failedPostInstalls = [];
655
668
  for (const postCmd of integration.postInstall || []) {
656
669
  let finalPostCmd = postCmd;
657
670
  // If on Windows and we used WSL for the main install, we must use it for the post command too
@@ -662,11 +675,16 @@ async function runUniversalInstaller(integration, key, { silent = false, home =
662
675
  try {
663
676
  await execAsync(finalPostCmd, { timeout: 60 * 1000, maxBuffer: 10 * 1024 * 1024 });
664
677
  } catch (e) {
665
- if (!silent) fmt.logWarn(`Post-install step failed: ${finalPostCmd} — ${e.message}`);
666
- // Non-fatal user can re-run later
678
+ failedPostInstalls.push(finalPostCmd);
679
+ if (!silent) fmt.logWarn(`Post-install step failed: ${finalPostCmd} ${getErrorMessage(e)}`);
667
680
  }
668
681
  }
669
682
 
683
+ if (failedPostInstalls.length > 0) {
684
+ if (!silent) spinner.stop(chalk.yellow('Post-install failed'));
685
+ return false;
686
+ }
687
+
670
688
  if (!silent) spinner.stop('✓ Installed');
671
689
  return true;
672
690
  } catch (e) {
@@ -739,6 +757,13 @@ export async function installIntegration(key, options = {}) {
739
757
 
740
758
  try {
741
759
  if (integration.type === 'plugin') {
760
+ if (integration.requiresGitSubmodule && !hasGitSubmoduleSupport()) {
761
+ if (!silent) {
762
+ fmt.logWarn(`${integration.label} skipped — this Git installation does not include submodule support.`);
763
+ }
764
+ return false;
765
+ }
766
+
742
767
  // PLUGIN: Run claude plugin install
743
768
  await runClaudePlugin(integration.installCmd, { silent, marketplaceSource: integration.marketplaceSource ?? null });
744
769
  recordInstalled(key, 'plugin', { home });
@@ -1003,6 +1028,52 @@ export function suggestForTeam(namespace) {
1003
1028
  .map(([key]) => key);
1004
1029
  }
1005
1030
 
1031
+ export function getSuggestedIntegrationStatuses(namespace, options = {}) {
1032
+ const manifest = readManifest(options.home);
1033
+
1034
+ return suggestForTeam(namespace).map((key) => {
1035
+ const integration = INTEGRATIONS[key];
1036
+ const entry = manifest.installed[key];
1037
+
1038
+ if (entry && entry.status !== 'skipped') {
1039
+ return {
1040
+ key,
1041
+ label: integration.label,
1042
+ type: integration.type,
1043
+ status: 'installed',
1044
+ installedAt: entry.installedAt || null,
1045
+ };
1046
+ }
1047
+
1048
+ if (entry?.status === 'skipped') {
1049
+ return {
1050
+ key,
1051
+ label: integration.label,
1052
+ type: integration.type,
1053
+ status: 'skipped',
1054
+ reason: entry.reason || 'previous setup skipped',
1055
+ };
1056
+ }
1057
+
1058
+ if (integration.requiresGitSubmodule && !hasGitSubmoduleSupport()) {
1059
+ return {
1060
+ key,
1061
+ label: integration.label,
1062
+ type: integration.type,
1063
+ status: 'skipped',
1064
+ reason: 'Git submodule support unavailable',
1065
+ };
1066
+ }
1067
+
1068
+ return {
1069
+ key,
1070
+ label: integration.label,
1071
+ type: integration.type,
1072
+ status: 'pending',
1073
+ };
1074
+ });
1075
+ }
1076
+
1006
1077
  // ────────────────────────────────────────────────────────────────────────────────
1007
1078
  // AUTO-INSTALL (called from init.mjs - installs suggested integrations)
1008
1079
  // ────────────────────────────────────────────────────────────────────────────────
package/link.mjs CHANGED
@@ -1,10 +1,9 @@
1
1
  // link.mjs — Create symlinks from IDE dirs → .aw_registry/
2
2
 
3
- import { existsSync, lstatSync, mkdirSync, readdirSync, unlinkSync, symlinkSync, rmdirSync, realpathSync, rmSync } from 'node:fs';
4
- import { dirname, join, relative } from 'node:path';
3
+ import { existsSync, lstatSync, mkdirSync, readdirSync, unlinkSync, symlinkSync, rmdirSync, realpathSync } from 'node:fs';
4
+ import { join, relative } from 'node:path';
5
5
  import { homedir } from 'node:os';
6
6
  import * as fmt from './fmt.mjs';
7
- import { ensureCommandSurface } from './c4/commandSurface.mjs';
8
7
  import { getLocalRegistryDir } from './git.mjs';
9
8
 
10
9
  function forceSymlink(target, linkPath) {
@@ -131,31 +130,6 @@ function cleanIdeSymlinks(cwd) {
131
130
  }
132
131
  }
133
132
 
134
- function pruneManagedAwCommandNamespaces(cwd) {
135
- let pruned = 0;
136
- for (const ide of IDE_DIRS) {
137
- const commandDir = join(cwd, ide, 'commands', 'aw');
138
- if (!existsSync(commandDir)) continue;
139
- try {
140
- rmSync(commandDir, { recursive: true, force: true });
141
- pruned++;
142
- } catch {
143
- // Best effort: stale command cleanup should not block registry linking.
144
- }
145
- }
146
- return pruned;
147
- }
148
-
149
- function ensureSkillSlashAdapters(cwd, awDir) {
150
- const awHome = dirname(awDir);
151
- let linked = 0;
152
- for (const harness of ['cursor-cloud', 'codex-web']) {
153
- const result = ensureCommandSurface({ harness, home: cwd, awHome });
154
- linked += result.found.length;
155
- }
156
- return linked;
157
- }
158
-
159
133
  /**
160
134
  * Remove all symlinks in a directory, then prune empty subdirectories.
161
135
  * Walks depth-first so children are cleaned before parents.
@@ -184,12 +158,10 @@ function cleanSymlinksRecursive(dir) {
184
158
  }
185
159
 
186
160
  /**
187
- * Compute flat IDE names. The top-level `aw` namespace is already encoded in
188
- * its skill and command names, so keep those names stable instead of producing
189
- * aw-aw-plan or aw-plan.md variants.
161
+ * Compute flat IDE name: namespace-slug (always includes namespace).
190
162
  */
191
- function flatRegistryName(ns, ...parts) {
192
- return (ns === 'aw' ? parts : [ns, ...parts]).join('-');
163
+ function flatName(ns, name) {
164
+ return `${ns}-${name}`;
193
165
  }
194
166
 
195
167
  /**
@@ -222,9 +194,6 @@ export function linkWorkspace(cwd, awDirOverride = null, { silent = false } = {}
222
194
 
223
195
  // Clean old symlinks first
224
196
  cleanIdeSymlinks(cwd);
225
- if (cwd === HOME) {
226
- pruneManagedAwCommandNamespaces(cwd);
227
- }
228
197
 
229
198
  const namespaces = listNamespaceDirs(awDir);
230
199
 
@@ -233,7 +202,7 @@ export function linkWorkspace(cwd, awDirOverride = null, { silent = false } = {}
233
202
  for (const type of FILE_TYPES) {
234
203
  for (const { typeDirPath, segments } of findNestedTypeDirs(join(awDir, ns), type)) {
235
204
  for (const file of readdirSync(typeDirPath).filter(f => f.endsWith('.md') && !f.startsWith('.'))) {
236
- const flat = flatRegistryName(ns, ...segments, file);
205
+ const flat = [ns, ...segments, file].join('-');
237
206
 
238
207
  for (const ide of IDE_DIRS) {
239
208
  const linkDir = join(cwd, ide, type);
@@ -250,7 +219,7 @@ export function linkWorkspace(cwd, awDirOverride = null, { silent = false } = {}
250
219
  // Skills: per-skill directory symlinks (recursive for nested domain dirs)
251
220
  for (const { typeDirPath: skillsDir, segments } of findNestedTypeDirs(join(awDir, ns), 'skills')) {
252
221
  for (const skill of listDirs(skillsDir)) {
253
- const flat = flatRegistryName(ns, ...segments, skill);
222
+ const flat = [ns, ...segments, skill].join('-');
254
223
 
255
224
  for (const ide of IDE_DIRS) {
256
225
  const linkDir = join(cwd, ide, 'skills');
@@ -264,7 +233,7 @@ export function linkWorkspace(cwd, awDirOverride = null, { silent = false } = {}
264
233
 
265
234
  for (const { skillDirPath, segments: skillSegments } of findSkillRootDirs(skillsDir)) {
266
235
  if (skillSegments.length <= 1) continue;
267
- const flat = flatRegistryName(ns, ...segments, ...skillSegments);
236
+ const flat = [ns, ...segments, ...skillSegments].join('-');
268
237
 
269
238
  for (const ide of IDE_DIRS) {
270
239
  const linkDir = join(cwd, ide, 'skills');
@@ -285,7 +254,7 @@ export function linkWorkspace(cwd, awDirOverride = null, { silent = false } = {}
285
254
  if (isColocated) {
286
255
  // Colocated evals: agents/evals/<slug>/ — one level of slug dirs
287
256
  for (const evalSlug of listDirs(evalsDir)) {
288
- const flat = flatRegistryName(ns, ...segments, evalSlug);
257
+ const flat = [ns, ...segments, evalSlug].join('-');
289
258
  for (const ide of IDE_DIRS) {
290
259
  const linkDir = join(cwd, ide, 'evals');
291
260
  mkdirSync(linkDir, { recursive: true });
@@ -300,7 +269,7 @@ export function linkWorkspace(cwd, awDirOverride = null, { silent = false } = {}
300
269
  for (const subType of listDirs(evalsDir)) {
301
270
  const subDir = join(evalsDir, subType);
302
271
  for (const evalName of listDirs(subDir)) {
303
- const flat = flatRegistryName(ns, ...segments, subType, evalName);
272
+ const flat = [ns, ...segments, subType, evalName].join('-');
304
273
  for (const ide of IDE_DIRS) {
305
274
  const linkDir = join(cwd, ide, 'evals');
306
275
  mkdirSync(linkDir, { recursive: true });
@@ -341,7 +310,7 @@ export function linkWorkspace(cwd, awDirOverride = null, { silent = false } = {}
341
310
  for (const { typeDirPath: skillsDir, segments } of findNestedTypeDirs(join(awDir, ns), 'skills')) {
342
311
  mkdirSync(agentsSkillsDir, { recursive: true });
343
312
  for (const skill of listDirs(skillsDir)) {
344
- const flat = flatRegistryName(ns, ...segments, skill);
313
+ const flat = [ns, ...segments, skill].join('-');
345
314
  const linkPath = join(agentsSkillsDir, flat);
346
315
  const targetPath = join(skillsDir, skill);
347
316
  const relTarget = relative(agentsSkillsDir, targetPath);
@@ -350,7 +319,7 @@ export function linkWorkspace(cwd, awDirOverride = null, { silent = false } = {}
350
319
 
351
320
  for (const { skillDirPath, segments: skillSegments } of findSkillRootDirs(skillsDir)) {
352
321
  if (skillSegments.length <= 1) continue;
353
- const flat = flatRegistryName(ns, ...segments, ...skillSegments);
322
+ const flat = [ns, ...segments, ...skillSegments].join('-');
354
323
  const linkPath = join(agentsSkillsDir, flat);
355
324
  const relTarget = relative(agentsSkillsDir, skillDirPath);
356
325
  if (linkNestedSkillRoot(relTarget, linkPath, { silent })) created++;
@@ -373,10 +342,22 @@ export function linkWorkspace(cwd, awDirOverride = null, { silent = false } = {}
373
342
  }
374
343
  }
375
344
 
376
- // Slash adapters are skills-only. Keep the harness discovery paths populated,
377
- // but point every /aw:* file at .aw_registry/aw/skills/*/SKILL.md.
378
- if (cwd === HOME) {
379
- try { created += ensureSkillSlashAdapters(cwd, awDir); } catch { /* best effort */ }
345
+ // Commands: per-file symlinks (recursive for nested domain dirs)
346
+ for (const ns of namespaces) {
347
+ for (const { typeDirPath: commandsDir, segments } of findNestedTypeDirs(join(awDir, ns), 'commands')) {
348
+ for (const file of readdirSync(commandsDir).filter(f => f.endsWith('.md') && !f.startsWith('.'))) {
349
+ const cmdFileName = [ns, ...segments, file].join('-');
350
+
351
+ for (const ide of IDE_DIRS) {
352
+ const linkDir = join(cwd, ide, 'commands', 'aw');
353
+ mkdirSync(linkDir, { recursive: true });
354
+ const linkPath = join(linkDir, cmdFileName);
355
+ const targetPath = join(commandsDir, file);
356
+ const relTarget = relative(linkDir, targetPath);
357
+ try { forceSymlink(relTarget, linkPath); created++; } catch { /* best effort */ }
358
+ }
359
+ }
360
+ }
380
361
  }
381
362
 
382
363
  // AW-PROTOCOL.md: symlink into each IDE dir root so commands can always find it
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.70-beta.2",
3
+ "version": "0.1.70-beta.4",
4
4
  "description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
5
5
  "type": "module",
6
6
  "bin": {
package/startup.mjs CHANGED
@@ -83,10 +83,8 @@ function resolveRegistryRoot(homeDir = homedir()) {
83
83
  ].find(existsSync) || null;
84
84
  }
85
85
 
86
- function awRuntimeHookDestinationPath(homeDir = homedir()) {
87
- const registryRoot = resolveRegistryRoot(homeDir);
88
- if (!registryRoot) return null;
89
- return join(registryRoot, 'aw', 'skills', 'using-aw-skills', 'hooks', 'session-start.sh');
86
+ function awRuntimeHookSourcePath(homeDir = homedir()) {
87
+ return join(homeDir, '.aw-ecc', 'skills', 'using-aw-skills', 'hooks', 'session-start.sh');
90
88
  }
91
89
 
92
90
  function readJson(filePath, fallback = {}) {
@@ -395,10 +393,27 @@ function hasCodexSessionStartScript(homeDir = homedir()) {
395
393
  }
396
394
 
397
395
  export function ensureAwRuntimeHook(homeDir = homedir()) {
398
- const destinationPath = awRuntimeHookDestinationPath(homeDir);
399
- if (!destinationPath || !existsSync(destinationPath)) return [];
396
+ const sourcePath = awRuntimeHookSourcePath(homeDir);
397
+ const registryRoot = resolveRegistryRoot(homeDir);
398
+ if (!existsSync(sourcePath) || !registryRoot) return [];
399
+
400
+ const destinationPath = join(
401
+ registryRoot,
402
+ 'platform',
403
+ 'core',
404
+ 'skills',
405
+ 'using-aw-skills',
406
+ 'hooks',
407
+ 'session-start.sh',
408
+ );
409
+ const sourceContent = readFileSync(sourcePath, 'utf8');
410
+ const existingContent = existsSync(destinationPath) ? readFileSync(destinationPath, 'utf8') : null;
411
+ if (existingContent === sourceContent) return [];
412
+
413
+ mkdirSync(dirname(destinationPath), { recursive: true });
414
+ writeFileSync(destinationPath, sourceContent);
400
415
  try { chmodSync(destinationPath, 0o755); } catch { /* best effort */ }
401
- return [];
416
+ return [destinationPath];
402
417
  }
403
418
 
404
419
  function hasCodexHooksEnabled(homeDir = homedir()) {