@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/commands/pull.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// commands/pull.mjs — Pull content from registry
|
|
1
|
+
// commands/pull.mjs — Pull content from registry using persistent git clone
|
|
2
2
|
|
|
3
3
|
import { mkdirSync, existsSync, readdirSync, copyFileSync, unlinkSync, rmdirSync } from 'node:fs';
|
|
4
4
|
import { join, relative } from 'node:path';
|
|
@@ -7,37 +7,20 @@ import { execSync } from 'node:child_process';
|
|
|
7
7
|
import * as config from '../config.mjs';
|
|
8
8
|
import * as fmt from '../fmt.mjs';
|
|
9
9
|
import { chalk } from '../fmt.mjs';
|
|
10
|
-
import {
|
|
11
|
-
import { REGISTRY_DIR, DOCS_SOURCE_DIR } from '../constants.mjs';
|
|
12
|
-
import { walkRegistryTree } from '../registry.mjs';
|
|
13
|
-
import { matchesAny } from '../glob.mjs';
|
|
14
|
-
import { computePlan } from '../plan.mjs';
|
|
15
|
-
import { applyActions } from '../apply.mjs';
|
|
16
|
-
import { update as updateManifest } from '../manifest.mjs';
|
|
17
|
-
import { resolveInput } from '../paths.mjs';
|
|
10
|
+
import { fetchAndMerge, addToSparseCheckout, isValidClone } from '../git.mjs';
|
|
11
|
+
import { REGISTRY_DIR, REGISTRY_REPO, DOCS_SOURCE_DIR } from '../constants.mjs';
|
|
18
12
|
import { linkWorkspace } from '../link.mjs';
|
|
19
13
|
import { generateCommands, copyInstructions } from '../integrate.mjs';
|
|
20
14
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (pattern !== 'platform') return actions;
|
|
25
|
-
return actions.filter(a => !(a.type === 'commands' && a.namespacePath === 'platform'));
|
|
26
|
-
}
|
|
15
|
+
const HOME = homedir();
|
|
16
|
+
const AW_HOME = join(HOME, '.aw');
|
|
17
|
+
const GLOBAL_AW_DIR = join(HOME, '.aw_registry');
|
|
27
18
|
|
|
28
19
|
export async function pullCommand(args) {
|
|
29
20
|
const input = args._positional?.[0] || '';
|
|
30
21
|
const cwd = process.cwd();
|
|
31
|
-
const GLOBAL_AW_DIR = join(homedir(), '.aw_registry');
|
|
32
|
-
const localDir = join(cwd, '.aw_registry');
|
|
33
|
-
const workspaceDir = args._workspaceDir || (existsSync(join(localDir, '.sync-config.json')) ? localDir : GLOBAL_AW_DIR);
|
|
34
|
-
const dryRun = args['--dry-run'] === true;
|
|
35
|
-
const verbose = args['-v'] === true || args['--verbose'] === true;
|
|
36
22
|
const silent = args['--silent'] === true || args._silent === true;
|
|
37
|
-
let renameNamespace = args._renameNamespace || null;
|
|
38
|
-
const teamNSOverride = args._teamNS || null;
|
|
39
23
|
|
|
40
|
-
// Silent mode: wrap fmt to suppress all output and exit cleanly on errors
|
|
41
24
|
const log = {
|
|
42
25
|
cancel: silent ? () => { process.exit(0); } : fmt.cancel,
|
|
43
26
|
logInfo: silent ? () => {} : fmt.logInfo,
|
|
@@ -50,304 +33,82 @@ export async function pullCommand(args) {
|
|
|
50
33
|
spinner: silent ? () => ({ start: () => {}, stop: () => {} }) : fmt.spinner,
|
|
51
34
|
};
|
|
52
35
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const cfg = config.load(workspaceDir);
|
|
56
|
-
if (!cfg) log.cancel('No .sync-config.json found. Run: aw init');
|
|
57
|
-
if (cfg.include.length === 0) {
|
|
58
|
-
log.cancel('Nothing to pull. Add paths first:\n\n aw pull <path>');
|
|
59
|
-
}
|
|
60
|
-
log.logInfo(`Pulling ${chalk.cyan(cfg.include.length)} synced path${cfg.include.length > 1 ? 's' : ''}...`);
|
|
61
|
-
const pullJobs = cfg.include.map(p => {
|
|
62
|
-
const isTeamNs = p !== 'platform';
|
|
63
|
-
const derivedTeamNS = isTeamNs ? p.replace(/\//g, '-') : undefined;
|
|
64
|
-
return pullAsync({
|
|
65
|
-
...args,
|
|
66
|
-
_positional: [isTeamNs ? '[template]' : p],
|
|
67
|
-
_workspaceDir: workspaceDir,
|
|
68
|
-
_skipIntegrate: true,
|
|
69
|
-
_renameNamespace: isTeamNs ? p : undefined,
|
|
70
|
-
_teamNS: derivedTeamNS,
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
await Promise.all(pullJobs);
|
|
74
|
-
// Post-pull IDE integration (once after all paths pulled)
|
|
75
|
-
linkWorkspace(cwd);
|
|
76
|
-
generateCommands(cwd);
|
|
77
|
-
copyInstructions(cwd, null, cfg.namespace);
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Resolve input — accepts both local and registry paths
|
|
82
|
-
const resolved = resolveInput(input, workspaceDir);
|
|
83
|
-
let pattern = resolved.registryPath;
|
|
84
|
-
|
|
85
|
-
if (!pattern) {
|
|
86
|
-
log.cancel(`Could not resolve "${input}" to a registry path`);
|
|
87
|
-
}
|
|
36
|
+
const repoUrl = `https://github.com/${REGISTRY_REPO}.git`;
|
|
37
|
+
const hasClone = isValidClone(AW_HOME, repoUrl);
|
|
88
38
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
39
|
+
if (!hasClone) {
|
|
40
|
+
log.cancel('Registry not initialized. Run: aw init');
|
|
41
|
+
return;
|
|
92
42
|
}
|
|
93
43
|
|
|
94
|
-
|
|
95
|
-
const cfg = config.load(workspaceDir);
|
|
44
|
+
const cfg = config.load(GLOBAL_AW_DIR);
|
|
96
45
|
if (!cfg) {
|
|
97
46
|
log.cancel('No .sync-config.json found. Run: aw init');
|
|
47
|
+
return;
|
|
98
48
|
}
|
|
99
49
|
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
pathsToFetch.push(renameNamespace);
|
|
50
|
+
// If input is a new namespace to add
|
|
51
|
+
if (input && input !== 'platform') {
|
|
52
|
+
const sparsePath = `.aw_registry/${input}`;
|
|
53
|
+
if (!cfg.include.includes(input)) {
|
|
54
|
+
log.logStep(`Adding namespace ${chalk.cyan(input)} to sparse checkout...`);
|
|
55
|
+
addToSparseCheckout(AW_HOME, [sparsePath, 'content']);
|
|
56
|
+
config.addPattern(GLOBAL_AW_DIR, input);
|
|
57
|
+
}
|
|
109
58
|
}
|
|
110
|
-
|
|
111
|
-
|
|
59
|
+
|
|
60
|
+
// Fetch + merge latest
|
|
61
|
+
const s = log.spinner();
|
|
62
|
+
s.start('Fetching latest from registry...');
|
|
63
|
+
let fetchResult = { updated: false, conflicts: [] };
|
|
112
64
|
try {
|
|
113
|
-
|
|
65
|
+
fetchResult = fetchAndMerge(AW_HOME);
|
|
66
|
+
s.stop(fetchResult.updated ? 'Registry updated' : 'Already up to date');
|
|
114
67
|
} catch (e) {
|
|
115
|
-
s.stop(chalk.
|
|
116
|
-
log.
|
|
68
|
+
s.stop(chalk.yellow('Fetch failed'));
|
|
69
|
+
if (!silent) log.logWarn(`Fetch error: ${e.message}`);
|
|
117
70
|
}
|
|
118
71
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
// Check if actual namespace exists in the registry
|
|
123
|
-
if (renameNamespace && pattern === '[template]') {
|
|
124
|
-
const actualNsTopDir = renameNamespace.split('/')[0];
|
|
125
|
-
const actualNsPath = join(regBase, actualNsTopDir);
|
|
126
|
-
if (existsSync(actualNsPath)) {
|
|
127
|
-
const fullNsPath = join(regBase, ...renameNamespace.split('/'));
|
|
128
|
-
if (existsSync(fullNsPath) && listDirs(fullNsPath).length > 0) {
|
|
129
|
-
pattern = renameNamespace;
|
|
130
|
-
renameNamespace = null; // No rename needed — using actual content
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const registryDirs = [];
|
|
136
|
-
if (existsSync(regBase)) {
|
|
137
|
-
for (const name of listDirs(regBase)) {
|
|
138
|
-
registryDirs.push({ name, path: join(regBase, name) });
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (registryDirs.length === 0) {
|
|
143
|
-
s.stop(chalk.red('Not found'));
|
|
144
|
-
if (silent) return;
|
|
145
|
-
log.cancel(`Nothing found in registry for ${chalk.cyan(pattern)}`);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Rename namespace if requested (e.g., [template] → dev)
|
|
149
|
-
if (renameNamespace) {
|
|
150
|
-
for (const rd of registryDirs) {
|
|
151
|
-
if (rd.name === pattern) {
|
|
152
|
-
rd.name = renameNamespace;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
// Also remap pattern for include filter matching
|
|
156
|
-
pattern = renameNamespace;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Validate pattern matches actual content
|
|
160
|
-
let hasMatch = false;
|
|
161
|
-
for (const { name, path } of registryDirs) {
|
|
162
|
-
const entries = walkRegistryTree(path, name);
|
|
163
|
-
if (entries.some(e => matchesAny(e.registryPath, [pattern]))) {
|
|
164
|
-
hasMatch = true;
|
|
165
|
-
break;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
if (!hasMatch) {
|
|
170
|
-
s.stop(chalk.red('Not found'));
|
|
171
|
-
cleanup(tempDir);
|
|
172
|
-
if (silent) return;
|
|
173
|
-
log.cancel(`Nothing found in registry for ${chalk.cyan(pattern)}\n\n Check the pattern exists in the registry repo.`);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const fetched = registryDirs.map(d => chalk.cyan(d.name)).join(', ');
|
|
177
|
-
s.stop(`Fetched ${fetched}`);
|
|
178
|
-
|
|
179
|
-
// Add to config if not already there
|
|
180
|
-
if (!cfg.include.includes(pattern)) {
|
|
181
|
-
config.addPattern(workspaceDir, pattern);
|
|
182
|
-
log.logSuccess(`Added ${chalk.cyan(pattern)} to config`);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Compute plan
|
|
186
|
-
const { actions: rawActions } = computePlan(registryDirs, workspaceDir, [pattern], {
|
|
187
|
-
skipOrphans: false,
|
|
188
|
-
});
|
|
189
|
-
const actions = filterActions(rawActions, pattern);
|
|
190
|
-
|
|
191
|
-
if (dryRun) {
|
|
192
|
-
printDryRun(actions, verbose);
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// Apply
|
|
197
|
-
const s2 = log.spinner();
|
|
198
|
-
s2.start('Applying changes...');
|
|
199
|
-
const conflictCount = applyActions(actions, { teamNS: teamNSOverride || renameNamespace || undefined });
|
|
200
|
-
updateManifest(workspaceDir, actions, cfg.namespace, { fromTemplate: !!renameNamespace });
|
|
201
|
-
s2.stop('Changes applied');
|
|
202
|
-
|
|
203
|
-
// Copy root-level registry files (e.g. AW-PROTOCOL.md) that are fetched via
|
|
204
|
-
// sparse checkout but never processed by walkRegistryTree (dirs-only pipeline).
|
|
205
|
-
const ROOT_REGISTRY_FILES = ['AW-PROTOCOL.md'];
|
|
206
|
-
for (const fname of ROOT_REGISTRY_FILES) {
|
|
207
|
-
const src = join(regBase, fname);
|
|
208
|
-
if (existsSync(src)) {
|
|
209
|
-
copyFileSync(src, join(workspaceDir, fname));
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Sync docs from repo content/ into platform/docs/ (markdown only, skip images)
|
|
214
|
-
const contentSrc = join(tempDir, DOCS_SOURCE_DIR);
|
|
215
|
-
if (existsSync(contentSrc)) {
|
|
216
|
-
const docsDest = join(workspaceDir, 'platform', 'docs');
|
|
217
|
-
syncMarkdownTree(contentSrc, docsDest);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// MCP registration (second-class — skip if not available)
|
|
221
|
-
if (cfg.namespace) {
|
|
222
|
-
registerMcp(cfg.namespace);
|
|
223
|
-
}
|
|
72
|
+
if (fetchResult.conflicts.length > 0) {
|
|
73
|
+
log.logWarn(`Conflicts in: ${fetchResult.conflicts.join(', ')}`);
|
|
74
|
+
}
|
|
224
75
|
|
|
225
|
-
|
|
226
|
-
|
|
76
|
+
// Sync content/ → platform/docs/
|
|
77
|
+
syncDocs(AW_HOME, GLOBAL_AW_DIR);
|
|
227
78
|
|
|
228
|
-
|
|
229
|
-
|
|
79
|
+
// Re-link IDE dirs
|
|
80
|
+
if (!args._skipIntegrate) {
|
|
81
|
+
linkWorkspace(HOME);
|
|
82
|
+
generateCommands(HOME);
|
|
83
|
+
copyInstructions(HOME, null, cfg.namespace);
|
|
84
|
+
if (cwd !== HOME) {
|
|
230
85
|
linkWorkspace(cwd);
|
|
231
86
|
generateCommands(cwd);
|
|
232
|
-
copyInstructions(cwd, null, cfg.namespace);
|
|
233
87
|
}
|
|
88
|
+
}
|
|
234
89
|
|
|
235
|
-
|
|
236
|
-
|
|
90
|
+
if (!silent) {
|
|
91
|
+
registerMcp(cfg.namespace);
|
|
92
|
+
log.outro('Pull complete');
|
|
237
93
|
}
|
|
238
94
|
}
|
|
239
95
|
|
|
240
96
|
/**
|
|
241
|
-
*
|
|
242
|
-
* then applies changes synchronously. Returns a summary object instead
|
|
243
|
-
* of printing directly — the caller controls output.
|
|
97
|
+
* Sync ~/.aw/content/ markdown files → ~/.aw_registry/platform/docs/
|
|
244
98
|
*/
|
|
245
|
-
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const resolved = resolveInput(input, workspaceDir);
|
|
252
|
-
let pattern = resolved.registryPath;
|
|
253
|
-
if (!pattern) throw new Error(`Could not resolve "${input}" to a registry path`);
|
|
254
|
-
|
|
255
|
-
if (!existsSync(workspaceDir)) mkdirSync(workspaceDir, { recursive: true });
|
|
256
|
-
|
|
257
|
-
const cfg = config.load(workspaceDir);
|
|
258
|
-
if (!cfg) throw new Error('No .sync-config.json found');
|
|
259
|
-
|
|
260
|
-
// When pulling from template with rename, also fetch the actual namespace —
|
|
261
|
-
// if it exists in the registry, use it directly instead of the template.
|
|
262
|
-
const pathsToFetch = [pattern];
|
|
263
|
-
if (renameNamespace && pattern === '[template]') {
|
|
264
|
-
pathsToFetch.push(renameNamespace);
|
|
265
|
-
}
|
|
266
|
-
const sparsePaths = includeToSparsePaths(pathsToFetch);
|
|
267
|
-
const tempDir = await sparseCheckoutAsync(cfg.repo, sparsePaths);
|
|
268
|
-
|
|
269
|
-
try {
|
|
270
|
-
const regBase = join(tempDir, REGISTRY_DIR);
|
|
271
|
-
|
|
272
|
-
// Check if actual namespace exists in the registry
|
|
273
|
-
let useActualNamespace = false;
|
|
274
|
-
if (renameNamespace && pattern === '[template]') {
|
|
275
|
-
const actualNsTopDir = renameNamespace.split('/')[0];
|
|
276
|
-
const actualNsPath = join(regBase, actualNsTopDir);
|
|
277
|
-
if (existsSync(actualNsPath)) {
|
|
278
|
-
// Verify the full namespace path has content (not just the top-level team dir)
|
|
279
|
-
const fullNsPath = join(regBase, ...renameNamespace.split('/'));
|
|
280
|
-
if (existsSync(fullNsPath) && listDirs(fullNsPath).length > 0) {
|
|
281
|
-
useActualNamespace = true;
|
|
282
|
-
pattern = renameNamespace;
|
|
283
|
-
renameNamespace = null; // No rename needed — using actual content
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const registryDirs = [];
|
|
289
|
-
if (existsSync(regBase)) {
|
|
290
|
-
for (const name of listDirs(regBase)) {
|
|
291
|
-
registryDirs.push({ name, path: join(regBase, name) });
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
if (registryDirs.length === 0) {
|
|
296
|
-
throw new Error(`Nothing found in registry for ${pattern}`);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
if (renameNamespace) {
|
|
300
|
-
for (const rd of registryDirs) {
|
|
301
|
-
if (rd.name === pattern) rd.name = renameNamespace;
|
|
302
|
-
}
|
|
303
|
-
pattern = renameNamespace;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
let hasMatch = false;
|
|
307
|
-
for (const { name, path } of registryDirs) {
|
|
308
|
-
const entries = walkRegistryTree(path, name);
|
|
309
|
-
if (entries.some(e => matchesAny(e.registryPath, [pattern]))) {
|
|
310
|
-
hasMatch = true;
|
|
311
|
-
break;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
if (!hasMatch) {
|
|
316
|
-
throw new Error(`Nothing found in registry for ${pattern}`);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
if (!cfg.include.includes(pattern)) {
|
|
320
|
-
config.addPattern(workspaceDir, pattern);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
const { actions: rawActions } = computePlan(registryDirs, workspaceDir, [pattern], { skipOrphans: false });
|
|
324
|
-
const actions = filterActions(rawActions, pattern);
|
|
325
|
-
const conflictCount = applyActions(actions, { teamNS: teamNSOverride || renameNamespace || undefined });
|
|
326
|
-
updateManifest(workspaceDir, actions, cfg.namespace, { fromTemplate: !!renameNamespace });
|
|
327
|
-
|
|
328
|
-
const ROOT_REGISTRY_FILES = ['AW-PROTOCOL.md'];
|
|
329
|
-
for (const fname of ROOT_REGISTRY_FILES) {
|
|
330
|
-
const src = join(regBase, fname);
|
|
331
|
-
if (existsSync(src)) copyFileSync(src, join(workspaceDir, fname));
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// Sync docs from repo content/ into platform/docs/ (markdown only, skip images)
|
|
335
|
-
const contentSrc = join(tempDir, DOCS_SOURCE_DIR);
|
|
336
|
-
if (existsSync(contentSrc)) {
|
|
337
|
-
const docsDest = join(workspaceDir, 'platform', 'docs');
|
|
338
|
-
syncMarkdownTree(contentSrc, docsDest);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
return { pattern, actions, conflictCount };
|
|
342
|
-
} finally {
|
|
343
|
-
cleanup(tempDir);
|
|
344
|
-
}
|
|
99
|
+
function syncDocs(awHome, globalAwDir) {
|
|
100
|
+
const contentSrc = join(awHome, DOCS_SOURCE_DIR);
|
|
101
|
+
if (!existsSync(contentSrc)) return;
|
|
102
|
+
const docsDest = join(globalAwDir, 'platform', 'docs');
|
|
103
|
+
syncMarkdownTree(contentSrc, docsDest);
|
|
345
104
|
}
|
|
346
105
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
106
|
+
/**
|
|
107
|
+
* pullAsync — kept for backward compat; now delegates to pullCommand.
|
|
108
|
+
*/
|
|
109
|
+
export async function pullAsync(args) {
|
|
110
|
+
await pullCommand({ ...args, _skipIntegrate: true });
|
|
111
|
+
return { pattern: args._positional?.[0] || '', actions: [], conflictCount: 0 };
|
|
351
112
|
}
|
|
352
113
|
|
|
353
114
|
/**
|
|
@@ -377,7 +138,6 @@ function syncMarkdownTree(src, dest) {
|
|
|
377
138
|
const remotePaths = collectMarkdownPaths(src, src);
|
|
378
139
|
const localPaths = collectMarkdownPaths(dest, dest);
|
|
379
140
|
|
|
380
|
-
// Copy new and updated files
|
|
381
141
|
for (const rel of remotePaths) {
|
|
382
142
|
const srcPath = join(src, rel);
|
|
383
143
|
const destPath = join(dest, rel);
|
|
@@ -385,7 +145,6 @@ function syncMarkdownTree(src, dest) {
|
|
|
385
145
|
copyFileSync(srcPath, destPath);
|
|
386
146
|
}
|
|
387
147
|
|
|
388
|
-
// Delete local files not on remote
|
|
389
148
|
for (const rel of localPaths) {
|
|
390
149
|
if (!remotePaths.has(rel)) {
|
|
391
150
|
const destPath = join(dest, rel);
|
|
@@ -393,7 +152,6 @@ function syncMarkdownTree(src, dest) {
|
|
|
393
152
|
}
|
|
394
153
|
}
|
|
395
154
|
|
|
396
|
-
// Prune empty directories (bottom-up)
|
|
397
155
|
function pruneEmpty(dir) {
|
|
398
156
|
if (!existsSync(dir)) return;
|
|
399
157
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
@@ -416,74 +174,3 @@ function registerMcp(namespace) {
|
|
|
416
174
|
fmt.logWarn('MCP registration failed (pull still successful)');
|
|
417
175
|
}
|
|
418
176
|
}
|
|
419
|
-
|
|
420
|
-
function printDryRun(actions, verbose) {
|
|
421
|
-
const counts = { ADD: 0, UPDATE: 0, CONFLICT: 0, ORPHAN: 0, UNCHANGED: 0 };
|
|
422
|
-
const lines = [];
|
|
423
|
-
|
|
424
|
-
for (const type of ['agents', 'skills', 'commands', 'evals']) {
|
|
425
|
-
const items = actions.filter(a => a.type === type);
|
|
426
|
-
if (items.length === 0) continue;
|
|
427
|
-
|
|
428
|
-
lines.push(chalk.bold(`${type}/`));
|
|
429
|
-
for (const act of items.sort((a, b) => a.targetFilename.localeCompare(b.targetFilename))) {
|
|
430
|
-
counts[act.action] = (counts[act.action] || 0) + 1;
|
|
431
|
-
if (!verbose && act.action === 'UNCHANGED') continue;
|
|
432
|
-
const ns = act.namespacePath ? chalk.dim(` [${act.namespacePath}]`) : '';
|
|
433
|
-
lines.push(` ${fmt.actionLabel(act.action)} ${act.targetFilename}${ns}`);
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
if (lines.length > 0) {
|
|
438
|
-
fmt.note(lines.join('\n'), 'Dry Run');
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
fmt.logInfo(`Summary: ${fmt.countSummary(counts)}`);
|
|
442
|
-
fmt.logWarn('No files modified (--dry-run)');
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
function printSummary(actions, verbose, conflictCount) {
|
|
446
|
-
const conflicts = actions.filter(a => a.action === 'CONFLICT');
|
|
447
|
-
|
|
448
|
-
for (const type of ['agents', 'skills', 'commands', 'evals']) {
|
|
449
|
-
const typeActions = actions.filter(a => a.type === type);
|
|
450
|
-
if (typeActions.length === 0) continue;
|
|
451
|
-
|
|
452
|
-
const counts = { ADD: 0, UPDATE: 0, CONFLICT: 0, ORPHAN: 0, UNCHANGED: 0 };
|
|
453
|
-
for (const a of typeActions) counts[a.action]++;
|
|
454
|
-
|
|
455
|
-
const parts = [];
|
|
456
|
-
if (counts.ADD > 0) parts.push(chalk.green(`${counts.ADD} new`));
|
|
457
|
-
if (counts.UPDATE > 0) parts.push(chalk.cyan(`${counts.UPDATE} updated`));
|
|
458
|
-
if (counts.CONFLICT > 0) parts.push(chalk.red(`${counts.CONFLICT} conflict`));
|
|
459
|
-
if (counts.ORPHAN > 0) parts.push(chalk.yellow(`${counts.ORPHAN} removed`));
|
|
460
|
-
const detail = parts.length > 0 ? ` (${parts.join(', ')})` : '';
|
|
461
|
-
|
|
462
|
-
fmt.logSuccess(`${typeActions.length} ${type} pulled${detail}`);
|
|
463
|
-
|
|
464
|
-
if (verbose) {
|
|
465
|
-
for (const a of typeActions.filter(a => a.action !== 'UNCHANGED')) {
|
|
466
|
-
const ns = a.namespacePath ? chalk.dim(` [${a.namespacePath}]`) : '';
|
|
467
|
-
fmt.logMessage(` ${fmt.actionLabel(a.action)} ${a.targetFilename}${ns}`);
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
if (conflicts.length > 0) {
|
|
473
|
-
const conflictLines = conflicts.map(c => {
|
|
474
|
-
return `${chalk.red('both modified:')} ${c.type}/${c.targetFilename}`;
|
|
475
|
-
}).join('\n');
|
|
476
|
-
|
|
477
|
-
fmt.note(
|
|
478
|
-
conflictLines + '\n\n' +
|
|
479
|
-
chalk.dim('Fix conflicts, then re-run pull to verify.\n') +
|
|
480
|
-
chalk.dim('grep -r "<<<<<<< " .aw_registry/'),
|
|
481
|
-
chalk.red('Merge Conflicts')
|
|
482
|
-
);
|
|
483
|
-
|
|
484
|
-
fmt.outro(chalk.red('Pull completed with conflicts — resolve and re-run'));
|
|
485
|
-
process.exit(1);
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
fmt.outro('Pull complete');
|
|
489
|
-
}
|