@ghl-ai/aw 0.1.35 → 0.1.36-beta.10
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 +46 -48
- package/commands/init.mjs +174 -141
- package/commands/link-project.mjs +50 -10
- package/commands/nuke.mjs +73 -10
- package/commands/pull.mjs +60 -373
- package/commands/push.mjs +363 -286
- package/commands/search.mjs +3 -2
- package/commands/status.mjs +54 -79
- package/config.mjs +2 -2
- package/constants.mjs +6 -0
- package/ecc.mjs +180 -0
- package/fmt.mjs +2 -0
- package/git.mjs +467 -5
- package/hooks.mjs +49 -5
- package/integrate.mjs +11 -7
- package/link.mjs +7 -2
- 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, getLocalRegistryDir } 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();
|
|
15
|
-
|
|
17
|
+
|
|
18
|
+
const HOME = homedir();
|
|
19
|
+
const AW_HOME = join(HOME, '.aw');
|
|
20
|
+
const GLOBAL_AW_DIR = join(HOME, '.aw_registry');
|
|
21
|
+
const workspaceDir = getLocalRegistryDir(cwd, GLOBAL_AW_DIR);
|
|
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,36 +1,46 @@
|
|
|
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, readFileSync, rmSync } from 'node:fs';
|
|
8
8
|
import { execSync } from 'node:child_process';
|
|
9
|
-
import { join, dirname } from 'node:path';
|
|
9
|
+
import { join, dirname, sep } from 'node:path';
|
|
10
10
|
import { homedir } from 'node:os';
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
|
-
import { readFileSync } from 'node:fs';
|
|
13
12
|
import * as config from '../config.mjs';
|
|
14
13
|
import * as fmt from '../fmt.mjs';
|
|
15
14
|
import { chalk } from '../fmt.mjs';
|
|
16
|
-
import { pullCommand, pullAsync } from './pull.mjs';
|
|
17
15
|
import { linkWorkspace } from '../link.mjs';
|
|
18
16
|
import { generateCommands, copyInstructions, initAwDocs } from '../integrate.mjs';
|
|
19
17
|
import { setupMcp } from '../mcp.mjs';
|
|
20
18
|
import { autoUpdate, promptUpdate } from '../update.mjs';
|
|
21
19
|
import { installGlobalHooks } from '../hooks.mjs';
|
|
20
|
+
import { installAwEcc } from '../ecc.mjs';
|
|
21
|
+
import {
|
|
22
|
+
initPersistentClone,
|
|
23
|
+
isValidClone,
|
|
24
|
+
fetchAndMerge,
|
|
25
|
+
addToSparseCheckout,
|
|
26
|
+
addProjectWorktree,
|
|
27
|
+
isWorktree,
|
|
28
|
+
includeToSparsePaths,
|
|
29
|
+
sparseCheckoutAsync,
|
|
30
|
+
cleanup,
|
|
31
|
+
} from '../git.mjs';
|
|
32
|
+
import { REGISTRY_DIR, REGISTRY_REPO } from '../constants.mjs';
|
|
22
33
|
|
|
23
34
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
24
35
|
const VERSION = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')).version;
|
|
25
36
|
|
|
26
37
|
const HOME = homedir();
|
|
27
38
|
const GLOBAL_AW_DIR = join(HOME, '.aw_registry');
|
|
28
|
-
const
|
|
39
|
+
const AW_HOME = join(HOME, '.aw');
|
|
29
40
|
|
|
30
41
|
// ── IDE tasks for auto-pull ─────────────────────────────────────────────
|
|
31
42
|
|
|
32
43
|
function installIdeTasks() {
|
|
33
|
-
// VS Code / Cursor task — runs aw pull on folder open
|
|
34
44
|
const vscodeTask = {
|
|
35
45
|
version: '2.0.0',
|
|
36
46
|
tasks: [
|
|
@@ -45,18 +55,15 @@ function installIdeTasks() {
|
|
|
45
55
|
],
|
|
46
56
|
};
|
|
47
57
|
|
|
48
|
-
// Install globally for VS Code and Cursor
|
|
49
58
|
for (const ide of ['Code', 'Cursor']) {
|
|
50
59
|
const userDir = join(HOME, 'Library', 'Application Support', ide, 'User');
|
|
51
60
|
if (!existsSync(userDir)) continue;
|
|
52
61
|
|
|
53
62
|
const tasksPath = join(userDir, 'tasks.json');
|
|
54
63
|
if (existsSync(tasksPath)) {
|
|
55
|
-
// Don't override existing tasks — check if aw task already there
|
|
56
64
|
try {
|
|
57
65
|
const existing = JSON.parse(readFileSync(tasksPath, 'utf8'));
|
|
58
66
|
if (existing.tasks?.some(t => t.label === 'aw: sync registry' || t.label === 'aw: pull registry')) continue;
|
|
59
|
-
// Add our task to existing
|
|
60
67
|
existing.tasks = existing.tasks || [];
|
|
61
68
|
existing.tasks.push(vscodeTask.tasks[0]);
|
|
62
69
|
writeFileSync(tasksPath, JSON.stringify(existing, null, 2) + '\n');
|
|
@@ -69,33 +76,10 @@ function installIdeTasks() {
|
|
|
69
76
|
}
|
|
70
77
|
}
|
|
71
78
|
|
|
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
79
|
const ALLOWED_NAMESPACES = ['platform', 'revex', 'mobile', 'commerce', 'leadgen', 'crm', 'marketplace', 'ai'];
|
|
96
80
|
|
|
97
81
|
export async function initCommand(args) {
|
|
98
|
-
|
|
82
|
+
let namespace = args['--namespace'] || null;
|
|
99
83
|
let user = args['--user'] || '';
|
|
100
84
|
const silent = args['--silent'] === true;
|
|
101
85
|
|
|
@@ -103,24 +87,11 @@ export async function initCommand(args) {
|
|
|
103
87
|
|
|
104
88
|
// ── Validate ──────────────────────────────────────────────────────────
|
|
105
89
|
|
|
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
|
|
90
|
+
let nsParts = namespace ? namespace.split('/') : [];
|
|
91
|
+
let team = nsParts[0] || null;
|
|
92
|
+
let subTeam = nsParts[1] || null;
|
|
93
|
+
let teamNS = subTeam ? `${team}-${subTeam}` : team;
|
|
94
|
+
let folderName = subTeam ? `${team}/${subTeam}` : team;
|
|
124
95
|
|
|
125
96
|
if (team && !ALLOWED_NAMESPACES.includes(team)) {
|
|
126
97
|
const list = ALLOWED_NAMESPACES.map(n => chalk.cyan(n)).join(', ');
|
|
@@ -151,70 +122,113 @@ export async function initCommand(args) {
|
|
|
151
122
|
fmt.cancel(`Invalid sub-team '${subTeam}' — must match: ${SLUG_RE}`);
|
|
152
123
|
}
|
|
153
124
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const
|
|
125
|
+
// ── Detect installation state ─────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
const repoUrl = `https://github.com/${REGISTRY_REPO}.git`;
|
|
128
|
+
const isGitNative = isValidClone(AW_HOME, repoUrl);
|
|
129
|
+
const isLegacy = !isGitNative && existsSync(GLOBAL_AW_DIR) && !lstatSync(GLOBAL_AW_DIR).isSymbolicLink();
|
|
130
|
+
|
|
131
|
+
// ── Probe remote registry to check if namespace exists (fresh install only) ──
|
|
132
|
+
|
|
133
|
+
let namespaceExistsInRemote = false;
|
|
134
|
+
if (folderName && !silent && !isGitNative && !isLegacy) {
|
|
135
|
+
try {
|
|
136
|
+
const probePaths = includeToSparsePaths([folderName]);
|
|
137
|
+
const probeDir = await sparseCheckoutAsync(REGISTRY_REPO, probePaths);
|
|
138
|
+
try {
|
|
139
|
+
const fullNsPath = join(probeDir, REGISTRY_DIR, ...folderName.split('/'));
|
|
140
|
+
namespaceExistsInRemote = existsSync(fullNsPath) &&
|
|
141
|
+
readdirSync(fullNsPath, { withFileTypes: true })
|
|
142
|
+
.some(d => d.isDirectory() && !d.name.startsWith('.'));
|
|
143
|
+
} finally {
|
|
144
|
+
cleanup(probeDir);
|
|
145
|
+
}
|
|
146
|
+
} catch {
|
|
147
|
+
namespaceExistsInRemote = true;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (folderName && !silent && !isGitNative && !isLegacy && !namespaceExistsInRemote && process.stdin.isTTY) {
|
|
152
|
+
const choice = await fmt.select({
|
|
153
|
+
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?`,
|
|
154
|
+
options: [
|
|
155
|
+
{ value: 'platform-only', label: 'Continue with platform/ only (recommended for most users)' },
|
|
156
|
+
{ value: 'create-namespace', label: `Use '${folderName}' namespace (will be created)` },
|
|
157
|
+
],
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
if (fmt.isCancel(choice)) {
|
|
161
|
+
fmt.cancel('Operation cancelled.');
|
|
162
|
+
process.exit(0);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (choice === 'platform-only') {
|
|
166
|
+
namespace = null; team = null; subTeam = null; teamNS = null; folderName = null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
157
170
|
const cwd = process.cwd();
|
|
158
171
|
|
|
159
|
-
// ──
|
|
172
|
+
// ── Re-init path: already set up with native git clone ────────────────
|
|
160
173
|
|
|
161
|
-
if (
|
|
174
|
+
if (isGitNative) {
|
|
162
175
|
const cfg = config.load(GLOBAL_AW_DIR);
|
|
163
176
|
|
|
164
|
-
// Add new sub-team if not already tracked
|
|
165
177
|
const isNewSubTeam = folderName && cfg && !cfg.include.includes(folderName);
|
|
166
178
|
if (isNewSubTeam) {
|
|
167
179
|
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
|
-
});
|
|
180
|
+
const newSparsePaths = [`.aw_registry/${folderName}`, `content`];
|
|
181
|
+
addToSparseCheckout(AW_HOME, newSparsePaths);
|
|
176
182
|
config.addPattern(GLOBAL_AW_DIR, folderName);
|
|
177
183
|
} else {
|
|
178
184
|
if (!silent) fmt.logStep('Already initialized — syncing...');
|
|
179
185
|
}
|
|
180
186
|
|
|
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);
|
|
187
|
+
const s = fmt.spinner ? fmt.spinner() : { start: () => {}, stop: () => {} };
|
|
188
|
+
if (!silent) s.start('Fetching latest...');
|
|
189
|
+
try {
|
|
190
|
+
fetchAndMerge(AW_HOME);
|
|
191
|
+
if (!silent) s.stop('Registry updated');
|
|
192
|
+
} catch (e) {
|
|
193
|
+
if (!silent) s.stop(chalk.yellow('Fetch failed (continuing with local)'));
|
|
199
194
|
}
|
|
200
195
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
196
|
+
const freshCfg = config.load(GLOBAL_AW_DIR);
|
|
197
|
+
|
|
198
|
+
await installAwEcc(cwd, { silent });
|
|
204
199
|
copyInstructions(HOME, null, freshCfg?.namespace || team) || [];
|
|
205
200
|
initAwDocs(HOME);
|
|
206
201
|
await setupMcp(HOME, freshCfg?.namespace || team, { silent });
|
|
207
202
|
if (cwd !== HOME) await setupMcp(cwd, freshCfg?.namespace || team, { silent });
|
|
208
203
|
installGlobalHooks();
|
|
209
204
|
|
|
210
|
-
//
|
|
211
|
-
if (cwd !== HOME
|
|
205
|
+
// Remove old local .git/hooks/post-checkout that pre-dates core.hooksPath (creates stale .aw_registry symlink)
|
|
206
|
+
if (cwd !== HOME) {
|
|
207
|
+
const oldLocalHook = join(cwd, '.git', 'hooks', 'post-checkout');
|
|
212
208
|
try {
|
|
213
|
-
|
|
214
|
-
if (
|
|
209
|
+
const content = readFileSync(oldLocalHook, 'utf8');
|
|
210
|
+
if (content.includes('aw: auto-link registry') || content.includes('ln -s "$AW_REGISTRY"')) {
|
|
211
|
+
rmSync(oldLocalHook);
|
|
212
|
+
if (!silent) fmt.logStep('Removed legacy .git/hooks/post-checkout');
|
|
213
|
+
}
|
|
214
|
+
} catch { /* not there, fine */ }
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const isInsideAw = cwd.endsWith(`${sep}.aw`) || cwd.includes(`${sep}.aw${sep}`);
|
|
218
|
+
if (cwd !== HOME && !isInsideAw && !isWorktree(join(cwd, '.aw'))) {
|
|
219
|
+
try {
|
|
220
|
+
addProjectWorktree(AW_HOME, cwd);
|
|
221
|
+
if (!silent) fmt.logStep('Linked current project as git worktree');
|
|
215
222
|
} catch { /* best effort */ }
|
|
216
223
|
}
|
|
217
224
|
|
|
225
|
+
// Wire ~/.claude/.cursor/.codex to the project's registry when in a project,
|
|
226
|
+
// so edits to project/.aw/.aw_registry/ are instantly visible in global IDE dirs.
|
|
227
|
+
const projectRegistryDir = cwd !== HOME ? join(cwd, '.aw', REGISTRY_DIR) : null;
|
|
228
|
+
const awDirForLinks = (projectRegistryDir && existsSync(projectRegistryDir)) ? projectRegistryDir : null;
|
|
229
|
+
linkWorkspace(HOME, awDirForLinks);
|
|
230
|
+
generateCommands(HOME);
|
|
231
|
+
|
|
218
232
|
if (silent) {
|
|
219
233
|
autoUpdate(await args._updateCheck);
|
|
220
234
|
} else {
|
|
@@ -223,64 +237,76 @@ export async function initCommand(args) {
|
|
|
223
237
|
'',
|
|
224
238
|
` ${chalk.green('✓')} Registry updated`,
|
|
225
239
|
` ${chalk.green('✓')} IDE integration refreshed`,
|
|
226
|
-
cwd !== HOME &&
|
|
240
|
+
cwd !== HOME && isWorktree(join(cwd, '.aw')) ? ` ${chalk.green('✓')} Current project linked` : null,
|
|
227
241
|
].filter(Boolean).join('\n'));
|
|
228
242
|
}
|
|
229
243
|
return;
|
|
230
244
|
}
|
|
231
245
|
|
|
246
|
+
// ── Legacy migration: old ~/.aw_registry/ dir → notify user ──────────
|
|
247
|
+
|
|
248
|
+
if (isLegacy) {
|
|
249
|
+
if (!silent) {
|
|
250
|
+
fmt.logWarn([
|
|
251
|
+
'Legacy installation detected (~/.aw_registry/ is a plain directory).',
|
|
252
|
+
'',
|
|
253
|
+
` Run ${chalk.bold('aw nuke')} first to remove the old install, then ${chalk.bold('aw init')} again.`,
|
|
254
|
+
` This will migrate to the new native git clone at ~/.aw/`,
|
|
255
|
+
].join('\n'));
|
|
256
|
+
}
|
|
257
|
+
// Fall through to full init — create AW_HOME fresh
|
|
258
|
+
}
|
|
259
|
+
|
|
232
260
|
// ── Full init: first time setup ───────────────────────────────────────
|
|
233
261
|
|
|
234
|
-
// Auto-detect user
|
|
235
262
|
if (!user) {
|
|
236
263
|
try {
|
|
237
264
|
user = execSync('git config user.name', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
238
265
|
} catch { /* git not configured */ }
|
|
239
266
|
}
|
|
240
267
|
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
268
|
+
// Determine sparse paths
|
|
269
|
+
const sparsePaths = [`.aw_registry/platform`, `content`, `.aw_registry/AW-PROTOCOL.md`, `CODEOWNERS`];
|
|
270
|
+
if (folderName) {
|
|
271
|
+
sparsePaths.push(`.aw_registry/${folderName}`);
|
|
272
|
+
}
|
|
245
273
|
|
|
246
274
|
fmt.note([
|
|
247
|
-
`${chalk.dim('source:')} ~/.aw_registry
|
|
275
|
+
`${chalk.dim('source:')} ~/.aw/ → ~/.aw_registry/ (symlink)`,
|
|
248
276
|
folderName ? `${chalk.dim('namespace:')} ${folderName}` : `${chalk.dim('namespace:')} ${chalk.dim('none')}`,
|
|
249
|
-
user ? `${chalk.dim('user:')} ${
|
|
277
|
+
user ? `${chalk.dim('user:')} ${user}` : null,
|
|
250
278
|
`${chalk.dim('version:')} v${VERSION}`,
|
|
251
|
-
].filter(Boolean).join('\n'), 'Config
|
|
279
|
+
].filter(Boolean).join('\n'), 'Config');
|
|
252
280
|
|
|
253
|
-
// Step 2: Pull registry content (parallel)
|
|
254
281
|
const s = fmt.spinner();
|
|
255
|
-
|
|
256
|
-
s.start(`Pulling ${pullTargets}...`);
|
|
282
|
+
s.start(`Cloning registry...`);
|
|
257
283
|
|
|
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
|
-
}
|
|
266
|
-
|
|
267
|
-
let pullResults;
|
|
268
284
|
try {
|
|
269
|
-
|
|
270
|
-
s.stop(
|
|
285
|
+
initPersistentClone(repoUrl, AW_HOME, sparsePaths);
|
|
286
|
+
s.stop('Registry cloned');
|
|
271
287
|
} catch (e) {
|
|
272
|
-
s.stop(chalk.red('
|
|
288
|
+
s.stop(chalk.red('Clone failed'));
|
|
273
289
|
fmt.cancel(e.message);
|
|
274
290
|
}
|
|
275
291
|
|
|
276
|
-
|
|
277
|
-
|
|
292
|
+
// Create backward-compat symlink: ~/.aw_registry/ → ~/.aw/.aw_registry/
|
|
293
|
+
if (!existsSync(GLOBAL_AW_DIR)) {
|
|
294
|
+
try {
|
|
295
|
+
symlinkSync(join(AW_HOME, REGISTRY_DIR), GLOBAL_AW_DIR);
|
|
296
|
+
fmt.logStep('Created ~/.aw_registry/ symlink');
|
|
297
|
+
} catch (e) {
|
|
298
|
+
fmt.logWarn(`Could not create symlink ~/.aw_registry/: ${e.message}`);
|
|
299
|
+
}
|
|
278
300
|
}
|
|
279
301
|
|
|
280
|
-
//
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
302
|
+
// Create sync config
|
|
303
|
+
const cfg = config.create(GLOBAL_AW_DIR, { namespace: team, user });
|
|
304
|
+
if (folderName) {
|
|
305
|
+
config.addPattern(GLOBAL_AW_DIR, folderName);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Step 3: Setup tasks, MCP, hooks
|
|
309
|
+
await installAwEcc(cwd, { silent });
|
|
284
310
|
const instructionFiles = copyInstructions(HOME, null, team) || [];
|
|
285
311
|
initAwDocs(HOME);
|
|
286
312
|
const mcpFiles = await setupMcp(HOME, team) || [];
|
|
@@ -288,39 +314,46 @@ export async function initCommand(args) {
|
|
|
288
314
|
const hooksInstalled = installGlobalHooks();
|
|
289
315
|
installIdeTasks();
|
|
290
316
|
|
|
291
|
-
//
|
|
292
|
-
if (cwd !== HOME
|
|
317
|
+
// Remove old local .git/hooks/post-checkout that pre-dates core.hooksPath
|
|
318
|
+
if (cwd !== HOME) {
|
|
319
|
+
const oldLocalHook = join(cwd, '.git', 'hooks', 'post-checkout');
|
|
320
|
+
try {
|
|
321
|
+
const content = readFileSync(oldLocalHook, 'utf8');
|
|
322
|
+
if (content.includes('aw: auto-link registry') || content.includes('ln -s "$AW_REGISTRY"')) {
|
|
323
|
+
rmSync(oldLocalHook);
|
|
324
|
+
}
|
|
325
|
+
} catch { /* not there, fine */ }
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Step 4: Link current project as a git worktree (gives IDE git panel)
|
|
329
|
+
const isInsideAw = cwd.endsWith(`${sep}.aw`) || cwd.includes(`${sep}.aw${sep}`);
|
|
330
|
+
if (cwd !== HOME && !isInsideAw && !isWorktree(join(cwd, '.aw'))) {
|
|
293
331
|
try {
|
|
294
|
-
|
|
295
|
-
fmt.logStep('Linked
|
|
332
|
+
addProjectWorktree(AW_HOME, cwd);
|
|
333
|
+
fmt.logStep('Linked current project as git worktree');
|
|
296
334
|
} catch { /* best effort */ }
|
|
297
335
|
}
|
|
298
336
|
|
|
299
|
-
// Step 5:
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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);
|
|
337
|
+
// Step 5: Wire ~/.claude/.cursor/.codex to the project's registry when in a project,
|
|
338
|
+
// so edits to project/.aw/.aw_registry/ are instantly visible in global IDE dirs.
|
|
339
|
+
fmt.logStep('Linking IDE symlinks...');
|
|
340
|
+
const projectRegistryDir = cwd !== HOME ? join(cwd, '.aw', REGISTRY_DIR) : null;
|
|
341
|
+
const awDirForLinks = (projectRegistryDir && existsSync(projectRegistryDir)) ? projectRegistryDir : null;
|
|
342
|
+
linkWorkspace(HOME, awDirForLinks);
|
|
343
|
+
generateCommands(HOME);
|
|
311
344
|
|
|
312
345
|
// Offer to update if a newer version is available
|
|
313
346
|
await promptUpdate(await args._updateCheck);
|
|
314
347
|
|
|
315
|
-
// Done
|
|
316
348
|
fmt.outro([
|
|
317
349
|
'Install complete',
|
|
318
350
|
'',
|
|
319
|
-
` ${chalk.green('✓')} Source of truth: ~/.
|
|
351
|
+
` ${chalk.green('✓')} Source of truth: ~/.aw/ (git clone)`,
|
|
352
|
+
` ${chalk.green('✓')} Symlink: ~/.aw_registry/ → ~/.aw/.aw_registry/`,
|
|
320
353
|
` ${chalk.green('✓')} IDE integration: ~/.claude/, ~/.cursor/, ~/.codex/`,
|
|
321
354
|
hooksInstalled ? ` ${chalk.green('✓')} Git hooks: auto-sync on pull/clone (core.hooksPath)` : null,
|
|
322
355
|
` ${chalk.green('✓')} IDE task: auto-sync on workspace open`,
|
|
323
|
-
cwd !== HOME &&
|
|
356
|
+
cwd !== HOME && isWorktree(join(cwd, '.aw')) ? ` ${chalk.green('✓')} Linked in current project` : null,
|
|
324
357
|
'',
|
|
325
358
|
` ${chalk.dim('Existing repos:')} ${chalk.bold('cd <project> && aw link')}`,
|
|
326
359
|
` ${chalk.dim('New clones:')} auto-linked via git hook`,
|