@ghl-ai/aw 0.1.34-beta.14 → 0.1.34-beta.15
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/apply.mjs +60 -0
- package/commands/drop.mjs +49 -34
- package/commands/init.mjs +119 -159
- package/commands/nuke.mjs +2 -16
- package/commands/pull.mjs +373 -137
- package/commands/push.mjs +39 -33
- package/commands/status.mjs +54 -75
- package/constants.mjs +1 -1
- package/git.mjs +41 -263
- package/link.mjs +1 -1
- package/manifest.mjs +64 -0
- package/package.json +4 -1
- package/plan.mjs +138 -0
package/apply.mjs
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// apply.mjs — Apply sync plan (file operations). Zero dependencies.
|
|
2
|
+
|
|
3
|
+
import { readFileSync, writeFileSync, mkdirSync, cpSync, existsSync } from 'node:fs';
|
|
4
|
+
import { dirname } from 'node:path';
|
|
5
|
+
|
|
6
|
+
function writeConflictMarkers(filePath, localContent, registryContent) {
|
|
7
|
+
writeFileSync(filePath, [
|
|
8
|
+
'<<<<<<< LOCAL',
|
|
9
|
+
localContent.trimEnd(),
|
|
10
|
+
'=======',
|
|
11
|
+
registryContent.trimEnd(),
|
|
12
|
+
'>>>>>>> REGISTRY',
|
|
13
|
+
'',
|
|
14
|
+
].join('\n'));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Apply actions to the workspace.
|
|
19
|
+
* All actions are file-level (no directory-level operations).
|
|
20
|
+
*
|
|
21
|
+
* @param {Array} actions
|
|
22
|
+
* @param {{ teamNS?: string }} opts
|
|
23
|
+
* teamNS — when set, replaces all occurrences of `$TEAM_NS` in file content
|
|
24
|
+
* with this value. Used when pulling [template] as a renamed team namespace.
|
|
25
|
+
* @returns {number} count of files with conflicts
|
|
26
|
+
*/
|
|
27
|
+
export function applyActions(actions, { teamNS } = {}) {
|
|
28
|
+
let conflicts = 0;
|
|
29
|
+
|
|
30
|
+
for (const act of actions) {
|
|
31
|
+
switch (act.action) {
|
|
32
|
+
case 'ADD':
|
|
33
|
+
case 'UPDATE':
|
|
34
|
+
mkdirSync(dirname(act.targetPath), { recursive: true });
|
|
35
|
+
if (teamNS && act.sourcePath.endsWith('.md')) {
|
|
36
|
+
const content = readFileSync(act.sourcePath, 'utf8').replaceAll('$TEAM_NS', teamNS);
|
|
37
|
+
writeFileSync(act.targetPath, content);
|
|
38
|
+
} else {
|
|
39
|
+
cpSync(act.sourcePath, act.targetPath);
|
|
40
|
+
}
|
|
41
|
+
break;
|
|
42
|
+
|
|
43
|
+
case 'CONFLICT': {
|
|
44
|
+
const local = existsSync(act.targetPath) ? readFileSync(act.targetPath, 'utf8') : '';
|
|
45
|
+
let registry = readFileSync(act.sourcePath, 'utf8');
|
|
46
|
+
if (teamNS) registry = registry.replaceAll('$TEAM_NS', teamNS);
|
|
47
|
+
mkdirSync(dirname(act.targetPath), { recursive: true });
|
|
48
|
+
writeConflictMarkers(act.targetPath, local, registry);
|
|
49
|
+
conflicts++;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
case 'ORPHAN':
|
|
54
|
+
case 'UNCHANGED':
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return conflicts;
|
|
60
|
+
}
|
package/commands/drop.mjs
CHANGED
|
@@ -1,21 +1,18 @@
|
|
|
1
1
|
// commands/drop.mjs — Remove file or synced path from workspace
|
|
2
2
|
|
|
3
|
-
import { join } from 'node:path';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
4
|
import { rmSync, existsSync } from 'node:fs';
|
|
5
|
-
import { homedir } from 'node:os';
|
|
6
5
|
import * as config from '../config.mjs';
|
|
7
6
|
import * as fmt from '../fmt.mjs';
|
|
8
7
|
import { chalk } from '../fmt.mjs';
|
|
8
|
+
import { matchesAny } from '../glob.mjs';
|
|
9
9
|
import { resolveInput } from '../paths.mjs';
|
|
10
|
-
import {
|
|
11
|
-
import { REGISTRY_DIR } from '../constants.mjs';
|
|
10
|
+
import { load as loadManifest, save as saveManifest } from '../manifest.mjs';
|
|
12
11
|
|
|
13
12
|
export function dropCommand(args) {
|
|
14
13
|
const input = args._positional?.[0];
|
|
15
14
|
const cwd = process.cwd();
|
|
16
|
-
const
|
|
17
|
-
const localDir = join(cwd, '.aw_registry');
|
|
18
|
-
const workspaceDir = existsSync(join(localDir, '.sync-config.json')) ? localDir : GLOBAL_AW_DIR;
|
|
15
|
+
const workspaceDir = join(cwd, '.aw_registry');
|
|
19
16
|
|
|
20
17
|
fmt.intro('aw drop');
|
|
21
18
|
|
|
@@ -26,10 +23,7 @@ export function dropCommand(args) {
|
|
|
26
23
|
const cfg = config.load(workspaceDir);
|
|
27
24
|
if (!cfg) fmt.cancel('No .sync-config.json found. Run: aw init');
|
|
28
25
|
|
|
29
|
-
|
|
30
|
-
fmt.cancel('Registry not initialized as git repo. Run: aw init');
|
|
31
|
-
}
|
|
32
|
-
|
|
26
|
+
// Resolve to registry path (accepts both local and registry paths)
|
|
33
27
|
const resolved = resolveInput(input, workspaceDir);
|
|
34
28
|
const regPath = resolved.registryPath;
|
|
35
29
|
|
|
@@ -37,37 +31,58 @@ export function dropCommand(args) {
|
|
|
37
31
|
fmt.cancel(`Could not resolve "${input}" to a registry path`);
|
|
38
32
|
}
|
|
39
33
|
|
|
40
|
-
// Check if this
|
|
34
|
+
// Check if this path (or a parent) is in config → remove from config + delete files
|
|
41
35
|
const isConfigPath = cfg.include.some(p => p === regPath || p.startsWith(regPath + '/'));
|
|
42
36
|
|
|
43
37
|
if (isConfigPath) {
|
|
44
38
|
config.removePattern(workspaceDir, regPath);
|
|
45
39
|
fmt.logSuccess(`Removed ${chalk.cyan(regPath)} from sync config`);
|
|
40
|
+
}
|
|
46
41
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const currentPaths = gitSparseList(workspaceDir);
|
|
50
|
-
const newPaths = currentPaths.filter(p => p !== sparsePath && !p.startsWith(sparsePath + '/'));
|
|
42
|
+
// Delete matching local files
|
|
43
|
+
const removed = deleteMatchingFiles(workspaceDir, regPath);
|
|
51
44
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
rmSync(targetPath, { recursive: true, force: true });
|
|
61
|
-
fmt.logInfo(`Removed ${chalk.cyan(regPath)} from workspace`);
|
|
62
|
-
fmt.logWarn(`Path still in sync config — next ${chalk.dim('aw pull')} will restore it`);
|
|
63
|
-
} else if (existsSync(targetPath + '.md')) {
|
|
64
|
-
rmSync(targetPath + '.md', { force: true });
|
|
65
|
-
fmt.logInfo(`Removed ${chalk.cyan(regPath)}.md from workspace`);
|
|
66
|
-
fmt.logWarn(`Path still in sync config — next ${chalk.dim('aw pull')} will restore it`);
|
|
67
|
-
} else {
|
|
68
|
-
fmt.cancel(`Nothing found for ${chalk.cyan(regPath)}.\n\n Use ${chalk.dim('aw status')} to see synced paths.`);
|
|
69
|
-
}
|
|
45
|
+
if (removed > 0) {
|
|
46
|
+
fmt.logInfo(`${chalk.bold(removed)} file${removed > 1 ? 's' : ''} removed from workspace`);
|
|
47
|
+
} else if (!isConfigPath) {
|
|
48
|
+
fmt.cancel(`Nothing found for ${chalk.cyan(regPath)}.\n\n Use ${chalk.dim('aw status')} to see synced paths.`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!isConfigPath && removed > 0) {
|
|
52
|
+
fmt.logWarn(`Path still in sync config — next ${chalk.dim('aw pull')} will restore it`);
|
|
70
53
|
}
|
|
71
54
|
|
|
72
55
|
fmt.outro('Done');
|
|
73
56
|
}
|
|
57
|
+
|
|
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$/, '');
|
|
88
|
+
}
|
package/commands/init.mjs
CHANGED
|
@@ -1,25 +1,19 @@
|
|
|
1
|
-
// commands/init.mjs —
|
|
1
|
+
// commands/init.mjs — Clean init: clone registry, link IDEs, global git hooks.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// ~/.aw_registry/platform/docs — symlink to content/ (full docs)
|
|
3
|
+
// No shell profile modifications. No daemons. No background processes.
|
|
4
|
+
// Uses core.hooksPath (git-lfs pattern) for system-wide hook interception.
|
|
5
|
+
// Uses IDE tasks for auto-pull on workspace open.
|
|
7
6
|
|
|
8
|
-
import { mkdirSync, existsSync, writeFileSync, symlinkSync
|
|
7
|
+
import { mkdirSync, existsSync, writeFileSync, symlinkSync } from 'node:fs';
|
|
9
8
|
import { execSync } from 'node:child_process';
|
|
10
9
|
import { join, dirname } from 'node:path';
|
|
11
10
|
import { homedir } from 'node:os';
|
|
12
11
|
import { fileURLToPath } from 'node:url';
|
|
12
|
+
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 {
|
|
17
|
-
gitInit, gitPull, gitSparseAdd, gitSparseList, gitSparseSet, gitLsRemote,
|
|
18
|
-
isGitRepo, withLock, includeToSparsePaths, addGitExcludes,
|
|
19
|
-
ensureNamespaceLinks, ensureDocsSymlink,
|
|
20
|
-
GLOBAL_GIT_DIR,
|
|
21
|
-
} from '../git.mjs';
|
|
22
|
-
import { REGISTRY_BASE_BRANCH, REGISTRY_DIR, REGISTRY_REPO } from '../constants.mjs';
|
|
16
|
+
import { pullCommand, pullAsync } from './pull.mjs';
|
|
23
17
|
import { linkWorkspace } from '../link.mjs';
|
|
24
18
|
import { generateCommands, copyInstructions, initAwDocs } from '../integrate.mjs';
|
|
25
19
|
import { setupMcp } from '../mcp.mjs';
|
|
@@ -33,29 +27,36 @@ const HOME = homedir();
|
|
|
33
27
|
const GLOBAL_AW_DIR = join(HOME, '.aw_registry');
|
|
34
28
|
const MANIFEST_PATH = join(GLOBAL_AW_DIR, '.aw-manifest.json');
|
|
35
29
|
|
|
36
|
-
// ── IDE tasks
|
|
30
|
+
// ── IDE tasks for auto-pull ─────────────────────────────────────────────
|
|
37
31
|
|
|
38
32
|
function installIdeTasks() {
|
|
33
|
+
// VS Code / Cursor task — runs aw pull on folder open
|
|
39
34
|
const vscodeTask = {
|
|
40
35
|
version: '2.0.0',
|
|
41
|
-
tasks: [
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
36
|
+
tasks: [
|
|
37
|
+
{
|
|
38
|
+
label: 'aw: sync registry',
|
|
39
|
+
type: 'shell',
|
|
40
|
+
command: 'aw init --silent',
|
|
41
|
+
presentation: { reveal: 'silent', panel: 'shared', close: true },
|
|
42
|
+
runOptions: { runOn: 'folderOpen' },
|
|
43
|
+
problemMatcher: [],
|
|
44
|
+
},
|
|
45
|
+
],
|
|
49
46
|
};
|
|
50
47
|
|
|
48
|
+
// Install globally for VS Code and Cursor
|
|
51
49
|
for (const ide of ['Code', 'Cursor']) {
|
|
52
50
|
const userDir = join(HOME, 'Library', 'Application Support', ide, 'User');
|
|
53
51
|
if (!existsSync(userDir)) continue;
|
|
52
|
+
|
|
54
53
|
const tasksPath = join(userDir, 'tasks.json');
|
|
55
54
|
if (existsSync(tasksPath)) {
|
|
55
|
+
// Don't override existing tasks — check if aw task already there
|
|
56
56
|
try {
|
|
57
57
|
const existing = JSON.parse(readFileSync(tasksPath, 'utf8'));
|
|
58
58
|
if (existing.tasks?.some(t => t.label === 'aw: sync registry' || t.label === 'aw: pull registry')) continue;
|
|
59
|
+
// Add our task to existing
|
|
59
60
|
existing.tasks = existing.tasks || [];
|
|
60
61
|
existing.tasks.push(vscodeTask.tasks[0]);
|
|
61
62
|
writeFileSync(tasksPath, JSON.stringify(existing, null, 2) + '\n');
|
|
@@ -72,48 +73,26 @@ function saveManifest(data) {
|
|
|
72
73
|
writeFileSync(MANIFEST_PATH, JSON.stringify(data, null, 2) + '\n');
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
const ALLOWED_NAMESPACES = ['platform', 'revex', 'mobile', 'commerce', 'leadgen', 'crm', 'marketplace', 'ai'];
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const gitDir = GLOBAL_GIT_DIR;
|
|
82
|
-
const templateDir = join(gitDir, REGISTRY_DIR, '[template]');
|
|
83
|
-
const targetDir = join(gitDir, REGISTRY_DIR, ...folderName.split('/'));
|
|
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;
|
|
84
81
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
82
|
+
const counts = { ADD: 0, UPDATE: 0, CONFLICT: 0 };
|
|
83
|
+
for (const a of typeActions) counts[a.action] = (counts[a.action] || 0) + 1;
|
|
88
84
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
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(', ')})` : '';
|
|
92
90
|
|
|
93
|
-
|
|
94
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
95
|
-
const fullPath = join(dir, entry.name);
|
|
96
|
-
if (entry.isDirectory()) {
|
|
97
|
-
walkAndReplace(fullPath, search, replace);
|
|
98
|
-
} else if (entry.name.endsWith('.md')) {
|
|
99
|
-
const content = readFileSync(fullPath, 'utf8');
|
|
100
|
-
if (content.includes(search)) {
|
|
101
|
-
writeFileSync(fullPath, content.replaceAll(search, replace));
|
|
102
|
-
}
|
|
103
|
-
}
|
|
91
|
+
fmt.logSuccess(`${chalk.cyan(pattern)}: ${typeActions.length} ${type}${detail}`);
|
|
104
92
|
}
|
|
105
93
|
}
|
|
106
94
|
|
|
107
|
-
|
|
108
|
-
* Remove [template] from sparse checkout after bootstrapping.
|
|
109
|
-
*/
|
|
110
|
-
function removeTemplateFromSparse() {
|
|
111
|
-
const currentPaths = gitSparseList(GLOBAL_AW_DIR);
|
|
112
|
-
const cleanPaths = currentPaths.filter(p => !p.includes('[template]'));
|
|
113
|
-
if (cleanPaths.length !== currentPaths.length) {
|
|
114
|
-
gitSparseSet(GLOBAL_AW_DIR, cleanPaths);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
95
|
+
const ALLOWED_NAMESPACES = ['platform', 'revex', 'mobile', 'commerce', 'leadgen', 'crm', 'marketplace', 'ai'];
|
|
117
96
|
|
|
118
97
|
export async function initCommand(args) {
|
|
119
98
|
const namespace = args['--namespace'] || null;
|
|
@@ -122,7 +101,7 @@ export async function initCommand(args) {
|
|
|
122
101
|
|
|
123
102
|
fmt.intro(`aw init ${chalk.dim('v' + VERSION)}`);
|
|
124
103
|
|
|
125
|
-
// ── Validate
|
|
104
|
+
// ── Validate ──────────────────────────────────────────────────────────
|
|
126
105
|
|
|
127
106
|
if (!namespace && !silent) {
|
|
128
107
|
const list = ALLOWED_NAMESPACES.map(n => chalk.cyan(n)).join(', ');
|
|
@@ -136,11 +115,12 @@ export async function initCommand(args) {
|
|
|
136
115
|
].join('\n'));
|
|
137
116
|
}
|
|
138
117
|
|
|
118
|
+
// Parse team/sub-team
|
|
139
119
|
const nsParts = namespace ? namespace.split('/') : [];
|
|
140
120
|
const team = nsParts[0] || null;
|
|
141
121
|
const subTeam = nsParts[1] || null;
|
|
142
|
-
const teamNS = subTeam ? `${team}-${subTeam}` : team;
|
|
143
|
-
const folderName = subTeam ? `${team}/${subTeam}` : team;
|
|
122
|
+
const teamNS = subTeam ? `${team}-${subTeam}` : team; // for $TEAM_NS replacement
|
|
123
|
+
const folderName = subTeam ? `${team}/${subTeam}` : team; // for .aw_registry/ path
|
|
144
124
|
|
|
145
125
|
if (team && !ALLOWED_NAMESPACES.includes(team)) {
|
|
146
126
|
const list = ALLOWED_NAMESPACES.map(n => chalk.cyan(n)).join(', ');
|
|
@@ -171,55 +151,63 @@ export async function initCommand(args) {
|
|
|
171
151
|
fmt.cancel(`Invalid sub-team '${subTeam}' — must match: ${SLUG_RE}`);
|
|
172
152
|
}
|
|
173
153
|
|
|
174
|
-
const cwd = process.cwd();
|
|
175
|
-
const hasGitRepo = isGitRepo(GLOBAL_AW_DIR);
|
|
176
154
|
const hasConfig = config.exists(GLOBAL_AW_DIR);
|
|
155
|
+
const hasPlatform = existsSync(join(GLOBAL_AW_DIR, 'platform'));
|
|
156
|
+
const isExisting = hasConfig && hasPlatform;
|
|
157
|
+
const cwd = process.cwd();
|
|
177
158
|
|
|
178
|
-
// ── Fast path: already initialized
|
|
159
|
+
// ── Fast path: already initialized → just pull + link ─────────────────
|
|
179
160
|
|
|
180
|
-
if (
|
|
161
|
+
if (isExisting) {
|
|
181
162
|
const cfg = config.load(GLOBAL_AW_DIR);
|
|
182
|
-
const isNewSubTeam = folderName && cfg && !cfg.include.includes(folderName);
|
|
183
163
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
} else {
|
|
201
|
-
gitSparseAdd(GLOBAL_AW_DIR, [`${REGISTRY_DIR}/[template]`]);
|
|
202
|
-
try { gitPull(GLOBAL_AW_DIR); } catch { /* */ }
|
|
203
|
-
bootstrapFromTemplate(folderName, teamNS);
|
|
204
|
-
removeTemplateFromSparse();
|
|
205
|
-
}
|
|
206
|
-
config.addPattern(GLOBAL_AW_DIR, folderName);
|
|
207
|
-
}
|
|
208
|
-
}, { skipIfLocked: silent });
|
|
209
|
-
|
|
210
|
-
// Refresh symlinks + docs
|
|
211
|
-
ensureDocsSymlink(GLOBAL_AW_DIR);
|
|
212
|
-
ensureNamespaceLinks(GLOBAL_AW_DIR);
|
|
164
|
+
// Add new sub-team if not already tracked
|
|
165
|
+
const isNewSubTeam = folderName && cfg && !cfg.include.includes(folderName);
|
|
166
|
+
if (isNewSubTeam) {
|
|
167
|
+
if (!silent) fmt.logStep(`Adding sub-team ${chalk.cyan(folderName)}...`);
|
|
168
|
+
await pullAsync({
|
|
169
|
+
...args,
|
|
170
|
+
_positional: ['[template]'],
|
|
171
|
+
_renameNamespace: folderName,
|
|
172
|
+
_teamNS: teamNS,
|
|
173
|
+
_workspaceDir: GLOBAL_AW_DIR,
|
|
174
|
+
_skipIntegrate: true,
|
|
175
|
+
});
|
|
176
|
+
config.addPattern(GLOBAL_AW_DIR, folderName);
|
|
177
|
+
} else {
|
|
178
|
+
if (!silent) fmt.logStep('Already initialized — syncing...');
|
|
179
|
+
}
|
|
213
180
|
|
|
181
|
+
// Pull latest (parallel)
|
|
182
|
+
// cfg.include has the renamed namespace (e.g. 'revex/courses'), but the repo
|
|
183
|
+
// only has '.aw_registry/[template]/' — remap non-platform entries back.
|
|
214
184
|
const freshCfg = config.load(GLOBAL_AW_DIR);
|
|
185
|
+
if (freshCfg && freshCfg.include.length > 0) {
|
|
186
|
+
const pullJobs = freshCfg.include.map(p => {
|
|
187
|
+
const isTeamNs = p !== 'platform';
|
|
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);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Re-link IDE dirs + hooks (idempotent)
|
|
215
202
|
linkWorkspace(HOME);
|
|
216
203
|
generateCommands(HOME);
|
|
217
|
-
copyInstructions(HOME, null, freshCfg?.namespace || team);
|
|
204
|
+
copyInstructions(HOME, null, freshCfg?.namespace || team) || [];
|
|
218
205
|
initAwDocs(HOME);
|
|
219
|
-
setupMcp(HOME, freshCfg?.namespace || team);
|
|
206
|
+
setupMcp(HOME, freshCfg?.namespace || team) || [];
|
|
220
207
|
if (cwd !== HOME) setupMcp(cwd, freshCfg?.namespace || team);
|
|
221
208
|
installGlobalHooks();
|
|
222
209
|
|
|
210
|
+
// Link current project if needed
|
|
223
211
|
if (cwd !== HOME && !existsSync(join(cwd, '.aw_registry'))) {
|
|
224
212
|
try {
|
|
225
213
|
symlinkSync(GLOBAL_AW_DIR, join(cwd, '.aw_registry'));
|
|
@@ -241,86 +229,55 @@ export async function initCommand(args) {
|
|
|
241
229
|
return;
|
|
242
230
|
}
|
|
243
231
|
|
|
244
|
-
// ── Full init
|
|
245
|
-
|
|
246
|
-
// Backup old ~/.aw_registry/ if it's a real dir without our git clone
|
|
247
|
-
if (existsSync(GLOBAL_AW_DIR) && !isGitRepo(GLOBAL_AW_DIR)) {
|
|
248
|
-
const backupDir = `${GLOBAL_AW_DIR}.bak-${new Date().toISOString().slice(0, 10)}`;
|
|
249
|
-
fmt.logWarn(`Existing ~/.aw_registry/ found — backing up to ${backupDir}`);
|
|
250
|
-
renameSync(GLOBAL_AW_DIR, backupDir);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// Ensure ~/.aw_registry/ exists as a real directory
|
|
254
|
-
mkdirSync(GLOBAL_AW_DIR, { recursive: true });
|
|
232
|
+
// ── Full init: first time setup ───────────────────────────────────────
|
|
255
233
|
|
|
234
|
+
// Auto-detect user
|
|
256
235
|
if (!user) {
|
|
257
236
|
try {
|
|
258
237
|
user = execSync('git config user.name', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
259
|
-
} catch { /* */ }
|
|
238
|
+
} catch { /* git not configured */ }
|
|
260
239
|
}
|
|
261
240
|
|
|
241
|
+
// Step 1: Create global source of truth
|
|
242
|
+
mkdirSync(GLOBAL_AW_DIR, { recursive: true });
|
|
243
|
+
|
|
244
|
+
const cfg = config.create(GLOBAL_AW_DIR, { namespace: team, user });
|
|
245
|
+
|
|
262
246
|
fmt.note([
|
|
263
247
|
`${chalk.dim('source:')} ~/.aw_registry/`,
|
|
264
248
|
folderName ? `${chalk.dim('namespace:')} ${folderName}` : `${chalk.dim('namespace:')} ${chalk.dim('none')}`,
|
|
265
|
-
user ? `${chalk.dim('user:')} ${user}` : null,
|
|
249
|
+
user ? `${chalk.dim('user:')} ${cfg.user}` : null,
|
|
266
250
|
`${chalk.dim('version:')} v${VERSION}`,
|
|
267
251
|
].filter(Boolean).join('\n'), 'Config created');
|
|
268
252
|
|
|
269
|
-
// Step
|
|
253
|
+
// Step 2: Pull registry content (parallel)
|
|
270
254
|
const s = fmt.spinner();
|
|
271
255
|
const pullTargets = folderName ? `platform + ${folderName}` : 'platform';
|
|
272
|
-
s.start(`
|
|
256
|
+
s.start(`Pulling ${pullTargets}...`);
|
|
273
257
|
|
|
274
|
-
const
|
|
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
|
+
}
|
|
275
266
|
|
|
267
|
+
let pullResults;
|
|
276
268
|
try {
|
|
277
|
-
|
|
278
|
-
s.stop(
|
|
269
|
+
pullResults = await Promise.all(pullJobs);
|
|
270
|
+
s.stop(`Pulled ${pullTargets}`);
|
|
279
271
|
} catch (e) {
|
|
280
|
-
s.stop(chalk.red('
|
|
272
|
+
s.stop(chalk.red('Pull failed'));
|
|
281
273
|
fmt.cancel(e.message);
|
|
282
274
|
}
|
|
283
275
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
`${REGISTRY_DIR}/.sync-config.json`,
|
|
287
|
-
`${REGISTRY_DIR}/.aw-manifest.json`,
|
|
288
|
-
`${REGISTRY_DIR}/platform/docs`,
|
|
289
|
-
'.aw-lock/',
|
|
290
|
-
]);
|
|
291
|
-
|
|
292
|
-
// Step 3: Write config
|
|
293
|
-
const newCfg = config.create(GLOBAL_AW_DIR, { namespace: team, user });
|
|
294
|
-
|
|
295
|
-
// Step 4: Add namespace
|
|
296
|
-
if (folderName) {
|
|
297
|
-
const s2 = fmt.spinner();
|
|
298
|
-
s2.start(`Setting up namespace ${chalk.cyan(folderName)}...`);
|
|
299
|
-
|
|
300
|
-
const nsExists = gitLsRemote(GLOBAL_AW_DIR, folderName);
|
|
301
|
-
if (nsExists) {
|
|
302
|
-
gitSparseAdd(GLOBAL_AW_DIR, [`${REGISTRY_DIR}/${folderName}`]);
|
|
303
|
-
s2.stop(`Namespace ${chalk.cyan(folderName)} checked out`);
|
|
304
|
-
} else {
|
|
305
|
-
gitSparseAdd(GLOBAL_AW_DIR, [`${REGISTRY_DIR}/[template]`]);
|
|
306
|
-
bootstrapFromTemplate(folderName, teamNS);
|
|
307
|
-
removeTemplateFromSparse();
|
|
308
|
-
s2.stop(`Namespace ${chalk.cyan(folderName)} bootstrapped from template`);
|
|
309
|
-
}
|
|
310
|
-
config.addPattern(GLOBAL_AW_DIR, folderName);
|
|
276
|
+
for (const { pattern, actions } of pullResults) {
|
|
277
|
+
printPullSummary(pattern, actions);
|
|
311
278
|
}
|
|
312
279
|
|
|
313
|
-
|
|
314
|
-
config.addPattern(GLOBAL_AW_DIR, 'platform');
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// Step 5: Docs symlink (content/ → platform/docs)
|
|
318
|
-
ensureDocsSymlink(GLOBAL_AW_DIR);
|
|
319
|
-
|
|
320
|
-
// Step 6: Namespace symlinks (platform → .git-source/.aw_registry/platform)
|
|
321
|
-
ensureNamespaceLinks(GLOBAL_AW_DIR);
|
|
322
|
-
|
|
323
|
-
// Step 7: Link IDE dirs
|
|
280
|
+
// Step 3: Link IDE dirs + setup tasks
|
|
324
281
|
fmt.logStep('Linking IDE symlinks...');
|
|
325
282
|
linkWorkspace(HOME);
|
|
326
283
|
generateCommands(HOME);
|
|
@@ -331,7 +288,7 @@ export async function initCommand(args) {
|
|
|
331
288
|
const hooksInstalled = installGlobalHooks();
|
|
332
289
|
installIdeTasks();
|
|
333
290
|
|
|
334
|
-
// Step
|
|
291
|
+
// Step 4: Symlink in current directory if it's a git repo
|
|
335
292
|
if (cwd !== HOME && !existsSync(join(cwd, '.aw_registry'))) {
|
|
336
293
|
try {
|
|
337
294
|
symlinkSync(GLOBAL_AW_DIR, join(cwd, '.aw_registry'));
|
|
@@ -339,9 +296,9 @@ export async function initCommand(args) {
|
|
|
339
296
|
} catch { /* best effort */ }
|
|
340
297
|
}
|
|
341
298
|
|
|
342
|
-
// Step
|
|
343
|
-
|
|
344
|
-
version:
|
|
299
|
+
// Step 5: Write manifest for nuke cleanup
|
|
300
|
+
const manifest = {
|
|
301
|
+
version: 1,
|
|
345
302
|
installedAt: new Date().toISOString(),
|
|
346
303
|
globalDir: GLOBAL_AW_DIR,
|
|
347
304
|
createdFiles: [
|
|
@@ -349,14 +306,17 @@ export async function initCommand(args) {
|
|
|
349
306
|
...mcpFiles.map(p => p.startsWith(HOME) ? p.slice(HOME.length + 1) : p),
|
|
350
307
|
],
|
|
351
308
|
globalHooksDir: hooksInstalled ? join(HOME, '.aw', 'hooks') : null,
|
|
352
|
-
}
|
|
309
|
+
};
|
|
310
|
+
saveManifest(manifest);
|
|
353
311
|
|
|
312
|
+
// Offer to update if a newer version is available
|
|
354
313
|
await promptUpdate(await args._updateCheck);
|
|
355
314
|
|
|
315
|
+
// Done
|
|
356
316
|
fmt.outro([
|
|
357
317
|
'Install complete',
|
|
358
318
|
'',
|
|
359
|
-
` ${chalk.green('✓')} Source of truth: ~/.aw_registry
|
|
319
|
+
` ${chalk.green('✓')} Source of truth: ~/.aw_registry/`,
|
|
360
320
|
` ${chalk.green('✓')} IDE integration: ~/.claude/, ~/.cursor/, ~/.codex/`,
|
|
361
321
|
hooksInstalled ? ` ${chalk.green('✓')} Git hooks: auto-sync on pull/clone (core.hooksPath)` : null,
|
|
362
322
|
` ${chalk.green('✓')} IDE task: auto-sync on workspace open`,
|
package/commands/nuke.mjs
CHANGED
|
@@ -286,24 +286,10 @@ export function nukeCommand(args) {
|
|
|
286
286
|
} catch { /* not installed via npm or no permissions */ }
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
-
// 9. Remove ~/.aw_registry/ (
|
|
290
|
-
|
|
291
|
-
try {
|
|
292
|
-
if (lstatSync(GLOBAL_AW_DIR).isSymbolicLink()) {
|
|
293
|
-
unlinkSync(GLOBAL_AW_DIR);
|
|
294
|
-
} else {
|
|
295
|
-
rmSync(GLOBAL_AW_DIR, { recursive: true, force: true });
|
|
296
|
-
}
|
|
297
|
-
} catch { rmSync(GLOBAL_AW_DIR, { recursive: true, force: true }); }
|
|
289
|
+
// 9. Remove ~/.aw_registry/ itself (source of truth — last!)
|
|
290
|
+
rmSync(GLOBAL_AW_DIR, { recursive: true, force: true });
|
|
298
291
|
fmt.logStep('Removed ~/.aw_registry/');
|
|
299
292
|
|
|
300
|
-
// Legacy cleanup: remove ~/.aw_repo/ if it exists from older versions
|
|
301
|
-
const legacyRepo = join(HOME, '.aw_repo');
|
|
302
|
-
if (existsSync(legacyRepo)) {
|
|
303
|
-
rmSync(legacyRepo, { recursive: true, force: true });
|
|
304
|
-
fmt.logStep('Removed legacy ~/.aw_repo/');
|
|
305
|
-
}
|
|
306
|
-
|
|
307
293
|
fmt.outro([
|
|
308
294
|
'Fully removed',
|
|
309
295
|
'',
|