@ghl-ai/aw 0.1.70-beta.2 → 0.1.70-beta.3
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/c4/claudePluginRegistry.mjs +3 -3
- package/c4/commandSurface.mjs +225 -150
- package/c4/cursorRulesShim.mjs +41 -19
- package/c4/diagnostics.mjs +42 -23
- package/c4/eccRegistryBridge.mjs +236 -0
- package/c4/index.mjs +2 -1
- package/c4/preflight.mjs +18 -12
- package/c4/templates/scripts/aw-c4-bootstrap.sh +14 -14
- package/codex.mjs +4 -13
- package/commands/c4.mjs +34 -31
- package/commands/doctor.mjs +61 -24
- package/commands/init.mjs +14 -20
- package/commands/pull.mjs +1 -2
- package/constants.mjs +0 -3
- package/ecc.mjs +74 -41
- package/git.mjs +9 -12
- package/hooks/codex-home.mjs +3 -3
- package/integrate.mjs +61 -5
- package/integrations.mjs +28 -3
- package/link.mjs +28 -47
- package/package.json +1 -1
- package/startup.mjs +22 -7
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
|
-
*
|
|
177
|
-
*
|
|
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
|
-
|
|
187
|
-
|
|
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
|
-
|
|
666
|
-
|
|
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 });
|
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
|
|
4
|
-
import {
|
|
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
|
|
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
|
|
192
|
-
return
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
//
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
package/startup.mjs
CHANGED
|
@@ -83,10 +83,8 @@ function resolveRegistryRoot(homeDir = homedir()) {
|
|
|
83
83
|
].find(existsSync) || null;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
function
|
|
87
|
-
|
|
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
|
|
399
|
-
|
|
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()) {
|