@ghl-ai/aw 0.1.35 → 0.1.36-beta.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.
- package/cli.mjs +2 -1
- package/commands/init.mjs +66 -28
- package/commands/nuke.mjs +18 -9
- package/commands/push.mjs +139 -38
- package/config.mjs +2 -2
- package/ecc.mjs +180 -0
- package/fmt.mjs +2 -0
- package/integrate.mjs +8 -6
- package/package.json +3 -2
package/cli.mjs
CHANGED
|
@@ -72,7 +72,8 @@ function printHelp() {
|
|
|
72
72
|
const sec = (title) => `\n ${chalk.bold.underline(title)}`;
|
|
73
73
|
const help = [
|
|
74
74
|
sec('Setup'),
|
|
75
|
-
cmd('aw init
|
|
75
|
+
cmd('aw init', 'Initialize workspace (platform/ only)'),
|
|
76
|
+
cmd('aw init --namespace <team/sub-team>', 'Add a team namespace (optional)'),
|
|
76
77
|
` ${chalk.dim('Teams: platform, revex, mobile, commerce, leadgen, crm, marketplace, ai')}`,
|
|
77
78
|
` ${chalk.dim('Example: aw init --namespace revex/courses')}`,
|
|
78
79
|
|
package/commands/init.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// Uses core.hooksPath (git-lfs pattern) for system-wide hook interception.
|
|
5
5
|
// Uses IDE tasks for auto-pull on workspace open.
|
|
6
6
|
|
|
7
|
-
import { mkdirSync, existsSync, writeFileSync, symlinkSync } from 'node:fs';
|
|
7
|
+
import { mkdirSync, existsSync, writeFileSync, symlinkSync, readdirSync } from 'node:fs';
|
|
8
8
|
import { execSync } from 'node:child_process';
|
|
9
9
|
import { join, dirname } from 'node:path';
|
|
10
10
|
import { homedir } from 'node:os';
|
|
@@ -14,11 +14,14 @@ import * as config from '../config.mjs';
|
|
|
14
14
|
import * as fmt from '../fmt.mjs';
|
|
15
15
|
import { chalk } from '../fmt.mjs';
|
|
16
16
|
import { pullCommand, pullAsync } from './pull.mjs';
|
|
17
|
+
import { sparseCheckoutAsync, includeToSparsePaths, cleanup } from '../git.mjs';
|
|
18
|
+
import { REGISTRY_DIR, REGISTRY_REPO } from '../constants.mjs';
|
|
17
19
|
import { linkWorkspace } from '../link.mjs';
|
|
18
20
|
import { generateCommands, copyInstructions, initAwDocs } from '../integrate.mjs';
|
|
19
21
|
import { setupMcp } from '../mcp.mjs';
|
|
20
22
|
import { autoUpdate, promptUpdate } from '../update.mjs';
|
|
21
23
|
import { installGlobalHooks } from '../hooks.mjs';
|
|
24
|
+
import { installAwEcc } from '../ecc.mjs';
|
|
22
25
|
|
|
23
26
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
24
27
|
const VERSION = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')).version;
|
|
@@ -95,7 +98,7 @@ function printPullSummary(pattern, actions) {
|
|
|
95
98
|
const ALLOWED_NAMESPACES = ['platform', 'revex', 'mobile', 'commerce', 'leadgen', 'crm', 'marketplace', 'ai'];
|
|
96
99
|
|
|
97
100
|
export async function initCommand(args) {
|
|
98
|
-
|
|
101
|
+
let namespace = args['--namespace'] || null;
|
|
99
102
|
let user = args['--user'] || '';
|
|
100
103
|
const silent = args['--silent'] === true;
|
|
101
104
|
|
|
@@ -103,24 +106,12 @@ export async function initCommand(args) {
|
|
|
103
106
|
|
|
104
107
|
// ── Validate ──────────────────────────────────────────────────────────
|
|
105
108
|
|
|
106
|
-
if (!namespace && !silent) {
|
|
107
|
-
const list = ALLOWED_NAMESPACES.map(n => chalk.cyan(n)).join(', ');
|
|
108
|
-
fmt.cancel([
|
|
109
|
-
`Missing required ${chalk.bold('--namespace')} flag`,
|
|
110
|
-
'',
|
|
111
|
-
` ${chalk.dim('Usage:')} aw init --namespace <team/sub-team>`,
|
|
112
|
-
` ${chalk.dim('Teams:')} ${list}`,
|
|
113
|
-
'',
|
|
114
|
-
` ${chalk.dim('Example:')} ${chalk.bold('aw init --namespace commerce/payments')}`,
|
|
115
|
-
].join('\n'));
|
|
116
|
-
}
|
|
117
|
-
|
|
118
109
|
// Parse team/sub-team
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
110
|
+
let nsParts = namespace ? namespace.split('/') : [];
|
|
111
|
+
let team = nsParts[0] || null;
|
|
112
|
+
let subTeam = nsParts[1] || null;
|
|
113
|
+
let teamNS = subTeam ? `${team}-${subTeam}` : team; // for $TEAM_NS replacement
|
|
114
|
+
let folderName = subTeam ? `${team}/${subTeam}` : team; // for .aw_registry/ path
|
|
124
115
|
|
|
125
116
|
if (team && !ALLOWED_NAMESPACES.includes(team)) {
|
|
126
117
|
const list = ALLOWED_NAMESPACES.map(n => chalk.cyan(n)).join(', ');
|
|
@@ -151,6 +142,47 @@ export async function initCommand(args) {
|
|
|
151
142
|
fmt.cancel(`Invalid sub-team '${subTeam}' — must match: ${SLUG_RE}`);
|
|
152
143
|
}
|
|
153
144
|
|
|
145
|
+
// ── Probe remote registry to check if namespace exists ────────────────
|
|
146
|
+
|
|
147
|
+
let namespaceExistsInRemote = false;
|
|
148
|
+
if (folderName && !silent) {
|
|
149
|
+
try {
|
|
150
|
+
const probePaths = includeToSparsePaths([folderName]);
|
|
151
|
+
const probeDir = await sparseCheckoutAsync(REGISTRY_REPO, probePaths);
|
|
152
|
+
try {
|
|
153
|
+
const fullNsPath = join(probeDir, REGISTRY_DIR, ...folderName.split('/'));
|
|
154
|
+
namespaceExistsInRemote = existsSync(fullNsPath) &&
|
|
155
|
+
readdirSync(fullNsPath, { withFileTypes: true })
|
|
156
|
+
.some(d => d.isDirectory() && !d.name.startsWith('.'));
|
|
157
|
+
} finally {
|
|
158
|
+
cleanup(probeDir);
|
|
159
|
+
}
|
|
160
|
+
} catch {
|
|
161
|
+
// Network error — skip probe, proceed without prompt
|
|
162
|
+
namespaceExistsInRemote = true;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// If namespace does NOT exist in remote, ask user to confirm
|
|
167
|
+
if (folderName && !silent && !namespaceExistsInRemote && process.stdin.isTTY) {
|
|
168
|
+
const choice = await fmt.select({
|
|
169
|
+
message: `The namespace '${folderName}' does not exist in the registry yet and will be created from [template].\nplatform/ includes shared agents, skills & commands that cover most use cases.\nHow would you like to proceed?`,
|
|
170
|
+
options: [
|
|
171
|
+
{ value: 'platform-only', label: 'Continue with platform/ only (recommended for most users)' },
|
|
172
|
+
{ value: 'create-namespace', label: `Create '${folderName}' namespace from template` },
|
|
173
|
+
],
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
if (fmt.isCancel(choice)) {
|
|
177
|
+
fmt.cancel('Operation cancelled.');
|
|
178
|
+
process.exit(0);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (choice === 'platform-only') {
|
|
182
|
+
namespace = null; team = null; subTeam = null; teamNS = null; folderName = null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
154
186
|
const hasConfig = config.exists(GLOBAL_AW_DIR);
|
|
155
187
|
const hasPlatform = existsSync(join(GLOBAL_AW_DIR, 'platform'));
|
|
156
188
|
const isExisting = hasConfig && hasPlatform;
|
|
@@ -181,25 +213,30 @@ export async function initCommand(args) {
|
|
|
181
213
|
// Pull latest (parallel)
|
|
182
214
|
// cfg.include has the renamed namespace (e.g. 'revex/courses'), but the repo
|
|
183
215
|
// only has '.aw_registry/[template]/' — remap non-platform entries back.
|
|
216
|
+
// Platform is never stored in cfg.include but must always be pulled.
|
|
184
217
|
const freshCfg = config.load(GLOBAL_AW_DIR);
|
|
218
|
+
const pullJobs = [
|
|
219
|
+
pullAsync({ ...args, _positional: ['platform'], _workspaceDir: GLOBAL_AW_DIR, _skipIntegrate: true }),
|
|
220
|
+
];
|
|
185
221
|
if (freshCfg && freshCfg.include.length > 0) {
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
const derivedTeamNS =
|
|
189
|
-
|
|
222
|
+
for (const p of freshCfg.include) {
|
|
223
|
+
if (p === 'platform') continue; // already added above
|
|
224
|
+
const derivedTeamNS = p.replace(/\//g, '-');
|
|
225
|
+
pullJobs.push(pullAsync({
|
|
190
226
|
...args,
|
|
191
|
-
_positional: [
|
|
227
|
+
_positional: ['[template]'],
|
|
192
228
|
_workspaceDir: GLOBAL_AW_DIR,
|
|
193
229
|
_skipIntegrate: true,
|
|
194
|
-
_renameNamespace:
|
|
230
|
+
_renameNamespace: p,
|
|
195
231
|
_teamNS: derivedTeamNS,
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
await Promise.all(pullJobs);
|
|
232
|
+
}));
|
|
233
|
+
}
|
|
199
234
|
}
|
|
235
|
+
await Promise.all(pullJobs);
|
|
200
236
|
|
|
201
237
|
// Re-link IDE dirs + hooks (idempotent)
|
|
202
238
|
linkWorkspace(HOME);
|
|
239
|
+
await installAwEcc(cwd, { silent });
|
|
203
240
|
generateCommands(HOME);
|
|
204
241
|
copyInstructions(HOME, null, freshCfg?.namespace || team) || [];
|
|
205
242
|
initAwDocs(HOME);
|
|
@@ -280,6 +317,7 @@ export async function initCommand(args) {
|
|
|
280
317
|
// Step 3: Link IDE dirs + setup tasks
|
|
281
318
|
fmt.logStep('Linking IDE symlinks...');
|
|
282
319
|
linkWorkspace(HOME);
|
|
320
|
+
await installAwEcc(cwd, { silent });
|
|
283
321
|
generateCommands(HOME);
|
|
284
322
|
const instructionFiles = copyInstructions(HOME, null, team) || [];
|
|
285
323
|
initAwDocs(HOME);
|
package/commands/nuke.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { execSync } from 'node:child_process';
|
|
|
9
9
|
import * as fmt from '../fmt.mjs';
|
|
10
10
|
import { chalk } from '../fmt.mjs';
|
|
11
11
|
import { removeGlobalHooks } from '../hooks.mjs';
|
|
12
|
+
import { uninstallAwEcc } from '../ecc.mjs';
|
|
12
13
|
|
|
13
14
|
const HOME = homedir();
|
|
14
15
|
const GLOBAL_AW_DIR = join(HOME, '.aw_registry');
|
|
@@ -243,28 +244,31 @@ export function nukeCommand(args) {
|
|
|
243
244
|
// 2. Remove IDE symlinks (only those pointing to .aw_registry)
|
|
244
245
|
removeIdeSymlinks();
|
|
245
246
|
|
|
246
|
-
// 3. Remove
|
|
247
|
+
// 3. Remove aw-ecc installed files (agents, commands, rules, skills, hooks)
|
|
248
|
+
uninstallAwEcc();
|
|
249
|
+
|
|
250
|
+
// 4. Remove .aw_registry symlinks from ALL project directories
|
|
247
251
|
removeProjectSymlinks();
|
|
248
252
|
|
|
249
|
-
//
|
|
253
|
+
// 5. Remove git hooks (core.hooksPath + legacy template)
|
|
250
254
|
removeGitHooks(manifest);
|
|
251
255
|
|
|
252
|
-
//
|
|
256
|
+
// 6. Remove IDE auto-init tasks
|
|
253
257
|
removeIdeTasks();
|
|
254
258
|
|
|
255
|
-
//
|
|
259
|
+
// Remove upgrade lock/log (inside .aw_registry, must happen before dir removal)
|
|
256
260
|
for (const p of [join(GLOBAL_AW_DIR, '.aw-upgrade.lock'), join(GLOBAL_AW_DIR, '.aw-upgrade.log')]) {
|
|
257
261
|
try { if (existsSync(p)) rmSync(p, { recursive: true, force: true }); } catch { /* best effort */ }
|
|
258
262
|
}
|
|
259
263
|
|
|
260
|
-
//
|
|
264
|
+
// 7. Remove ~/.aw_docs/
|
|
261
265
|
const awDocs = join(HOME, '.aw_docs');
|
|
262
266
|
if (existsSync(awDocs)) {
|
|
263
267
|
rmSync(awDocs, { recursive: true, force: true });
|
|
264
268
|
fmt.logStep('Removed ~/.aw_docs/');
|
|
265
269
|
}
|
|
266
270
|
|
|
267
|
-
//
|
|
271
|
+
// 8. Remove any manual `aw` symlinks (e.g. ~/.local/bin/aw)
|
|
268
272
|
const manualBins = [
|
|
269
273
|
join(HOME, '.local', 'bin', 'aw'),
|
|
270
274
|
join(HOME, 'bin', 'aw'),
|
|
@@ -278,7 +282,7 @@ export function nukeCommand(args) {
|
|
|
278
282
|
} catch { /* doesn't exist */ }
|
|
279
283
|
}
|
|
280
284
|
|
|
281
|
-
//
|
|
285
|
+
// 9. Uninstall npm global package (skip if already in npm uninstall lifecycle)
|
|
282
286
|
if (!process.env.npm_lifecycle_event) {
|
|
283
287
|
try {
|
|
284
288
|
execSync('npm uninstall -g @ghl-ai/aw', { stdio: 'pipe', timeout: 15000 });
|
|
@@ -286,8 +290,12 @@ export function nukeCommand(args) {
|
|
|
286
290
|
} catch { /* not installed via npm or no permissions */ }
|
|
287
291
|
}
|
|
288
292
|
|
|
289
|
-
//
|
|
290
|
-
|
|
293
|
+
// 10. Remove ~/.aw_registry/ itself (source of truth — last!)
|
|
294
|
+
try {
|
|
295
|
+
rmSync(GLOBAL_AW_DIR, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
296
|
+
} catch {
|
|
297
|
+
try { execSync(`rm -rf "${GLOBAL_AW_DIR}"`, { stdio: 'pipe' }); } catch { /* best effort */ }
|
|
298
|
+
}
|
|
291
299
|
fmt.logStep('Removed ~/.aw_registry/');
|
|
292
300
|
|
|
293
301
|
fmt.outro([
|
|
@@ -295,6 +303,7 @@ export function nukeCommand(args) {
|
|
|
295
303
|
'',
|
|
296
304
|
` ${chalk.green('✓')} Generated files cleaned`,
|
|
297
305
|
` ${chalk.green('✓')} IDE symlinks cleaned`,
|
|
306
|
+
` ${chalk.green('✓')} aw-ecc engine removed`,
|
|
298
307
|
` ${chalk.green('✓')} Project symlinks cleaned`,
|
|
299
308
|
` ${chalk.green('✓')} Git hooks removed`,
|
|
300
309
|
` ${chalk.green('✓')} IDE auto-sync tasks removed`,
|
package/commands/push.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// commands/push.mjs — Push local agents/skills to registry via PR (single file or batch)
|
|
2
2
|
|
|
3
|
-
import { existsSync, statSync, mkdirSync, cpSync, mkdtempSync, readFileSync, appendFileSync, readdirSync } from 'node:fs';
|
|
3
|
+
import { existsSync, statSync, mkdirSync, cpSync, mkdtempSync, readFileSync, appendFileSync, readdirSync, unlinkSync, rmdirSync } from 'node:fs';
|
|
4
4
|
import { basename, dirname, resolve, join, relative } from 'node:path';
|
|
5
5
|
import { execSync, execFileSync } from 'node:child_process';
|
|
6
6
|
import { tmpdir } from 'node:os';
|
|
@@ -68,20 +68,40 @@ function collectBatchFiles(folderAbsPath, workspaceDir) {
|
|
|
68
68
|
namespace: entry.namespacePath,
|
|
69
69
|
slug: entry.slug,
|
|
70
70
|
isDir: false,
|
|
71
|
+
deleted: false,
|
|
71
72
|
};
|
|
72
73
|
});
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
/**
|
|
76
77
|
* Collect all modified files from manifest (for no-args push).
|
|
77
|
-
* Returns array of { absPath, registryTarget, type, namespace, slug, isDir }.
|
|
78
|
+
* Returns array of { absPath, registryTarget, type, namespace, slug, isDir, deleted }.
|
|
78
79
|
*/
|
|
79
80
|
function collectModifiedFiles(workspaceDir) {
|
|
80
81
|
const manifest = loadManifest(workspaceDir);
|
|
81
82
|
const files = [];
|
|
82
83
|
for (const [key, entry] of Object.entries(manifest.files || {})) {
|
|
83
84
|
const filePath = join(workspaceDir, key);
|
|
84
|
-
|
|
85
|
+
|
|
86
|
+
// File deleted locally — include as deletion if it was previously pushed
|
|
87
|
+
if (!existsSync(filePath)) {
|
|
88
|
+
if (entry.registry_sha256) {
|
|
89
|
+
const meta = parseManifestKey(key);
|
|
90
|
+
if (meta) {
|
|
91
|
+
files.push({
|
|
92
|
+
absPath: filePath,
|
|
93
|
+
registryTarget: `${REGISTRY_DIR}/${key}`,
|
|
94
|
+
type: meta.type,
|
|
95
|
+
namespace: meta.namespace,
|
|
96
|
+
slug: meta.slug,
|
|
97
|
+
isDir: false,
|
|
98
|
+
deleted: true,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
85
105
|
const currentHash = hashFile(filePath);
|
|
86
106
|
const isModified = currentHash !== entry.sha256;
|
|
87
107
|
const isNew = !entry.registry_sha256; // Template-derived, never pushed to remote
|
|
@@ -95,6 +115,7 @@ function collectModifiedFiles(workspaceDir) {
|
|
|
95
115
|
namespace: meta.namespace,
|
|
96
116
|
slug: meta.slug,
|
|
97
117
|
isDir: false,
|
|
118
|
+
deleted: false,
|
|
98
119
|
});
|
|
99
120
|
}
|
|
100
121
|
}
|
|
@@ -124,69 +145,108 @@ function parseManifestKey(key) {
|
|
|
124
145
|
function generateBranchName(files) {
|
|
125
146
|
const shortId = Date.now().toString(36).slice(-5);
|
|
126
147
|
const namespaces = [...new Set(files.map(f => f.namespace))];
|
|
148
|
+
const hasDeletes = files.some(f => f.deleted);
|
|
149
|
+
const allDeletes = files.every(f => f.deleted);
|
|
150
|
+
const prefix = allDeletes ? 'remove' : hasDeletes ? 'sync' : 'upload';
|
|
127
151
|
|
|
128
152
|
if (files.length === 1) {
|
|
129
153
|
const f = files[0];
|
|
130
154
|
const nsSlug = f.namespace.replace(/\//g, '-');
|
|
131
|
-
return
|
|
155
|
+
return `${prefix}/${nsSlug}-${f.type}-${f.slug}-${shortId}`;
|
|
132
156
|
}
|
|
133
157
|
|
|
134
158
|
if (namespaces.length === 1) {
|
|
135
|
-
return
|
|
159
|
+
return `${prefix}/${namespaces[0].replace(/\//g, '-')}-${shortId}`;
|
|
136
160
|
}
|
|
137
161
|
|
|
138
|
-
return
|
|
162
|
+
return `${prefix}/batch-${shortId}`;
|
|
139
163
|
}
|
|
140
164
|
|
|
141
165
|
function generatePrTitle(files) {
|
|
142
166
|
if (files.length === 1) {
|
|
143
167
|
const f = files[0];
|
|
144
|
-
|
|
168
|
+
const verb = f.deleted ? 'Remove' : 'Add';
|
|
169
|
+
const prep = f.deleted ? 'from' : 'to';
|
|
170
|
+
return `${verb} ${f.slug} (${f.type}) ${prep} ${f.namespace}`;
|
|
145
171
|
}
|
|
146
172
|
|
|
147
|
-
const
|
|
173
|
+
const addCounts = {};
|
|
174
|
+
const deleteCounts = {};
|
|
148
175
|
for (const f of files) {
|
|
149
|
-
|
|
176
|
+
const target = f.deleted ? deleteCounts : addCounts;
|
|
177
|
+
target[f.type] = (target[f.type] || 0) + 1;
|
|
178
|
+
}
|
|
179
|
+
const parts = [];
|
|
180
|
+
for (const [type, count] of Object.entries(addCounts)) {
|
|
181
|
+
parts.push(`+${count} ${type}`);
|
|
182
|
+
}
|
|
183
|
+
for (const [type, count] of Object.entries(deleteCounts)) {
|
|
184
|
+
parts.push(`-${count} ${type}`);
|
|
150
185
|
}
|
|
151
|
-
const countParts = Object.entries(counts).map(([type, count]) => `${count} ${type}`);
|
|
152
186
|
const namespaces = [...new Set(files.map(f => f.namespace))];
|
|
153
187
|
|
|
154
188
|
if (namespaces.length === 1) {
|
|
155
|
-
return `sync: ${
|
|
189
|
+
return `sync: ${parts.join(', ')} in ${namespaces[0]}`;
|
|
156
190
|
}
|
|
157
|
-
return `sync: ${
|
|
191
|
+
return `sync: ${parts.join(', ')} across ${namespaces.join(', ')}`;
|
|
158
192
|
}
|
|
159
193
|
|
|
160
194
|
function generatePrBody(files, newNamespaces) {
|
|
161
195
|
if (files.length === 1) {
|
|
162
196
|
const f = files[0];
|
|
197
|
+
const action = f.deleted ? 'Registry Removal' : 'Registry Upload';
|
|
163
198
|
const bodyParts = [
|
|
164
|
-
|
|
199
|
+
`## ${action}`,
|
|
165
200
|
'',
|
|
166
201
|
`- **Type:** ${f.type}`,
|
|
167
202
|
`- **Slug:** ${f.slug}`,
|
|
168
203
|
`- **Namespace:** ${f.namespace}`,
|
|
169
204
|
`- **Path:** \`${f.registryTarget}\``,
|
|
170
205
|
];
|
|
206
|
+
if (f.deleted) {
|
|
207
|
+
bodyParts.push('', '> File was deleted locally and removed from registry.');
|
|
208
|
+
}
|
|
171
209
|
if (newNamespaces.length > 0) {
|
|
172
210
|
bodyParts.push('', '> **New namespace** — CODEOWNERS entry auto-added.');
|
|
173
211
|
}
|
|
174
|
-
bodyParts.push('', 'Uploaded via
|
|
212
|
+
bodyParts.push('', `${f.deleted ? 'Removed' : 'Uploaded'} via \`aw push\``);
|
|
175
213
|
return bodyParts.join('\n');
|
|
176
214
|
}
|
|
177
215
|
|
|
178
|
-
// Batch body — group by type
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
if (!grouped[f.type]) grouped[f.type] = [];
|
|
182
|
-
grouped[f.type].push(f);
|
|
183
|
-
}
|
|
216
|
+
// Batch body — group by action then type
|
|
217
|
+
const added = files.filter(f => !f.deleted);
|
|
218
|
+
const deleted = files.filter(f => f.deleted);
|
|
184
219
|
|
|
185
220
|
const bodyParts = ['## Registry Sync', ''];
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
221
|
+
|
|
222
|
+
if (added.length > 0) {
|
|
223
|
+
const grouped = {};
|
|
224
|
+
for (const f of added) {
|
|
225
|
+
if (!grouped[f.type]) grouped[f.type] = [];
|
|
226
|
+
grouped[f.type].push(f);
|
|
227
|
+
}
|
|
228
|
+
bodyParts.push('### Added / Updated');
|
|
229
|
+
for (const [type, items] of Object.entries(grouped)) {
|
|
230
|
+
bodyParts.push(`**${type}**`);
|
|
231
|
+
for (const item of items.sort((a, b) => a.slug.localeCompare(b.slug))) {
|
|
232
|
+
bodyParts.push(`- \`${item.slug}\` (${item.namespace})`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
bodyParts.push('');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (deleted.length > 0) {
|
|
239
|
+
const grouped = {};
|
|
240
|
+
for (const f of deleted) {
|
|
241
|
+
if (!grouped[f.type]) grouped[f.type] = [];
|
|
242
|
+
grouped[f.type].push(f);
|
|
243
|
+
}
|
|
244
|
+
bodyParts.push('### Removed');
|
|
245
|
+
for (const [type, items] of Object.entries(grouped)) {
|
|
246
|
+
bodyParts.push(`**${type}**`);
|
|
247
|
+
for (const item of items.sort((a, b) => a.slug.localeCompare(b.slug))) {
|
|
248
|
+
bodyParts.push(`- ~~\`${item.slug}\`~~ (${item.namespace})`);
|
|
249
|
+
}
|
|
190
250
|
}
|
|
191
251
|
bodyParts.push('');
|
|
192
252
|
}
|
|
@@ -196,7 +256,7 @@ function generatePrBody(files, newNamespaces) {
|
|
|
196
256
|
bodyParts.push('');
|
|
197
257
|
}
|
|
198
258
|
|
|
199
|
-
bodyParts.push(`---`, `Synced via \`aw push\` (${files.length} files)`);
|
|
259
|
+
bodyParts.push(`---`, `Synced via \`aw push\` (${files.length} files: ${added.length} added/updated, ${deleted.length} removed)`);
|
|
200
260
|
return bodyParts.join('\n');
|
|
201
261
|
}
|
|
202
262
|
|
|
@@ -205,16 +265,22 @@ function generatePrBody(files, newNamespaces) {
|
|
|
205
265
|
function pushFiles(files, { repo, dryRun, workspaceDir }) {
|
|
206
266
|
// Summary
|
|
207
267
|
const counts = {};
|
|
268
|
+
const deleteCounts = {};
|
|
208
269
|
for (const f of files) {
|
|
209
|
-
|
|
270
|
+
const target = f.deleted ? deleteCounts : counts;
|
|
271
|
+
target[f.type] = (target[f.type] || 0) + 1;
|
|
210
272
|
}
|
|
211
|
-
const countParts =
|
|
273
|
+
const countParts = [
|
|
274
|
+
...Object.entries(counts).map(([type, count]) => `${count} ${type}`),
|
|
275
|
+
...Object.entries(deleteCounts).map(([type, count]) => `${count} ${type} removed`),
|
|
276
|
+
];
|
|
212
277
|
fmt.logInfo(`${chalk.bold(files.length)} file${files.length > 1 ? 's' : ''} to push (${countParts.join(', ')})`);
|
|
213
278
|
|
|
214
279
|
if (dryRun) {
|
|
215
280
|
for (const f of files) {
|
|
216
281
|
const ns = chalk.dim(` [${f.namespace}]`);
|
|
217
|
-
|
|
282
|
+
const label = f.deleted ? chalk.red('DELETE') : chalk.yellow(f.type);
|
|
283
|
+
fmt.logMessage(` ${label}/${f.slug}${ns}`);
|
|
218
284
|
}
|
|
219
285
|
fmt.logWarn('No changes made (--dry-run)');
|
|
220
286
|
fmt.outro(chalk.dim('Remove --dry-run to push'));
|
|
@@ -235,12 +301,14 @@ function pushFiles(files, { repo, dryRun, workspaceDir }) {
|
|
|
235
301
|
const branch = generateBranchName(files);
|
|
236
302
|
execSync(`git checkout -b ${branch}`, { cwd: tempDir, stdio: 'pipe' });
|
|
237
303
|
|
|
304
|
+
const addedFiles = files.filter(f => !f.deleted);
|
|
305
|
+
const deletedFiles = files.filter(f => f.deleted);
|
|
238
306
|
const s2 = fmt.spinner();
|
|
239
|
-
s2.start(`
|
|
307
|
+
s2.start(`Applying ${files.length} change${files.length > 1 ? 's' : ''} to registry...`);
|
|
240
308
|
|
|
241
|
-
// Copy
|
|
309
|
+
// Copy added/modified files
|
|
242
310
|
let copyErrors = [];
|
|
243
|
-
for (const file of
|
|
311
|
+
for (const file of addedFiles) {
|
|
244
312
|
try {
|
|
245
313
|
const targetFull = join(tempDir, file.registryTarget);
|
|
246
314
|
mkdirSync(dirname(targetFull), { recursive: true });
|
|
@@ -254,9 +322,33 @@ function pushFiles(files, { repo, dryRun, workspaceDir }) {
|
|
|
254
322
|
}
|
|
255
323
|
}
|
|
256
324
|
|
|
325
|
+
// Remove deleted files from the clone
|
|
326
|
+
for (const file of deletedFiles) {
|
|
327
|
+
try {
|
|
328
|
+
const targetFull = join(tempDir, file.registryTarget);
|
|
329
|
+
if (existsSync(targetFull)) {
|
|
330
|
+
unlinkSync(targetFull);
|
|
331
|
+
// Prune empty parent dirs up to REGISTRY_DIR
|
|
332
|
+
let dir = dirname(targetFull);
|
|
333
|
+
while (basename(dir) !== REGISTRY_DIR && dir !== tempDir) {
|
|
334
|
+
try {
|
|
335
|
+
if (readdirSync(dir).length === 0) {
|
|
336
|
+
rmdirSync(dir);
|
|
337
|
+
dir = dirname(dir);
|
|
338
|
+
} else {
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
} catch { break; }
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
} catch (e) {
|
|
345
|
+
copyErrors.push({ file: file.registryTarget, error: e.message });
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
257
349
|
if (copyErrors.length > 0) {
|
|
258
350
|
for (const err of copyErrors) {
|
|
259
|
-
fmt.logWarn(`Failed to
|
|
351
|
+
fmt.logWarn(`Failed to process ${err.file}: ${err.error}`);
|
|
260
352
|
}
|
|
261
353
|
}
|
|
262
354
|
|
|
@@ -289,7 +381,7 @@ function pushFiles(files, { repo, dryRun, workspaceDir }) {
|
|
|
289
381
|
|
|
290
382
|
const prTitle = generatePrTitle(files);
|
|
291
383
|
const commitMsg = files.length === 1
|
|
292
|
-
? `registry: add ${files[0].type}/${files[0].slug} to ${files[0].namespace}`
|
|
384
|
+
? `registry: ${files[0].deleted ? 'remove' : 'add'} ${files[0].type}/${files[0].slug} ${files[0].deleted ? 'from' : 'to'} ${files[0].namespace}`
|
|
293
385
|
: `registry: sync ${files.length} files (${countParts.join(', ')})`;
|
|
294
386
|
execSync(`git commit -m "${commitMsg}"`, { cwd: tempDir, stdio: 'pipe' });
|
|
295
387
|
|
|
@@ -323,20 +415,28 @@ function pushFiles(files, { repo, dryRun, workspaceDir }) {
|
|
|
323
415
|
fmt.logInfo(`New namespace${newNamespaces.length > 1 ? 's' : ''} ${chalk.cyan(newNamespaces.join(', '))} — CODEOWNERS entr${newNamespaces.length > 1 ? 'ies' : 'y'} added`);
|
|
324
416
|
}
|
|
325
417
|
if (files.length > 1) {
|
|
326
|
-
for (const [type, items] of Object.entries(groupBy(
|
|
418
|
+
for (const [type, items] of Object.entries(groupBy(addedFiles, 'type'))) {
|
|
327
419
|
fmt.logSuccess(`${items.length} ${type} pushed`);
|
|
328
420
|
}
|
|
421
|
+
for (const [type, items] of Object.entries(groupBy(deletedFiles, 'type'))) {
|
|
422
|
+
fmt.logSuccess(`${items.length} ${type} removed`);
|
|
423
|
+
}
|
|
424
|
+
} else if (files.length === 1 && files[0].deleted) {
|
|
425
|
+
fmt.logSuccess(`${files[0].type}/${files[0].slug} removed`);
|
|
329
426
|
}
|
|
330
|
-
// Update manifest — mark pushed files as synced
|
|
427
|
+
// Update manifest — mark pushed files as synced, remove deleted entries
|
|
331
428
|
if (workspaceDir) {
|
|
332
429
|
const manifest = loadManifest(workspaceDir);
|
|
333
|
-
for (const file of
|
|
334
|
-
// Convert registryTarget back to manifest key (strip REGISTRY_DIR/ prefix)
|
|
430
|
+
for (const file of addedFiles) {
|
|
335
431
|
const manifestKey = file.registryTarget.replace(`${REGISTRY_DIR}/`, '');
|
|
336
432
|
if (manifest.files[manifestKey]) {
|
|
337
433
|
manifest.files[manifestKey].registry_sha256 = manifest.files[manifestKey].sha256;
|
|
338
434
|
}
|
|
339
435
|
}
|
|
436
|
+
for (const file of deletedFiles) {
|
|
437
|
+
const manifestKey = file.registryTarget.replace(`${REGISTRY_DIR}/`, '');
|
|
438
|
+
delete manifest.files[manifestKey];
|
|
439
|
+
}
|
|
340
440
|
saveManifest(workspaceDir, manifest);
|
|
341
441
|
}
|
|
342
442
|
|
|
@@ -374,7 +474,7 @@ export function pushCommand(args) {
|
|
|
374
474
|
if (!input) {
|
|
375
475
|
const files = collectModifiedFiles(workspaceDir);
|
|
376
476
|
if (files.length === 0) {
|
|
377
|
-
fmt.cancel('Nothing to push — no modified or
|
|
477
|
+
fmt.cancel('Nothing to push — no modified, new, or deleted files.\n\n Use `aw status` to see synced files.');
|
|
378
478
|
}
|
|
379
479
|
pushFiles(files, { repo, dryRun, workspaceDir });
|
|
380
480
|
return;
|
|
@@ -445,6 +545,7 @@ export function pushCommand(args) {
|
|
|
445
545
|
namespace: namespacePath,
|
|
446
546
|
slug,
|
|
447
547
|
isDir,
|
|
548
|
+
deleted: false,
|
|
448
549
|
}], { repo, dryRun, workspaceDir });
|
|
449
550
|
}
|
|
450
551
|
|
package/config.mjs
CHANGED
|
@@ -48,7 +48,7 @@ export function create(workspaceDir, { namespace, user }) {
|
|
|
48
48
|
|
|
49
49
|
export function addPattern(workspaceDir, pattern) {
|
|
50
50
|
const config = load(workspaceDir);
|
|
51
|
-
if (!config) throw new Error('No .sync-config.json found. Run: aw
|
|
51
|
+
if (!config) throw new Error('No .sync-config.json found. Run: aw init');
|
|
52
52
|
// If a parent path already covers this, skip
|
|
53
53
|
if (config.include.some(p => pattern === p || pattern.startsWith(p + '/'))) {
|
|
54
54
|
return config;
|
|
@@ -62,7 +62,7 @@ export function addPattern(workspaceDir, pattern) {
|
|
|
62
62
|
|
|
63
63
|
export function removePattern(workspaceDir, pattern) {
|
|
64
64
|
const config = load(workspaceDir);
|
|
65
|
-
if (!config) throw new Error('No .sync-config.json found. Run: aw
|
|
65
|
+
if (!config) throw new Error('No .sync-config.json found. Run: aw init');
|
|
66
66
|
// Remove exact match + all children
|
|
67
67
|
config.include = config.include.filter(p => p !== pattern && !p.startsWith(pattern + '/'));
|
|
68
68
|
save(workspaceDir, config);
|
package/ecc.mjs
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import {
|
|
3
|
+
existsSync, readFileSync, readdirSync,
|
|
4
|
+
mkdirSync, rmSync, writeFileSync,
|
|
5
|
+
} from "node:fs";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import * as fmt from "./fmt.mjs";
|
|
9
|
+
|
|
10
|
+
const AW_ECC_REPO_SSH = "git@github.com:shreyansh-ghl/aw-ecc.git";
|
|
11
|
+
const AW_ECC_REPO_HTTPS = "https://github.com/shreyansh-ghl/aw-ecc.git";
|
|
12
|
+
const AW_ECC_TAG = "v1.2.2";
|
|
13
|
+
|
|
14
|
+
const MARKETPLACE_NAME = "aw-marketplace";
|
|
15
|
+
const PLUGIN_KEY = `aw@${MARKETPLACE_NAME}`;
|
|
16
|
+
|
|
17
|
+
function eccDir() { return join(homedir(), ".aw-ecc"); }
|
|
18
|
+
|
|
19
|
+
const FILE_COPY_TARGETS = ["cursor", "codex"];
|
|
20
|
+
|
|
21
|
+
const TARGET_STATE = {
|
|
22
|
+
cursor: { state: ".cursor/ecc-install-state.json" },
|
|
23
|
+
codex: { state: ".codex/ecc-install-state.json" },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function run(cmd, opts = {}) {
|
|
27
|
+
return execSync(cmd, { stdio: "pipe", ...opts });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function cloneOrUpdate(tag, dest) {
|
|
31
|
+
if (existsSync(join(dest, ".git"))) {
|
|
32
|
+
try {
|
|
33
|
+
run(`git -C ${dest} fetch --quiet --depth 1 origin tag ${tag}`);
|
|
34
|
+
run(`git -C ${dest} checkout --quiet ${tag}`);
|
|
35
|
+
return;
|
|
36
|
+
} catch { /* fall through to fresh clone */ }
|
|
37
|
+
}
|
|
38
|
+
if (existsSync(dest)) rmSync(dest, { recursive: true, force: true });
|
|
39
|
+
try {
|
|
40
|
+
run(`git clone --quiet --depth 1 --branch ${tag} ${AW_ECC_REPO_SSH} ${dest}`);
|
|
41
|
+
} catch {
|
|
42
|
+
run(`git clone --quiet --depth 1 --branch ${tag} ${AW_ECC_REPO_HTTPS} ${dest}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function installClaudePlugin(repoDir) {
|
|
47
|
+
try {
|
|
48
|
+
run(`claude plugin marketplace add ${repoDir} --scope user`);
|
|
49
|
+
} catch {
|
|
50
|
+
try { run(`claude plugin marketplace update ${MARKETPLACE_NAME}`); } catch { /* ok */ }
|
|
51
|
+
}
|
|
52
|
+
run(`claude plugin install ${PLUGIN_KEY} --scope user`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function uninstallClaudePlugin() {
|
|
56
|
+
try { run(`claude plugin uninstall ${PLUGIN_KEY} --scope user`); } catch { /* not installed */ }
|
|
57
|
+
try { run(`claude plugin marketplace remove ${MARKETPLACE_NAME}`); } catch { /* not registered */ }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function installAwEcc(
|
|
61
|
+
cwd,
|
|
62
|
+
{ targets = ["cursor", "claude", "codex"], silent = false } = {},
|
|
63
|
+
) {
|
|
64
|
+
if (!silent) fmt.logStep("Installing aw-ecc engine...");
|
|
65
|
+
|
|
66
|
+
const repoDir = eccDir();
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
cloneOrUpdate(AW_ECC_TAG, repoDir);
|
|
70
|
+
|
|
71
|
+
// Claude Code: plugin install via marketplace CLI (proper agent dispatch)
|
|
72
|
+
if (targets.includes("claude")) {
|
|
73
|
+
try {
|
|
74
|
+
installClaudePlugin(repoDir);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
if (!silent) fmt.logWarn(`Claude plugin install skipped: ${err.message}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Cursor + Codex: file-copy via install-apply.js
|
|
81
|
+
const fileCopyTargets = targets.filter((t) => FILE_COPY_TARGETS.includes(t));
|
|
82
|
+
if (fileCopyTargets.length > 0) {
|
|
83
|
+
run("npm install --no-audit --no-fund --ignore-scripts --loglevel=error", {
|
|
84
|
+
cwd: repoDir,
|
|
85
|
+
});
|
|
86
|
+
for (const target of fileCopyTargets) {
|
|
87
|
+
try {
|
|
88
|
+
run(
|
|
89
|
+
`node ${join(repoDir, "scripts/install-apply.js")} --target ${target} --profile full`,
|
|
90
|
+
{ cwd },
|
|
91
|
+
);
|
|
92
|
+
} catch { /* target not supported — skip */ }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!silent) fmt.logSuccess("aw-ecc engine installed");
|
|
97
|
+
} catch (err) {
|
|
98
|
+
if (!silent) fmt.logWarn(`aw-ecc install failed: ${err.message}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function uninstallAwEcc({ silent = false } = {}) {
|
|
103
|
+
const HOME = homedir();
|
|
104
|
+
let removed = 0;
|
|
105
|
+
|
|
106
|
+
// Claude Code: uninstall plugin + remove marketplace via CLI
|
|
107
|
+
try {
|
|
108
|
+
uninstallClaudePlugin();
|
|
109
|
+
removed++;
|
|
110
|
+
} catch { /* best effort */ }
|
|
111
|
+
|
|
112
|
+
// Cursor + Codex: remove file-copied content via install-state
|
|
113
|
+
for (const cfg of Object.values(TARGET_STATE)) {
|
|
114
|
+
const statePath = join(HOME, cfg.state);
|
|
115
|
+
if (!existsSync(statePath)) continue;
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const data = JSON.parse(readFileSync(statePath, "utf8"));
|
|
119
|
+
for (const op of data.operations || []) {
|
|
120
|
+
if (op.destinationPath && existsSync(op.destinationPath)) {
|
|
121
|
+
rmSync(op.destinationPath, { recursive: true, force: true });
|
|
122
|
+
removed++;
|
|
123
|
+
pruneEmptyParents(op.destinationPath, join(HOME, cfg.state.split("/")[0]));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
rmSync(statePath, { force: true });
|
|
127
|
+
pruneEmptyParents(statePath, join(HOME, cfg.state.split("/")[0]));
|
|
128
|
+
} catch { /* corrupted state — skip */ }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Clean leftover claude install-state from older file-copy versions
|
|
132
|
+
const claudeState = join(HOME, ".claude", "ecc", "install-state.json");
|
|
133
|
+
if (existsSync(claudeState)) {
|
|
134
|
+
try {
|
|
135
|
+
const data = JSON.parse(readFileSync(claudeState, "utf8"));
|
|
136
|
+
for (const op of data.operations || []) {
|
|
137
|
+
if (op.destinationPath && existsSync(op.destinationPath)) {
|
|
138
|
+
rmSync(op.destinationPath, { recursive: true, force: true });
|
|
139
|
+
removed++;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
rmSync(claudeState, { force: true });
|
|
143
|
+
pruneEmptyParents(claudeState, join(HOME, ".claude"));
|
|
144
|
+
} catch { /* best effort */ }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Clean leftover manual plugin cache from older versions
|
|
148
|
+
const oldCache = join(HOME, ".claude", "plugins", "cache", "aw");
|
|
149
|
+
if (existsSync(oldCache)) {
|
|
150
|
+
rmSync(oldCache, { recursive: true, force: true });
|
|
151
|
+
removed++;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Remove permanent aw-ecc repo clone
|
|
155
|
+
const repoDir = eccDir();
|
|
156
|
+
if (existsSync(repoDir)) {
|
|
157
|
+
rmSync(repoDir, { recursive: true, force: true });
|
|
158
|
+
removed++;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!silent && removed > 0)
|
|
162
|
+
fmt.logStep(`Removed ${removed} aw-ecc file${removed > 1 ? "s" : ""}`);
|
|
163
|
+
return removed;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function pruneEmptyParents(filePath, stopAt) {
|
|
167
|
+
let dir = dirname(filePath);
|
|
168
|
+
while (dir !== stopAt && dir.startsWith(stopAt)) {
|
|
169
|
+
try {
|
|
170
|
+
if (readdirSync(dir).length === 0) {
|
|
171
|
+
rmSync(dir);
|
|
172
|
+
dir = dirname(dir);
|
|
173
|
+
} else {
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
} catch {
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
package/fmt.mjs
CHANGED
|
@@ -36,6 +36,8 @@ export function banner(text, opts = {}) {
|
|
|
36
36
|
export const intro = (msg) => p.intro(chalk.bgCyan.black(` ${msg} `));
|
|
37
37
|
export const outro = (msg) => p.outro(chalk.green(msg));
|
|
38
38
|
export const spinner = () => p.spinner();
|
|
39
|
+
export const select = p.select;
|
|
40
|
+
export const isCancel = p.isCancel;
|
|
39
41
|
|
|
40
42
|
export function cancel(msg) {
|
|
41
43
|
p.cancel(msg);
|
package/integrate.mjs
CHANGED
|
@@ -56,11 +56,13 @@ function findFiles(dir, typeName) {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
|
-
* Copy
|
|
59
|
+
* Copy AGENTS.md to project root.
|
|
60
|
+
* CLAUDE.md is intentionally NOT generated — its routing rule hijacks plugin
|
|
61
|
+
* commands like /aw:plan, preventing proper agent dispatch.
|
|
60
62
|
*/
|
|
61
63
|
export function copyInstructions(cwd, tempDir, namespace) {
|
|
62
64
|
const createdFiles = [];
|
|
63
|
-
for (const file of ['
|
|
65
|
+
for (const file of ['AGENTS.md']) {
|
|
64
66
|
const dest = join(cwd, file);
|
|
65
67
|
if (existsSync(dest)) continue;
|
|
66
68
|
|
|
@@ -78,9 +80,7 @@ export function copyInstructions(cwd, tempDir, namespace) {
|
|
|
78
80
|
}
|
|
79
81
|
}
|
|
80
82
|
|
|
81
|
-
const content =
|
|
82
|
-
? generateClaudeMd(cwd, namespace)
|
|
83
|
-
: generateAgentsMd(cwd, namespace);
|
|
83
|
+
const content = generateAgentsMd(cwd, namespace);
|
|
84
84
|
if (content) {
|
|
85
85
|
writeFileSync(dest, content);
|
|
86
86
|
fmt.logSuccess(`Created ${file}`);
|
|
@@ -100,7 +100,9 @@ Team: ${team} | Local-first orchestration via \`.aw_docs/\` | MCPs: \`memory/*\`
|
|
|
100
100
|
|
|
101
101
|
> **Every non-trivial task MUST call \`Skill(skill: "platform-ai-task-router")\` BEFORE any response.**
|
|
102
102
|
>
|
|
103
|
-
> **
|
|
103
|
+
> **Exempt from routing (execute directly):**
|
|
104
|
+
> - **Plugin commands**: any \`/aw:*\` slash command — these have their own agent dispatch via the plugin system. Execute the plugin command as-is, do NOT re-route through the task router.
|
|
105
|
+
> - **Trivial tasks**: typo fixes, single-line edits, git ops, file exploration, factual code questions.
|
|
104
106
|
>
|
|
105
107
|
> Everything else — including tasks phrased as questions, suggestions, or discussions — routes first.
|
|
106
108
|
> **No conversational responses first. No planning first. Route first.**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ghl-ai/aw",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.36-beta.1",
|
|
4
4
|
"description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"registry.mjs",
|
|
25
25
|
"apply.mjs",
|
|
26
26
|
"update.mjs",
|
|
27
|
-
"hooks.mjs"
|
|
27
|
+
"hooks.mjs",
|
|
28
|
+
"ecc.mjs"
|
|
28
29
|
],
|
|
29
30
|
"engines": {
|
|
30
31
|
"node": ">=18.0.0"
|