@ghl-ai/aw 0.1.34-beta.13 → 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 +100 -199
- package/commands/nuke.mjs +2 -14
- package/commands/pull.mjs +374 -129
- package/commands/push.mjs +39 -33
- package/commands/status.mjs +54 -75
- package/constants.mjs +1 -1
- package/git.mjs +39 -221
- 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 — Clean init:
|
|
1
|
+
// commands/init.mjs — Clean init: clone registry, link IDEs, global git hooks.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// No temp dirs. No manifest hashing. Git handles sync natively.
|
|
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.
|
|
8
6
|
|
|
9
|
-
import { mkdirSync, existsSync, writeFileSync, symlinkSync
|
|
7
|
+
import { mkdirSync, existsSync, writeFileSync, symlinkSync } from 'node:fs';
|
|
10
8
|
import { execSync } from 'node:child_process';
|
|
11
9
|
import { join, dirname } from 'node:path';
|
|
12
10
|
import { homedir } from 'node:os';
|
|
13
11
|
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { readFileSync } from 'node:fs';
|
|
14
13
|
import * as config from '../config.mjs';
|
|
15
14
|
import * as fmt from '../fmt.mjs';
|
|
16
15
|
import { chalk } from '../fmt.mjs';
|
|
17
|
-
import {
|
|
18
|
-
gitInit, gitPull, gitSparseAdd, gitSparseList, gitSparseSet, gitLsRemote,
|
|
19
|
-
isGitRepo, withLock, includeToSparsePaths, addGitExcludes,
|
|
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';
|
|
@@ -36,6 +30,7 @@ const MANIFEST_PATH = join(GLOBAL_AW_DIR, '.aw-manifest.json');
|
|
|
36
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
36
|
tasks: [
|
|
@@ -50,15 +45,18 @@ function installIdeTasks() {
|
|
|
50
45
|
],
|
|
51
46
|
};
|
|
52
47
|
|
|
48
|
+
// Install globally for VS Code and Cursor
|
|
53
49
|
for (const ide of ['Code', 'Cursor']) {
|
|
54
50
|
const userDir = join(HOME, 'Library', 'Application Support', ide, 'User');
|
|
55
51
|
if (!existsSync(userDir)) continue;
|
|
56
52
|
|
|
57
53
|
const tasksPath = join(userDir, 'tasks.json');
|
|
58
54
|
if (existsSync(tasksPath)) {
|
|
55
|
+
// Don't override existing tasks — check if aw task already there
|
|
59
56
|
try {
|
|
60
57
|
const existing = JSON.parse(readFileSync(tasksPath, 'utf8'));
|
|
61
58
|
if (existing.tasks?.some(t => t.label === 'aw: sync registry' || t.label === 'aw: pull registry')) continue;
|
|
59
|
+
// Add our task to existing
|
|
62
60
|
existing.tasks = existing.tasks || [];
|
|
63
61
|
existing.tasks.push(vscodeTask.tasks[0]);
|
|
64
62
|
writeFileSync(tasksPath, JSON.stringify(existing, null, 2) + '\n');
|
|
@@ -75,85 +73,27 @@ function saveManifest(data) {
|
|
|
75
73
|
writeFileSync(MANIFEST_PATH, JSON.stringify(data, null, 2) + '\n');
|
|
76
74
|
}
|
|
77
75
|
|
|
78
|
-
const ALLOWED_NAMESPACES = ['platform', 'revex', 'mobile', 'commerce', 'leadgen', 'crm', 'marketplace', 'ai'];
|
|
79
76
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
function bootstrapFromTemplate(folderName, teamNS) {
|
|
85
|
-
// Template and target are inside GLOBAL_AW_DIR (which IS .aw_registry/)
|
|
86
|
-
const templateDir = join(GLOBAL_AW_DIR, '[template]');
|
|
87
|
-
const targetDir = join(GLOBAL_AW_DIR, ...folderName.split('/'));
|
|
88
|
-
|
|
89
|
-
if (!existsSync(templateDir)) {
|
|
90
|
-
throw new Error('Template directory not found. Ensure [template] is in sparse checkout.');
|
|
91
|
-
}
|
|
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;
|
|
92
81
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
82
|
+
const counts = { ADD: 0, UPDATE: 0, CONFLICT: 0 };
|
|
83
|
+
for (const a of typeActions) counts[a.action] = (counts[a.action] || 0) + 1;
|
|
96
84
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (
|
|
101
|
-
|
|
102
|
-
} else if (entry.name.endsWith('.md')) {
|
|
103
|
-
const content = readFileSync(fullPath, 'utf8');
|
|
104
|
-
if (content.includes(search)) {
|
|
105
|
-
writeFileSync(fullPath, content.replaceAll(search, replace));
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Ensure platform/docs symlink points to content/ in the git repo root.
|
|
113
|
-
*/
|
|
114
|
-
function ensureDocsSymlink() {
|
|
115
|
-
const docsLink = join(GLOBAL_AW_DIR, 'platform', 'docs');
|
|
116
|
-
const contentDir = join(GLOBAL_GIT_DIR, 'content');
|
|
117
|
-
|
|
118
|
-
if (!existsSync(contentDir)) return;
|
|
119
|
-
|
|
120
|
-
try {
|
|
121
|
-
// Check if already a correct symlink
|
|
122
|
-
if (existsSync(docsLink) && lstatSync(docsLink).isSymbolicLink()) return;
|
|
123
|
-
|
|
124
|
-
// Remove existing real directory (sparse checkout creates it with just README.md)
|
|
125
|
-
if (existsSync(docsLink)) {
|
|
126
|
-
rmSync(docsLink, { recursive: true, force: true });
|
|
127
|
-
}
|
|
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(', ')})` : '';
|
|
128
90
|
|
|
129
|
-
|
|
130
|
-
// Relative: .aw_repo/.aw_registry/platform/docs → ../../../content
|
|
131
|
-
symlinkSync('../../../content', docsLink);
|
|
132
|
-
} catch { /* best effort */ }
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Create the ~/.aw_registry symlink pointing to the .aw_registry/ subdir inside the git repo.
|
|
137
|
-
*/
|
|
138
|
-
function ensureWorkspaceSymlink() {
|
|
139
|
-
const target = join(GLOBAL_GIT_DIR, REGISTRY_DIR);
|
|
140
|
-
|
|
141
|
-
if (existsSync(GLOBAL_AW_DIR)) {
|
|
142
|
-
// Already exists — check if it's the right symlink
|
|
143
|
-
try {
|
|
144
|
-
if (lstatSync(GLOBAL_AW_DIR).isSymbolicLink()) return; // already a symlink, good
|
|
145
|
-
} catch { /* */ }
|
|
146
|
-
return; // exists as real dir, handled by caller (backup)
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Ensure parent dir for the symlink target exists
|
|
150
|
-
if (!existsSync(target)) {
|
|
151
|
-
mkdirSync(target, { recursive: true });
|
|
91
|
+
fmt.logSuccess(`${chalk.cyan(pattern)}: ${typeActions.length} ${type}${detail}`);
|
|
152
92
|
}
|
|
153
|
-
|
|
154
|
-
symlinkSync(target, GLOBAL_AW_DIR);
|
|
155
93
|
}
|
|
156
94
|
|
|
95
|
+
const ALLOWED_NAMESPACES = ['platform', 'revex', 'mobile', 'commerce', 'leadgen', 'crm', 'marketplace', 'ai'];
|
|
96
|
+
|
|
157
97
|
export async function initCommand(args) {
|
|
158
98
|
const namespace = args['--namespace'] || null;
|
|
159
99
|
let user = args['--user'] || '';
|
|
@@ -175,11 +115,12 @@ export async function initCommand(args) {
|
|
|
175
115
|
].join('\n'));
|
|
176
116
|
}
|
|
177
117
|
|
|
118
|
+
// Parse team/sub-team
|
|
178
119
|
const nsParts = namespace ? namespace.split('/') : [];
|
|
179
120
|
const team = nsParts[0] || null;
|
|
180
121
|
const subTeam = nsParts[1] || null;
|
|
181
|
-
const teamNS = subTeam ? `${team}-${subTeam}` : team;
|
|
182
|
-
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
|
|
183
124
|
|
|
184
125
|
if (team && !ALLOWED_NAMESPACES.includes(team)) {
|
|
185
126
|
const list = ALLOWED_NAMESPACES.map(n => chalk.cyan(n)).join(', ');
|
|
@@ -210,59 +151,63 @@ export async function initCommand(args) {
|
|
|
210
151
|
fmt.cancel(`Invalid sub-team '${subTeam}' — must match: ${SLUG_RE}`);
|
|
211
152
|
}
|
|
212
153
|
|
|
213
|
-
const cwd = process.cwd();
|
|
214
|
-
const hasGitRepo = existsSync(GLOBAL_GIT_DIR) && existsSync(join(GLOBAL_GIT_DIR, '.git'));
|
|
215
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();
|
|
216
158
|
|
|
217
159
|
// ── Fast path: already initialized → just pull + link ─────────────────
|
|
218
160
|
|
|
219
|
-
if (
|
|
161
|
+
if (isExisting) {
|
|
220
162
|
const cfg = config.load(GLOBAL_AW_DIR);
|
|
163
|
+
|
|
164
|
+
// Add new sub-team if not already tracked
|
|
221
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
|
+
}
|
|
222
180
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const { updated, conflicts } = gitPull(GLOBAL_AW_DIR);
|
|
227
|
-
if (conflicts && !silent) {
|
|
228
|
-
fmt.logWarn('Merge conflicts detected — resolve manually in ~/.aw_repo/');
|
|
229
|
-
}
|
|
230
|
-
} catch (e) {
|
|
231
|
-
if (!silent) fmt.logWarn(`Pull failed: ${e.message}`);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
if (isNewSubTeam) {
|
|
235
|
-
if (!silent) fmt.logStep(`Adding sub-team ${chalk.cyan(folderName)}...`);
|
|
236
|
-
|
|
237
|
-
const nsExists = gitLsRemote(GLOBAL_AW_DIR, folderName);
|
|
238
|
-
if (nsExists) {
|
|
239
|
-
gitSparseAdd(GLOBAL_AW_DIR, [`${REGISTRY_DIR}/${folderName}`]);
|
|
240
|
-
} else {
|
|
241
|
-
gitSparseAdd(GLOBAL_AW_DIR, [`${REGISTRY_DIR}/[template]`]);
|
|
242
|
-
try { gitPull(GLOBAL_AW_DIR); } catch { /* already up to date */ }
|
|
243
|
-
bootstrapFromTemplate(folderName, teamNS);
|
|
244
|
-
// Remove [template] from sparse checkout
|
|
245
|
-
const currentPaths = gitSparseList(GLOBAL_AW_DIR);
|
|
246
|
-
const cleanPaths = currentPaths.filter(p => !p.includes('[template]'));
|
|
247
|
-
if (cleanPaths.length !== currentPaths.length) {
|
|
248
|
-
gitSparseSet(GLOBAL_AW_DIR, cleanPaths);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
config.addPattern(GLOBAL_AW_DIR, folderName);
|
|
253
|
-
}
|
|
254
|
-
}, { skipIfLocked: silent });
|
|
255
|
-
|
|
256
|
-
ensureDocsSymlink();
|
|
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.
|
|
257
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)
|
|
258
202
|
linkWorkspace(HOME);
|
|
259
203
|
generateCommands(HOME);
|
|
260
|
-
copyInstructions(HOME, null, freshCfg?.namespace || team);
|
|
204
|
+
copyInstructions(HOME, null, freshCfg?.namespace || team) || [];
|
|
261
205
|
initAwDocs(HOME);
|
|
262
|
-
setupMcp(HOME, freshCfg?.namespace || team);
|
|
206
|
+
setupMcp(HOME, freshCfg?.namespace || team) || [];
|
|
263
207
|
if (cwd !== HOME) setupMcp(cwd, freshCfg?.namespace || team);
|
|
264
208
|
installGlobalHooks();
|
|
265
209
|
|
|
210
|
+
// Link current project if needed
|
|
266
211
|
if (cwd !== HOME && !existsSync(join(cwd, '.aw_registry'))) {
|
|
267
212
|
try {
|
|
268
213
|
symlinkSync(GLOBAL_AW_DIR, join(cwd, '.aw_registry'));
|
|
@@ -286,23 +231,6 @@ export async function initCommand(args) {
|
|
|
286
231
|
|
|
287
232
|
// ── Full init: first time setup ───────────────────────────────────────
|
|
288
233
|
|
|
289
|
-
// Backup old non-git ~/.aw_registry/ if it's a real directory (not a symlink)
|
|
290
|
-
if (existsSync(GLOBAL_AW_DIR)) {
|
|
291
|
-
try {
|
|
292
|
-
if (!lstatSync(GLOBAL_AW_DIR).isSymbolicLink()) {
|
|
293
|
-
const backupDir = `${GLOBAL_AW_DIR}.bak-${new Date().toISOString().slice(0, 10)}`;
|
|
294
|
-
fmt.logWarn(`Existing ~/.aw_registry/ directory found — backing up to ${backupDir}`);
|
|
295
|
-
renameSync(GLOBAL_AW_DIR, backupDir);
|
|
296
|
-
}
|
|
297
|
-
} catch { /* */ }
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Backup old ~/.aw_repo/ if it exists but isn't a git repo
|
|
301
|
-
if (existsSync(GLOBAL_GIT_DIR) && !existsSync(join(GLOBAL_GIT_DIR, '.git'))) {
|
|
302
|
-
const backupDir = `${GLOBAL_GIT_DIR}.bak-${new Date().toISOString().slice(0, 10)}`;
|
|
303
|
-
renameSync(GLOBAL_GIT_DIR, backupDir);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
234
|
// Auto-detect user
|
|
307
235
|
if (!user) {
|
|
308
236
|
try {
|
|
@@ -310,74 +238,46 @@ export async function initCommand(args) {
|
|
|
310
238
|
} catch { /* git not configured */ }
|
|
311
239
|
}
|
|
312
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
|
+
|
|
313
246
|
fmt.note([
|
|
314
247
|
`${chalk.dim('source:')} ~/.aw_registry/`,
|
|
315
248
|
folderName ? `${chalk.dim('namespace:')} ${folderName}` : `${chalk.dim('namespace:')} ${chalk.dim('none')}`,
|
|
316
|
-
user ? `${chalk.dim('user:')} ${user}` : null,
|
|
249
|
+
user ? `${chalk.dim('user:')} ${cfg.user}` : null,
|
|
317
250
|
`${chalk.dim('version:')} v${VERSION}`,
|
|
318
251
|
].filter(Boolean).join('\n'), 'Config created');
|
|
319
252
|
|
|
320
|
-
// Step
|
|
253
|
+
// Step 2: Pull registry content (parallel)
|
|
321
254
|
const s = fmt.spinner();
|
|
322
255
|
const pullTargets = folderName ? `platform + ${folderName}` : 'platform';
|
|
323
|
-
s.start(`
|
|
256
|
+
s.start(`Pulling ${pullTargets}...`);
|
|
324
257
|
|
|
325
|
-
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
|
+
}
|
|
326
266
|
|
|
267
|
+
let pullResults;
|
|
327
268
|
try {
|
|
328
|
-
|
|
329
|
-
s.stop(
|
|
269
|
+
pullResults = await Promise.all(pullJobs);
|
|
270
|
+
s.stop(`Pulled ${pullTargets}`);
|
|
330
271
|
} catch (e) {
|
|
331
|
-
s.stop(chalk.red('
|
|
272
|
+
s.stop(chalk.red('Pull failed'));
|
|
332
273
|
fmt.cancel(e.message);
|
|
333
274
|
}
|
|
334
275
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
// Step 3: Add git excludes for local config files
|
|
339
|
-
addGitExcludes(GLOBAL_GIT_DIR, [
|
|
340
|
-
`${REGISTRY_DIR}/.sync-config.json`,
|
|
341
|
-
`${REGISTRY_DIR}/.aw-manifest.json`,
|
|
342
|
-
'.aw-lock/',
|
|
343
|
-
]);
|
|
344
|
-
|
|
345
|
-
// Step 4: Write config (goes into the symlinked dir)
|
|
346
|
-
const newCfg = config.create(GLOBAL_AW_DIR, { namespace: team, user });
|
|
347
|
-
|
|
348
|
-
// Step 5: Add namespace via sparse-checkout
|
|
349
|
-
if (folderName) {
|
|
350
|
-
const s2 = fmt.spinner();
|
|
351
|
-
s2.start(`Setting up namespace ${chalk.cyan(folderName)}...`);
|
|
352
|
-
|
|
353
|
-
const nsExists = gitLsRemote(GLOBAL_AW_DIR, folderName);
|
|
354
|
-
if (nsExists) {
|
|
355
|
-
gitSparseAdd(GLOBAL_AW_DIR, [`${REGISTRY_DIR}/${folderName}`]);
|
|
356
|
-
s2.stop(`Namespace ${chalk.cyan(folderName)} checked out`);
|
|
357
|
-
} else {
|
|
358
|
-
// Temporarily add [template] to sparse checkout, bootstrap, then remove it
|
|
359
|
-
gitSparseAdd(GLOBAL_AW_DIR, [`${REGISTRY_DIR}/[template]`]);
|
|
360
|
-
bootstrapFromTemplate(folderName, teamNS);
|
|
361
|
-
// Remove [template] from sparse checkout — no longer needed
|
|
362
|
-
const currentPaths = gitSparseList(GLOBAL_AW_DIR);
|
|
363
|
-
const cleanPaths = currentPaths.filter(p => !p.includes('[template]'));
|
|
364
|
-
if (cleanPaths.length !== currentPaths.length) {
|
|
365
|
-
gitSparseSet(GLOBAL_AW_DIR, cleanPaths);
|
|
366
|
-
}
|
|
367
|
-
s2.stop(`Namespace ${chalk.cyan(folderName)} bootstrapped from template`);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
config.addPattern(GLOBAL_AW_DIR, folderName);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
if (!newCfg.include.includes('platform')) {
|
|
374
|
-
config.addPattern(GLOBAL_AW_DIR, 'platform');
|
|
276
|
+
for (const { pattern, actions } of pullResults) {
|
|
277
|
+
printPullSummary(pattern, actions);
|
|
375
278
|
}
|
|
376
279
|
|
|
377
|
-
// Step
|
|
378
|
-
ensureDocsSymlink();
|
|
379
|
-
|
|
380
|
-
// Step 7: Link IDE dirs + setup tasks
|
|
280
|
+
// Step 3: Link IDE dirs + setup tasks
|
|
381
281
|
fmt.logStep('Linking IDE symlinks...');
|
|
382
282
|
linkWorkspace(HOME);
|
|
383
283
|
generateCommands(HOME);
|
|
@@ -388,7 +288,7 @@ export async function initCommand(args) {
|
|
|
388
288
|
const hooksInstalled = installGlobalHooks();
|
|
389
289
|
installIdeTasks();
|
|
390
290
|
|
|
391
|
-
// Step
|
|
291
|
+
// Step 4: Symlink in current directory if it's a git repo
|
|
392
292
|
if (cwd !== HOME && !existsSync(join(cwd, '.aw_registry'))) {
|
|
393
293
|
try {
|
|
394
294
|
symlinkSync(GLOBAL_AW_DIR, join(cwd, '.aw_registry'));
|
|
@@ -396,12 +296,11 @@ export async function initCommand(args) {
|
|
|
396
296
|
} catch { /* best effort */ }
|
|
397
297
|
}
|
|
398
298
|
|
|
399
|
-
// Step
|
|
299
|
+
// Step 5: Write manifest for nuke cleanup
|
|
400
300
|
const manifest = {
|
|
401
|
-
version:
|
|
301
|
+
version: 1,
|
|
402
302
|
installedAt: new Date().toISOString(),
|
|
403
303
|
globalDir: GLOBAL_AW_DIR,
|
|
404
|
-
gitDir: GLOBAL_GIT_DIR,
|
|
405
304
|
createdFiles: [
|
|
406
305
|
...instructionFiles.map(p => p.startsWith(HOME) ? p.slice(HOME.length + 1) : p),
|
|
407
306
|
...mcpFiles.map(p => p.startsWith(HOME) ? p.slice(HOME.length + 1) : p),
|
|
@@ -410,12 +309,14 @@ export async function initCommand(args) {
|
|
|
410
309
|
};
|
|
411
310
|
saveManifest(manifest);
|
|
412
311
|
|
|
312
|
+
// Offer to update if a newer version is available
|
|
413
313
|
await promptUpdate(await args._updateCheck);
|
|
414
314
|
|
|
315
|
+
// Done
|
|
415
316
|
fmt.outro([
|
|
416
317
|
'Install complete',
|
|
417
318
|
'',
|
|
418
|
-
` ${chalk.green('✓')} Source of truth: ~/.aw_registry
|
|
319
|
+
` ${chalk.green('✓')} Source of truth: ~/.aw_registry/`,
|
|
419
320
|
` ${chalk.green('✓')} IDE integration: ~/.claude/, ~/.cursor/, ~/.codex/`,
|
|
420
321
|
hooksInstalled ? ` ${chalk.green('✓')} Git hooks: auto-sync on pull/clone (core.hooksPath)` : null,
|
|
421
322
|
` ${chalk.green('✓')} IDE task: auto-sync on workspace open`,
|
package/commands/nuke.mjs
CHANGED
|
@@ -286,20 +286,8 @@ export function nukeCommand(args) {
|
|
|
286
286
|
} catch { /* not installed via npm or no permissions */ }
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
-
// 9. Remove ~/.
|
|
290
|
-
|
|
291
|
-
if (existsSync(GLOBAL_GIT_DIR)) {
|
|
292
|
-
rmSync(GLOBAL_GIT_DIR, { recursive: true, force: true });
|
|
293
|
-
fmt.logStep('Removed ~/.aw_repo/');
|
|
294
|
-
}
|
|
295
|
-
// Remove ~/.aw_registry/ — may be a symlink (new) or real dir (legacy)
|
|
296
|
-
try {
|
|
297
|
-
if (lstatSync(GLOBAL_AW_DIR).isSymbolicLink()) {
|
|
298
|
-
unlinkSync(GLOBAL_AW_DIR);
|
|
299
|
-
} else {
|
|
300
|
-
rmSync(GLOBAL_AW_DIR, { recursive: true, force: true });
|
|
301
|
-
}
|
|
302
|
-
} 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 });
|
|
303
291
|
fmt.logStep('Removed ~/.aw_registry/');
|
|
304
292
|
|
|
305
293
|
fmt.outro([
|