@ghl-ai/aw 0.1.35 → 0.1.36-beta.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.
- package/cli.mjs +2 -1
- package/commands/drop.mjs +45 -47
- package/commands/init.mjs +122 -125
- package/commands/nuke.mjs +30 -10
- package/commands/pull.mjs +57 -370
- package/commands/push.mjs +297 -287
- package/commands/status.mjs +50 -80
- package/config.mjs +2 -2
- package/constants.mjs +6 -0
- package/ecc.mjs +180 -0
- package/fmt.mjs +2 -0
- package/git.mjs +233 -1
- package/integrate.mjs +8 -6
- package/package.json +3 -2
- package/apply.mjs +0 -79
- package/manifest.mjs +0 -64
- package/plan.mjs +0 -147
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/drop.mjs
CHANGED
|
@@ -2,87 +2,85 @@
|
|
|
2
2
|
|
|
3
3
|
import { join, resolve } from 'node:path';
|
|
4
4
|
import { rmSync, existsSync } from 'node:fs';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
5
6
|
import * as config from '../config.mjs';
|
|
6
7
|
import * as fmt from '../fmt.mjs';
|
|
7
8
|
import { chalk } from '../fmt.mjs';
|
|
8
|
-
import { matchesAny } from '../glob.mjs';
|
|
9
9
|
import { resolveInput } from '../paths.mjs';
|
|
10
|
-
import {
|
|
10
|
+
import { removeFromSparseCheckout, isValidClone } from '../git.mjs';
|
|
11
|
+
import { REGISTRY_DIR, REGISTRY_REPO } from '../constants.mjs';
|
|
12
|
+
import { linkWorkspace } from '../link.mjs';
|
|
11
13
|
|
|
12
14
|
export function dropCommand(args) {
|
|
13
15
|
const input = args._positional?.[0];
|
|
14
16
|
const cwd = process.cwd();
|
|
17
|
+
|
|
18
|
+
const HOME = homedir();
|
|
19
|
+
const AW_HOME = join(HOME, '.aw');
|
|
20
|
+
const GLOBAL_AW_DIR = join(HOME, '.aw_registry');
|
|
15
21
|
const workspaceDir = join(cwd, '.aw_registry');
|
|
16
22
|
|
|
17
23
|
fmt.intro('aw drop');
|
|
18
24
|
|
|
19
25
|
if (!input) {
|
|
20
|
-
fmt.cancel('Missing target. Usage:\n aw drop example-team (stop syncing namespace)\n aw drop example-team/skills/example-skill (stop syncing skill)
|
|
26
|
+
fmt.cancel('Missing target. Usage:\n aw drop example-team (stop syncing namespace)\n aw drop example-team/skills/example-skill (stop syncing skill)');
|
|
27
|
+
return;
|
|
21
28
|
}
|
|
22
29
|
|
|
23
|
-
const
|
|
24
|
-
if (!
|
|
30
|
+
const repoUrl = `https://github.com/${REGISTRY_REPO}.git`;
|
|
31
|
+
if (!isValidClone(AW_HOME, repoUrl)) {
|
|
32
|
+
fmt.cancel('Registry not initialized. Run: aw init');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
25
35
|
|
|
26
|
-
|
|
36
|
+
const cfg = config.load(GLOBAL_AW_DIR);
|
|
37
|
+
if (!cfg) {
|
|
38
|
+
fmt.cancel('No .sync-config.json found. Run: aw init');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Resolve to registry path
|
|
27
43
|
const resolved = resolveInput(input, workspaceDir);
|
|
28
44
|
const regPath = resolved.registryPath;
|
|
29
45
|
|
|
30
46
|
if (!regPath) {
|
|
31
47
|
fmt.cancel(`Could not resolve "${input}" to a registry path`);
|
|
48
|
+
return;
|
|
32
49
|
}
|
|
33
50
|
|
|
34
|
-
// Check if this path (or a parent) is in config
|
|
51
|
+
// Check if this path (or a parent) is in config
|
|
35
52
|
const isConfigPath = cfg.include.some(p => p === regPath || p.startsWith(regPath + '/'));
|
|
36
53
|
|
|
37
54
|
if (isConfigPath) {
|
|
38
|
-
|
|
55
|
+
// Remove from sparse checkout
|
|
56
|
+
try {
|
|
57
|
+
removeFromSparseCheckout(AW_HOME, [`${REGISTRY_DIR}/${regPath}`]);
|
|
58
|
+
} catch (e) {
|
|
59
|
+
fmt.logWarn(`Could not update sparse checkout: ${e.message}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
config.removePattern(GLOBAL_AW_DIR, regPath);
|
|
39
63
|
fmt.logSuccess(`Removed ${chalk.cyan(regPath)} from sync config`);
|
|
40
64
|
}
|
|
41
65
|
|
|
42
|
-
//
|
|
43
|
-
const
|
|
66
|
+
// Count removed files (they disappear from working tree via sparse checkout)
|
|
67
|
+
const registryAbsPath = join(AW_HOME, REGISTRY_DIR, regPath);
|
|
68
|
+
let removed = 0;
|
|
69
|
+
if (!existsSync(registryAbsPath)) {
|
|
70
|
+
removed = 1; // sparse checkout removed it
|
|
71
|
+
}
|
|
44
72
|
|
|
45
|
-
if (removed
|
|
46
|
-
fmt.logInfo(`${chalk.bold(removed)} file${removed > 1 ? 's' : ''} removed from workspace`);
|
|
47
|
-
} else if (!isConfigPath) {
|
|
73
|
+
if (!isConfigPath && removed === 0) {
|
|
48
74
|
fmt.cancel(`Nothing found for ${chalk.cyan(regPath)}.\n\n Use ${chalk.dim('aw status')} to see synced paths.`);
|
|
75
|
+
return;
|
|
49
76
|
}
|
|
50
77
|
|
|
51
|
-
if (!isConfigPath
|
|
52
|
-
fmt.logWarn(`Path
|
|
78
|
+
if (!isConfigPath) {
|
|
79
|
+
fmt.logWarn(`Path was not in sync config — no sparse checkout change made`);
|
|
53
80
|
}
|
|
54
81
|
|
|
55
|
-
|
|
56
|
-
|
|
82
|
+
// Re-link to remove dead symlinks
|
|
83
|
+
linkWorkspace(HOME);
|
|
57
84
|
|
|
58
|
-
|
|
59
|
-
* Find and delete local files whose registry path matches the given path.
|
|
60
|
-
*/
|
|
61
|
-
function deleteMatchingFiles(workspaceDir, path) {
|
|
62
|
-
const manifest = loadManifest(workspaceDir);
|
|
63
|
-
let removed = 0;
|
|
64
|
-
|
|
65
|
-
for (const [manifestKey] of Object.entries(manifest.files)) {
|
|
66
|
-
const registryPath = manifestKeyToRegistryPath(manifestKey);
|
|
67
|
-
|
|
68
|
-
if (matchesAny(registryPath, [path])) {
|
|
69
|
-
const filePath = join(workspaceDir, manifestKey);
|
|
70
|
-
if (existsSync(filePath)) {
|
|
71
|
-
rmSync(filePath, { recursive: true, force: true });
|
|
72
|
-
removed++;
|
|
73
|
-
}
|
|
74
|
-
delete manifest.files[manifestKey];
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
saveManifest(workspaceDir, manifest);
|
|
79
|
-
return removed;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Convert manifest key to registry path.
|
|
84
|
-
* Manifest key now mirrors registry: "platform/agents/architecture-reviewer.md" → "platform/agents/architecture-reviewer"
|
|
85
|
-
*/
|
|
86
|
-
function manifestKeyToRegistryPath(manifestKey) {
|
|
87
|
-
return manifestKey.replace(/\.md$/, '');
|
|
85
|
+
fmt.outro('Done');
|
|
88
86
|
}
|
package/commands/init.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
// commands/init.mjs — Clean init: clone registry, link IDEs, global git hooks.
|
|
1
|
+
// commands/init.mjs — Clean init: persistent git clone of registry, link IDEs, global git hooks.
|
|
2
2
|
//
|
|
3
3
|
// No shell profile modifications. No daemons. No background processes.
|
|
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 {
|
|
7
|
+
import { existsSync, writeFileSync, symlinkSync, lstatSync, 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';
|
|
@@ -13,24 +13,33 @@ import { readFileSync } from 'node:fs';
|
|
|
13
13
|
import * as config from '../config.mjs';
|
|
14
14
|
import * as fmt from '../fmt.mjs';
|
|
15
15
|
import { chalk } from '../fmt.mjs';
|
|
16
|
-
import { pullCommand, pullAsync } from './pull.mjs';
|
|
17
16
|
import { linkWorkspace } from '../link.mjs';
|
|
18
17
|
import { generateCommands, copyInstructions, initAwDocs } from '../integrate.mjs';
|
|
19
18
|
import { setupMcp } from '../mcp.mjs';
|
|
20
19
|
import { autoUpdate, promptUpdate } from '../update.mjs';
|
|
21
20
|
import { installGlobalHooks } from '../hooks.mjs';
|
|
21
|
+
import { installAwEcc } from '../ecc.mjs';
|
|
22
|
+
import {
|
|
23
|
+
initPersistentClone,
|
|
24
|
+
isValidClone,
|
|
25
|
+
fetchAndMerge,
|
|
26
|
+
addToSparseCheckout,
|
|
27
|
+
includeToSparsePaths,
|
|
28
|
+
sparseCheckoutAsync,
|
|
29
|
+
cleanup,
|
|
30
|
+
} from '../git.mjs';
|
|
31
|
+
import { REGISTRY_DIR, REGISTRY_REPO } from '../constants.mjs';
|
|
22
32
|
|
|
23
33
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
24
34
|
const VERSION = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')).version;
|
|
25
35
|
|
|
26
36
|
const HOME = homedir();
|
|
27
37
|
const GLOBAL_AW_DIR = join(HOME, '.aw_registry');
|
|
28
|
-
const
|
|
38
|
+
const AW_HOME = join(HOME, '.aw');
|
|
29
39
|
|
|
30
40
|
// ── IDE tasks for auto-pull ─────────────────────────────────────────────
|
|
31
41
|
|
|
32
42
|
function installIdeTasks() {
|
|
33
|
-
// VS Code / Cursor task — runs aw pull on folder open
|
|
34
43
|
const vscodeTask = {
|
|
35
44
|
version: '2.0.0',
|
|
36
45
|
tasks: [
|
|
@@ -45,18 +54,15 @@ function installIdeTasks() {
|
|
|
45
54
|
],
|
|
46
55
|
};
|
|
47
56
|
|
|
48
|
-
// Install globally for VS Code and Cursor
|
|
49
57
|
for (const ide of ['Code', 'Cursor']) {
|
|
50
58
|
const userDir = join(HOME, 'Library', 'Application Support', ide, 'User');
|
|
51
59
|
if (!existsSync(userDir)) continue;
|
|
52
60
|
|
|
53
61
|
const tasksPath = join(userDir, 'tasks.json');
|
|
54
62
|
if (existsSync(tasksPath)) {
|
|
55
|
-
// Don't override existing tasks — check if aw task already there
|
|
56
63
|
try {
|
|
57
64
|
const existing = JSON.parse(readFileSync(tasksPath, 'utf8'));
|
|
58
65
|
if (existing.tasks?.some(t => t.label === 'aw: sync registry' || t.label === 'aw: pull registry')) continue;
|
|
59
|
-
// Add our task to existing
|
|
60
66
|
existing.tasks = existing.tasks || [];
|
|
61
67
|
existing.tasks.push(vscodeTask.tasks[0]);
|
|
62
68
|
writeFileSync(tasksPath, JSON.stringify(existing, null, 2) + '\n');
|
|
@@ -69,33 +75,10 @@ function installIdeTasks() {
|
|
|
69
75
|
}
|
|
70
76
|
}
|
|
71
77
|
|
|
72
|
-
function saveManifest(data) {
|
|
73
|
-
writeFileSync(MANIFEST_PATH, JSON.stringify(data, null, 2) + '\n');
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
function printPullSummary(pattern, actions) {
|
|
78
|
-
for (const type of ['agents', 'skills', 'commands', 'evals']) {
|
|
79
|
-
const typeActions = actions.filter(a => a.type === type);
|
|
80
|
-
if (typeActions.length === 0) continue;
|
|
81
|
-
|
|
82
|
-
const counts = { ADD: 0, UPDATE: 0, CONFLICT: 0 };
|
|
83
|
-
for (const a of typeActions) counts[a.action] = (counts[a.action] || 0) + 1;
|
|
84
|
-
|
|
85
|
-
const parts = [];
|
|
86
|
-
if (counts.ADD > 0) parts.push(chalk.green(`${counts.ADD} new`));
|
|
87
|
-
if (counts.UPDATE > 0) parts.push(chalk.cyan(`${counts.UPDATE} updated`));
|
|
88
|
-
if (counts.CONFLICT > 0) parts.push(chalk.red(`${counts.CONFLICT} conflict`));
|
|
89
|
-
const detail = parts.length > 0 ? ` (${parts.join(', ')})` : '';
|
|
90
|
-
|
|
91
|
-
fmt.logSuccess(`${chalk.cyan(pattern)}: ${typeActions.length} ${type}${detail}`);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
78
|
const ALLOWED_NAMESPACES = ['platform', 'revex', 'mobile', 'commerce', 'leadgen', 'crm', 'marketplace', 'ai'];
|
|
96
79
|
|
|
97
80
|
export async function initCommand(args) {
|
|
98
|
-
|
|
81
|
+
let namespace = args['--namespace'] || null;
|
|
99
82
|
let user = args['--user'] || '';
|
|
100
83
|
const silent = args['--silent'] === true;
|
|
101
84
|
|
|
@@ -103,24 +86,11 @@ export async function initCommand(args) {
|
|
|
103
86
|
|
|
104
87
|
// ── Validate ──────────────────────────────────────────────────────────
|
|
105
88
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
// Parse team/sub-team
|
|
119
|
-
const nsParts = namespace ? namespace.split('/') : [];
|
|
120
|
-
const team = nsParts[0] || null;
|
|
121
|
-
const subTeam = nsParts[1] || null;
|
|
122
|
-
const teamNS = subTeam ? `${team}-${subTeam}` : team; // for $TEAM_NS replacement
|
|
123
|
-
const folderName = subTeam ? `${team}/${subTeam}` : team; // for .aw_registry/ path
|
|
89
|
+
let nsParts = namespace ? namespace.split('/') : [];
|
|
90
|
+
let team = nsParts[0] || null;
|
|
91
|
+
let subTeam = nsParts[1] || null;
|
|
92
|
+
let teamNS = subTeam ? `${team}-${subTeam}` : team;
|
|
93
|
+
let folderName = subTeam ? `${team}/${subTeam}` : team;
|
|
124
94
|
|
|
125
95
|
if (team && !ALLOWED_NAMESPACES.includes(team)) {
|
|
126
96
|
const list = ALLOWED_NAMESPACES.map(n => chalk.cyan(n)).join(', ');
|
|
@@ -151,55 +121,81 @@ export async function initCommand(args) {
|
|
|
151
121
|
fmt.cancel(`Invalid sub-team '${subTeam}' — must match: ${SLUG_RE}`);
|
|
152
122
|
}
|
|
153
123
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const
|
|
124
|
+
// ── Detect installation state ─────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
const repoUrl = `https://github.com/${REGISTRY_REPO}.git`;
|
|
127
|
+
const isGitNative = isValidClone(AW_HOME, repoUrl);
|
|
128
|
+
const isLegacy = !isGitNative && existsSync(GLOBAL_AW_DIR) && !lstatSync(GLOBAL_AW_DIR).isSymbolicLink();
|
|
129
|
+
|
|
130
|
+
// ── Probe remote registry to check if namespace exists (fresh install only) ──
|
|
131
|
+
|
|
132
|
+
let namespaceExistsInRemote = false;
|
|
133
|
+
if (folderName && !silent && !isGitNative && !isLegacy) {
|
|
134
|
+
try {
|
|
135
|
+
const probePaths = includeToSparsePaths([folderName]);
|
|
136
|
+
const probeDir = await sparseCheckoutAsync(REGISTRY_REPO, probePaths);
|
|
137
|
+
try {
|
|
138
|
+
const fullNsPath = join(probeDir, REGISTRY_DIR, ...folderName.split('/'));
|
|
139
|
+
namespaceExistsInRemote = existsSync(fullNsPath) &&
|
|
140
|
+
readdirSync(fullNsPath, { withFileTypes: true })
|
|
141
|
+
.some(d => d.isDirectory() && !d.name.startsWith('.'));
|
|
142
|
+
} finally {
|
|
143
|
+
cleanup(probeDir);
|
|
144
|
+
}
|
|
145
|
+
} catch {
|
|
146
|
+
namespaceExistsInRemote = true;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (folderName && !silent && !isGitNative && !isLegacy && !namespaceExistsInRemote && process.stdin.isTTY) {
|
|
151
|
+
const choice = await fmt.select({
|
|
152
|
+
message: `The namespace '${folderName}' does not exist in the registry yet.\nplatform/ includes shared agents, skills & commands that cover most use cases.\nHow would you like to proceed?`,
|
|
153
|
+
options: [
|
|
154
|
+
{ value: 'platform-only', label: 'Continue with platform/ only (recommended for most users)' },
|
|
155
|
+
{ value: 'create-namespace', label: `Use '${folderName}' namespace (will be created)` },
|
|
156
|
+
],
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
if (fmt.isCancel(choice)) {
|
|
160
|
+
fmt.cancel('Operation cancelled.');
|
|
161
|
+
process.exit(0);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (choice === 'platform-only') {
|
|
165
|
+
namespace = null; team = null; subTeam = null; teamNS = null; folderName = null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
157
169
|
const cwd = process.cwd();
|
|
158
170
|
|
|
159
|
-
// ──
|
|
171
|
+
// ── Re-init path: already set up with native git clone ────────────────
|
|
160
172
|
|
|
161
|
-
if (
|
|
173
|
+
if (isGitNative) {
|
|
162
174
|
const cfg = config.load(GLOBAL_AW_DIR);
|
|
163
175
|
|
|
164
|
-
// Add new sub-team if not already tracked
|
|
165
176
|
const isNewSubTeam = folderName && cfg && !cfg.include.includes(folderName);
|
|
166
177
|
if (isNewSubTeam) {
|
|
167
178
|
if (!silent) fmt.logStep(`Adding sub-team ${chalk.cyan(folderName)}...`);
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
_positional: ['[template]'],
|
|
171
|
-
_renameNamespace: folderName,
|
|
172
|
-
_teamNS: teamNS,
|
|
173
|
-
_workspaceDir: GLOBAL_AW_DIR,
|
|
174
|
-
_skipIntegrate: true,
|
|
175
|
-
});
|
|
179
|
+
const newSparsePaths = [`.aw_registry/${folderName}`, `content`];
|
|
180
|
+
addToSparseCheckout(AW_HOME, newSparsePaths);
|
|
176
181
|
config.addPattern(GLOBAL_AW_DIR, folderName);
|
|
177
182
|
} else {
|
|
178
183
|
if (!silent) fmt.logStep('Already initialized — syncing...');
|
|
179
184
|
}
|
|
180
185
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const derivedTeamNS = isTeamNs ? p.replace(/\//g, '-') : undefined;
|
|
189
|
-
return pullAsync({
|
|
190
|
-
...args,
|
|
191
|
-
_positional: [isTeamNs ? '[template]' : p],
|
|
192
|
-
_workspaceDir: GLOBAL_AW_DIR,
|
|
193
|
-
_skipIntegrate: true,
|
|
194
|
-
_renameNamespace: isTeamNs ? p : undefined,
|
|
195
|
-
_teamNS: derivedTeamNS,
|
|
196
|
-
});
|
|
197
|
-
});
|
|
198
|
-
await Promise.all(pullJobs);
|
|
186
|
+
const s = fmt.spinner ? fmt.spinner() : { start: () => {}, stop: () => {} };
|
|
187
|
+
if (!silent) s.start('Fetching latest...');
|
|
188
|
+
try {
|
|
189
|
+
fetchAndMerge(AW_HOME);
|
|
190
|
+
if (!silent) s.stop('Registry updated');
|
|
191
|
+
} catch (e) {
|
|
192
|
+
if (!silent) s.stop(chalk.yellow('Fetch failed (continuing with local)'));
|
|
199
193
|
}
|
|
200
194
|
|
|
201
|
-
|
|
195
|
+
const freshCfg = config.load(GLOBAL_AW_DIR);
|
|
196
|
+
|
|
202
197
|
linkWorkspace(HOME);
|
|
198
|
+
await installAwEcc(cwd, { silent });
|
|
203
199
|
generateCommands(HOME);
|
|
204
200
|
copyInstructions(HOME, null, freshCfg?.namespace || team) || [];
|
|
205
201
|
initAwDocs(HOME);
|
|
@@ -207,7 +203,6 @@ export async function initCommand(args) {
|
|
|
207
203
|
if (cwd !== HOME) await setupMcp(cwd, freshCfg?.namespace || team, { silent });
|
|
208
204
|
installGlobalHooks();
|
|
209
205
|
|
|
210
|
-
// Link current project if needed
|
|
211
206
|
if (cwd !== HOME && !existsSync(join(cwd, '.aw_registry'))) {
|
|
212
207
|
try {
|
|
213
208
|
symlinkSync(GLOBAL_AW_DIR, join(cwd, '.aw_registry'));
|
|
@@ -229,57 +224,72 @@ export async function initCommand(args) {
|
|
|
229
224
|
return;
|
|
230
225
|
}
|
|
231
226
|
|
|
227
|
+
// ── Legacy migration: old ~/.aw_registry/ dir → notify user ──────────
|
|
228
|
+
|
|
229
|
+
if (isLegacy) {
|
|
230
|
+
if (!silent) {
|
|
231
|
+
fmt.logWarn([
|
|
232
|
+
'Legacy installation detected (~/.aw_registry/ is a plain directory).',
|
|
233
|
+
'',
|
|
234
|
+
` Run ${chalk.bold('aw nuke')} first to remove the old install, then ${chalk.bold('aw init')} again.`,
|
|
235
|
+
` This will migrate to the new native git clone at ~/.aw/`,
|
|
236
|
+
].join('\n'));
|
|
237
|
+
}
|
|
238
|
+
// Fall through to full init — create AW_HOME fresh
|
|
239
|
+
}
|
|
240
|
+
|
|
232
241
|
// ── Full init: first time setup ───────────────────────────────────────
|
|
233
242
|
|
|
234
|
-
// Auto-detect user
|
|
235
243
|
if (!user) {
|
|
236
244
|
try {
|
|
237
245
|
user = execSync('git config user.name', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
238
246
|
} catch { /* git not configured */ }
|
|
239
247
|
}
|
|
240
248
|
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
249
|
+
// Determine sparse paths
|
|
250
|
+
const sparsePaths = [`.aw_registry/platform`, `content`, `.aw_registry/AW-PROTOCOL.md`];
|
|
251
|
+
if (folderName) {
|
|
252
|
+
sparsePaths.push(`.aw_registry/${folderName}`);
|
|
253
|
+
}
|
|
245
254
|
|
|
246
255
|
fmt.note([
|
|
247
|
-
`${chalk.dim('source:')} ~/.aw_registry
|
|
256
|
+
`${chalk.dim('source:')} ~/.aw/ → ~/.aw_registry/ (symlink)`,
|
|
248
257
|
folderName ? `${chalk.dim('namespace:')} ${folderName}` : `${chalk.dim('namespace:')} ${chalk.dim('none')}`,
|
|
249
|
-
user ? `${chalk.dim('user:')} ${
|
|
258
|
+
user ? `${chalk.dim('user:')} ${user}` : null,
|
|
250
259
|
`${chalk.dim('version:')} v${VERSION}`,
|
|
251
|
-
].filter(Boolean).join('\n'), 'Config
|
|
260
|
+
].filter(Boolean).join('\n'), 'Config');
|
|
252
261
|
|
|
253
|
-
// Step 2: Pull registry content (parallel)
|
|
254
262
|
const s = fmt.spinner();
|
|
255
|
-
|
|
256
|
-
s.start(`Pulling ${pullTargets}...`);
|
|
257
|
-
|
|
258
|
-
const pullJobs = [
|
|
259
|
-
pullAsync({ ...args, _positional: ['platform'], _workspaceDir: GLOBAL_AW_DIR, _skipIntegrate: true }),
|
|
260
|
-
];
|
|
261
|
-
if (folderName) {
|
|
262
|
-
pullJobs.push(
|
|
263
|
-
pullAsync({ ...args, _positional: ['[template]'], _renameNamespace: folderName, _teamNS: teamNS, _workspaceDir: GLOBAL_AW_DIR, _skipIntegrate: true }),
|
|
264
|
-
);
|
|
265
|
-
}
|
|
263
|
+
s.start(`Cloning registry...`);
|
|
266
264
|
|
|
267
|
-
let pullResults;
|
|
268
265
|
try {
|
|
269
|
-
|
|
270
|
-
s.stop(
|
|
266
|
+
initPersistentClone(repoUrl, AW_HOME, sparsePaths);
|
|
267
|
+
s.stop('Registry cloned');
|
|
271
268
|
} catch (e) {
|
|
272
|
-
s.stop(chalk.red('
|
|
269
|
+
s.stop(chalk.red('Clone failed'));
|
|
273
270
|
fmt.cancel(e.message);
|
|
274
271
|
}
|
|
275
272
|
|
|
276
|
-
|
|
277
|
-
|
|
273
|
+
// Create backward-compat symlink: ~/.aw_registry/ → ~/.aw/.aw_registry/
|
|
274
|
+
if (!existsSync(GLOBAL_AW_DIR)) {
|
|
275
|
+
try {
|
|
276
|
+
symlinkSync(join(AW_HOME, REGISTRY_DIR), GLOBAL_AW_DIR);
|
|
277
|
+
fmt.logStep('Created ~/.aw_registry/ symlink');
|
|
278
|
+
} catch (e) {
|
|
279
|
+
fmt.logWarn(`Could not create symlink ~/.aw_registry/: ${e.message}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Create sync config
|
|
284
|
+
const cfg = config.create(GLOBAL_AW_DIR, { namespace: team, user });
|
|
285
|
+
if (folderName) {
|
|
286
|
+
config.addPattern(GLOBAL_AW_DIR, folderName);
|
|
278
287
|
}
|
|
279
288
|
|
|
280
289
|
// Step 3: Link IDE dirs + setup tasks
|
|
281
290
|
fmt.logStep('Linking IDE symlinks...');
|
|
282
291
|
linkWorkspace(HOME);
|
|
292
|
+
await installAwEcc(cwd, { silent });
|
|
283
293
|
generateCommands(HOME);
|
|
284
294
|
const instructionFiles = copyInstructions(HOME, null, team) || [];
|
|
285
295
|
initAwDocs(HOME);
|
|
@@ -296,27 +306,14 @@ export async function initCommand(args) {
|
|
|
296
306
|
} catch { /* best effort */ }
|
|
297
307
|
}
|
|
298
308
|
|
|
299
|
-
// Step 5: Write manifest for nuke cleanup
|
|
300
|
-
const manifest = {
|
|
301
|
-
version: 1,
|
|
302
|
-
installedAt: new Date().toISOString(),
|
|
303
|
-
globalDir: GLOBAL_AW_DIR,
|
|
304
|
-
createdFiles: [
|
|
305
|
-
...instructionFiles.map(p => p.startsWith(HOME) ? p.slice(HOME.length + 1) : p),
|
|
306
|
-
...mcpFiles.map(p => p.startsWith(HOME) ? p.slice(HOME.length + 1) : p),
|
|
307
|
-
],
|
|
308
|
-
globalHooksDir: hooksInstalled ? join(HOME, '.aw', 'hooks') : null,
|
|
309
|
-
};
|
|
310
|
-
saveManifest(manifest);
|
|
311
|
-
|
|
312
309
|
// Offer to update if a newer version is available
|
|
313
310
|
await promptUpdate(await args._updateCheck);
|
|
314
311
|
|
|
315
|
-
// Done
|
|
316
312
|
fmt.outro([
|
|
317
313
|
'Install complete',
|
|
318
314
|
'',
|
|
319
|
-
` ${chalk.green('✓')} Source of truth: ~/.
|
|
315
|
+
` ${chalk.green('✓')} Source of truth: ~/.aw/ (git clone)`,
|
|
316
|
+
` ${chalk.green('✓')} Symlink: ~/.aw_registry/ → ~/.aw/.aw_registry/`,
|
|
320
317
|
` ${chalk.green('✓')} IDE integration: ~/.claude/, ~/.cursor/, ~/.codex/`,
|
|
321
318
|
hooksInstalled ? ` ${chalk.green('✓')} Git hooks: auto-sync on pull/clone (core.hooksPath)` : null,
|
|
322
319
|
` ${chalk.green('✓')} IDE task: auto-sync on workspace open`,
|
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,19 +290,35 @@ 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/ — now a symlink to ~/.aw/.aw_registry/
|
|
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
|
|
|
301
|
+
// 11. Remove ~/.aw/ (persistent git clone)
|
|
302
|
+
const AW_HOME = join(HOME, '.aw');
|
|
303
|
+
if (existsSync(AW_HOME)) {
|
|
304
|
+
try {
|
|
305
|
+
rmSync(AW_HOME, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
306
|
+
} catch {
|
|
307
|
+
try { execSync(`rm -rf "${AW_HOME}"`, { stdio: 'pipe' }); } catch { /* best effort */ }
|
|
308
|
+
}
|
|
309
|
+
fmt.logStep('Removed ~/.aw/');
|
|
310
|
+
}
|
|
311
|
+
|
|
293
312
|
fmt.outro([
|
|
294
313
|
'Fully removed',
|
|
295
314
|
'',
|
|
296
315
|
` ${chalk.green('✓')} Generated files cleaned`,
|
|
297
316
|
` ${chalk.green('✓')} IDE symlinks cleaned`,
|
|
317
|
+
` ${chalk.green('✓')} aw-ecc engine removed`,
|
|
298
318
|
` ${chalk.green('✓')} Project symlinks cleaned`,
|
|
299
319
|
` ${chalk.green('✓')} Git hooks removed`,
|
|
300
320
|
` ${chalk.green('✓')} IDE auto-sync tasks removed`,
|
|
301
|
-
` ${chalk.green('✓')} Source of truth deleted`,
|
|
321
|
+
` ${chalk.green('✓')} Source of truth deleted (symlink + git clone)`,
|
|
302
322
|
'',
|
|
303
323
|
` ${chalk.dim('No existing files were touched.')}`,
|
|
304
324
|
` ${chalk.dim('To reinstall:')} ${chalk.bold('aw init')}`,
|