@ghl-ai/aw 0.1.34-beta.8 → 0.1.34
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 +21 -2
- package/commands/pull.mjs +54 -15
- package/commands/push.mjs +19 -5
- package/constants.mjs +1 -1
- package/fmt.mjs +2 -2
- package/link.mjs +14 -4
- package/package.json +1 -1
- package/plan.mjs +28 -19
package/apply.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// apply.mjs — Apply sync plan (file operations). Zero dependencies.
|
|
2
2
|
|
|
3
|
-
import { readFileSync, writeFileSync, mkdirSync, cpSync, existsSync } from 'node:fs';
|
|
4
|
-
import { dirname } from 'node:path';
|
|
3
|
+
import { readFileSync, writeFileSync, mkdirSync, cpSync, existsSync, unlinkSync, rmdirSync, readdirSync } from 'node:fs';
|
|
4
|
+
import { dirname, basename } from 'node:path';
|
|
5
5
|
|
|
6
6
|
function writeConflictMarkers(filePath, localContent, registryContent) {
|
|
7
7
|
writeFileSync(filePath, [
|
|
@@ -51,6 +51,25 @@ export function applyActions(actions, { teamNS } = {}) {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
case 'ORPHAN':
|
|
54
|
+
// File was removed from registry — delete local copy
|
|
55
|
+
if (existsSync(act.targetPath)) {
|
|
56
|
+
try { unlinkSync(act.targetPath); } catch { /* best effort */ }
|
|
57
|
+
// Clean up empty parent dirs, stop at .aw_registry/ boundary
|
|
58
|
+
let dir = dirname(act.targetPath);
|
|
59
|
+
while (basename(dir) !== '.aw_registry') {
|
|
60
|
+
try {
|
|
61
|
+
const entries = readdirSync(dir);
|
|
62
|
+
if (entries.length === 0) {
|
|
63
|
+
rmdirSync(dir);
|
|
64
|
+
dir = dirname(dir);
|
|
65
|
+
} else {
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
} catch { break; }
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
break;
|
|
72
|
+
|
|
54
73
|
case 'UNCHANGED':
|
|
55
74
|
break;
|
|
56
75
|
}
|
package/commands/pull.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// commands/pull.mjs — Pull content from registry
|
|
2
2
|
|
|
3
|
-
import { mkdirSync, existsSync, readdirSync, copyFileSync } from 'node:fs';
|
|
4
|
-
import { join } from 'node:path';
|
|
3
|
+
import { mkdirSync, existsSync, readdirSync, copyFileSync, unlinkSync, rmdirSync } from 'node:fs';
|
|
4
|
+
import { join, relative } from 'node:path';
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
6
|
import { execSync } from 'node:child_process';
|
|
7
7
|
import * as config from '../config.mjs';
|
|
@@ -184,7 +184,7 @@ export async function pullCommand(args) {
|
|
|
184
184
|
|
|
185
185
|
// Compute plan
|
|
186
186
|
const { actions: rawActions } = computePlan(registryDirs, workspaceDir, [pattern], {
|
|
187
|
-
skipOrphans:
|
|
187
|
+
skipOrphans: false,
|
|
188
188
|
});
|
|
189
189
|
const actions = filterActions(rawActions, pattern);
|
|
190
190
|
|
|
@@ -214,7 +214,7 @@ export async function pullCommand(args) {
|
|
|
214
214
|
const contentSrc = join(tempDir, DOCS_SOURCE_DIR);
|
|
215
215
|
if (existsSync(contentSrc)) {
|
|
216
216
|
const docsDest = join(workspaceDir, 'platform', 'docs');
|
|
217
|
-
|
|
217
|
+
syncMarkdownTree(contentSrc, docsDest);
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
// MCP registration (second-class — skip if not available)
|
|
@@ -320,7 +320,7 @@ export async function pullAsync(args) {
|
|
|
320
320
|
config.addPattern(workspaceDir, pattern);
|
|
321
321
|
}
|
|
322
322
|
|
|
323
|
-
const { actions: rawActions } = computePlan(registryDirs, workspaceDir, [pattern], { skipOrphans:
|
|
323
|
+
const { actions: rawActions } = computePlan(registryDirs, workspaceDir, [pattern], { skipOrphans: false });
|
|
324
324
|
const actions = filterActions(rawActions, pattern);
|
|
325
325
|
const conflictCount = applyActions(actions, { teamNS: teamNSOverride || renameNamespace || undefined });
|
|
326
326
|
updateManifest(workspaceDir, actions, cfg.namespace, { fromTemplate: !!renameNamespace });
|
|
@@ -335,7 +335,7 @@ export async function pullAsync(args) {
|
|
|
335
335
|
const contentSrc = join(tempDir, DOCS_SOURCE_DIR);
|
|
336
336
|
if (existsSync(contentSrc)) {
|
|
337
337
|
const docsDest = join(workspaceDir, 'platform', 'docs');
|
|
338
|
-
|
|
338
|
+
syncMarkdownTree(contentSrc, docsDest);
|
|
339
339
|
}
|
|
340
340
|
|
|
341
341
|
return { pattern, actions, conflictCount };
|
|
@@ -351,21 +351,59 @@ function listDirs(dir) {
|
|
|
351
351
|
}
|
|
352
352
|
|
|
353
353
|
/**
|
|
354
|
-
*
|
|
355
|
-
* Skips images and other non-markdown content.
|
|
354
|
+
* Collect all .md file paths (relative) in a directory tree.
|
|
356
355
|
*/
|
|
357
|
-
function
|
|
358
|
-
|
|
359
|
-
|
|
356
|
+
function collectMarkdownPaths(dir, base) {
|
|
357
|
+
const paths = new Set();
|
|
358
|
+
if (!existsSync(dir)) return paths;
|
|
359
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
360
360
|
if (entry.name.startsWith('.')) continue;
|
|
361
|
-
const
|
|
362
|
-
const destPath = join(dest, entry.name);
|
|
361
|
+
const full = join(dir, entry.name);
|
|
363
362
|
if (entry.isDirectory()) {
|
|
364
|
-
|
|
363
|
+
for (const p of collectMarkdownPaths(full, base)) paths.add(p);
|
|
365
364
|
} else if (entry.name.endsWith('.md')) {
|
|
366
|
-
|
|
365
|
+
paths.add(relative(base, full));
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return paths;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Sync .md files from src to dest: copy new/changed, delete removed, prune empty dirs.
|
|
373
|
+
*/
|
|
374
|
+
function syncMarkdownTree(src, dest) {
|
|
375
|
+
mkdirSync(dest, { recursive: true });
|
|
376
|
+
|
|
377
|
+
const remotePaths = collectMarkdownPaths(src, src);
|
|
378
|
+
const localPaths = collectMarkdownPaths(dest, dest);
|
|
379
|
+
|
|
380
|
+
// Copy new and updated files
|
|
381
|
+
for (const rel of remotePaths) {
|
|
382
|
+
const srcPath = join(src, rel);
|
|
383
|
+
const destPath = join(dest, rel);
|
|
384
|
+
mkdirSync(join(dest, rel, '..'), { recursive: true });
|
|
385
|
+
copyFileSync(srcPath, destPath);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Delete local files not on remote
|
|
389
|
+
for (const rel of localPaths) {
|
|
390
|
+
if (!remotePaths.has(rel)) {
|
|
391
|
+
const destPath = join(dest, rel);
|
|
392
|
+
try { unlinkSync(destPath); } catch { /* best effort */ }
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Prune empty directories (bottom-up)
|
|
397
|
+
function pruneEmpty(dir) {
|
|
398
|
+
if (!existsSync(dir)) return;
|
|
399
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
400
|
+
if (entry.isDirectory()) pruneEmpty(join(dir, entry.name));
|
|
367
401
|
}
|
|
402
|
+
try {
|
|
403
|
+
if (readdirSync(dir).length === 0 && dir !== dest) rmdirSync(dir);
|
|
404
|
+
} catch { /* best effort */ }
|
|
368
405
|
}
|
|
406
|
+
pruneEmpty(dest);
|
|
369
407
|
}
|
|
370
408
|
|
|
371
409
|
function registerMcp(namespace) {
|
|
@@ -418,6 +456,7 @@ function printSummary(actions, verbose, conflictCount) {
|
|
|
418
456
|
if (counts.ADD > 0) parts.push(chalk.green(`${counts.ADD} new`));
|
|
419
457
|
if (counts.UPDATE > 0) parts.push(chalk.cyan(`${counts.UPDATE} updated`));
|
|
420
458
|
if (counts.CONFLICT > 0) parts.push(chalk.red(`${counts.CONFLICT} conflict`));
|
|
459
|
+
if (counts.ORPHAN > 0) parts.push(chalk.yellow(`${counts.ORPHAN} removed`));
|
|
421
460
|
const detail = parts.length > 0 ? ` (${parts.join(', ')})` : '';
|
|
422
461
|
|
|
423
462
|
fmt.logSuccess(`${typeActions.length} ${type} pulled${detail}`);
|
package/commands/push.mjs
CHANGED
|
@@ -23,25 +23,39 @@ function collectBatchFiles(folderAbsPath, workspaceDir) {
|
|
|
23
23
|
const relPath = relative(workspaceDir, folderAbsPath);
|
|
24
24
|
const segments = relPath.split('/');
|
|
25
25
|
|
|
26
|
-
//
|
|
27
|
-
//
|
|
26
|
+
// Detect if user pointed at or inside a type dir (agents/, skills/, etc.).
|
|
27
|
+
// e.g. "revex/courses/core/evals" → typeFilter=evals, subPathFilter=null
|
|
28
|
+
// e.g. "revex/courses/core/evals/agents" → typeFilter=evals, subPathFilter="agents"
|
|
29
|
+
// e.g. "revex/courses/core/evals/agents/architect" → typeFilter=evals, subPathFilter="agents/architect"
|
|
28
30
|
let typeFilter = null;
|
|
31
|
+
let subPathFilter = null;
|
|
29
32
|
let walkDir = folderAbsPath;
|
|
30
33
|
let walkBaseName = relPath;
|
|
31
34
|
|
|
32
35
|
for (let i = 0; i < segments.length; i++) {
|
|
33
36
|
if (PUSHABLE_TYPES.includes(segments[i])) {
|
|
34
37
|
// Everything before this segment is the namespace
|
|
35
|
-
|
|
36
|
-
walkBaseName =
|
|
38
|
+
const namespaceParts = segments.slice(0, i);
|
|
39
|
+
walkBaseName = namespaceParts.join('/');
|
|
40
|
+
// Walk from the namespace dir (parent of the type dir)
|
|
41
|
+
walkDir = join(workspaceDir, ...namespaceParts);
|
|
37
42
|
typeFilter = segments[i];
|
|
43
|
+
// Anything after the type dir is a sub-path filter (e.g., "agents/architect")
|
|
44
|
+
if (i + 1 < segments.length) {
|
|
45
|
+
subPathFilter = segments.slice(i + 1).join('/');
|
|
46
|
+
}
|
|
38
47
|
break;
|
|
39
48
|
}
|
|
40
49
|
}
|
|
41
50
|
|
|
42
51
|
const entries = walkRegistryTree(walkDir, walkBaseName);
|
|
43
52
|
return entries
|
|
44
|
-
.filter(entry =>
|
|
53
|
+
.filter(entry => {
|
|
54
|
+
if (typeFilter && entry.type !== typeFilter) return false;
|
|
55
|
+
// For sub-path filtering (e.g., only evals under "agents" or "agents/architect")
|
|
56
|
+
if (subPathFilter && !entry.slug.startsWith(subPathFilter)) return false;
|
|
57
|
+
return true;
|
|
58
|
+
})
|
|
45
59
|
.map(entry => {
|
|
46
60
|
// Skills and evals both have nested slug subdirs (e.g. skills/slug/file, evals/agents/developer/file)
|
|
47
61
|
const registryTarget = (entry.type === 'skills' || entry.type === 'evals')
|
package/constants.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// constants.mjs — Single source of truth for registry settings.
|
|
2
2
|
|
|
3
3
|
/** Base branch for PRs and sync checkout */
|
|
4
|
-
export const REGISTRY_BASE_BRANCH = '
|
|
4
|
+
export const REGISTRY_BASE_BRANCH = 'main';
|
|
5
5
|
|
|
6
6
|
/** Default registry repository */
|
|
7
7
|
export const REGISTRY_REPO = 'GoHighLevel/platform-docs';
|
package/fmt.mjs
CHANGED
|
@@ -81,7 +81,7 @@ export function actionLabel(action) {
|
|
|
81
81
|
case 'UPDATE': return chalk.bgCyan.black(' UPD ');
|
|
82
82
|
case 'SKIP': return chalk.bgYellow.black(' SKP ');
|
|
83
83
|
case 'CONFLICT': return chalk.bgRed.white(' CON ');
|
|
84
|
-
case 'ORPHAN': return chalk.
|
|
84
|
+
case 'ORPHAN': return chalk.bgRed.white(' DEL ');
|
|
85
85
|
case 'UNCHANGED': return chalk.dim(' --- ');
|
|
86
86
|
default: return chalk.dim(` ${action} `);
|
|
87
87
|
}
|
|
@@ -94,6 +94,6 @@ export function countSummary(counts) {
|
|
|
94
94
|
if (counts.UNCHANGED > 0) parts.push(chalk.dim(`${counts.UNCHANGED} unchanged`));
|
|
95
95
|
if (counts.SKIP > 0) parts.push(chalk.yellow(`${counts.SKIP} skipped`));
|
|
96
96
|
if (counts.CONFLICT > 0) parts.push(chalk.red(`${counts.CONFLICT} conflict`));
|
|
97
|
-
if (counts.ORPHAN > 0) parts.push(chalk.
|
|
97
|
+
if (counts.ORPHAN > 0) parts.push(chalk.red(`${counts.ORPHAN} removed`));
|
|
98
98
|
return parts.join(chalk.dim(' · '));
|
|
99
99
|
}
|
package/link.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// link.mjs — Create symlinks from IDE dirs → .aw_registry/
|
|
2
2
|
|
|
3
|
-
import { existsSync, lstatSync, mkdirSync, readdirSync, unlinkSync, symlinkSync } from 'node:fs';
|
|
3
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, unlinkSync, symlinkSync, rmdirSync } from 'node:fs';
|
|
4
4
|
import { join, relative } from 'node:path';
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
6
|
import * as fmt from './fmt.mjs';
|
|
@@ -80,11 +80,14 @@ function cleanIdeSymlinks(cwd) {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
/**
|
|
83
|
-
* Remove all symlinks in a directory
|
|
84
|
-
*
|
|
83
|
+
* Remove all symlinks in a directory, then prune empty subdirectories.
|
|
84
|
+
* Walks depth-first so children are cleaned before parents.
|
|
85
85
|
*/
|
|
86
86
|
function cleanSymlinksRecursive(dir) {
|
|
87
|
-
|
|
87
|
+
let entries;
|
|
88
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
89
|
+
|
|
90
|
+
for (const entry of entries) {
|
|
88
91
|
const p = join(dir, entry.name);
|
|
89
92
|
try {
|
|
90
93
|
if (lstatSync(p).isSymbolicLink()) {
|
|
@@ -94,6 +97,13 @@ function cleanSymlinksRecursive(dir) {
|
|
|
94
97
|
}
|
|
95
98
|
} catch { /* best effort */ }
|
|
96
99
|
}
|
|
100
|
+
|
|
101
|
+
// Remove directory if now empty (but never remove top-level IDE dirs like .claude/)
|
|
102
|
+
try {
|
|
103
|
+
if (readdirSync(dir).length === 0 && !IDE_DIRS.some(d => dir.endsWith(d))) {
|
|
104
|
+
rmdirSync(dir);
|
|
105
|
+
}
|
|
106
|
+
} catch { /* best effort */ }
|
|
97
107
|
}
|
|
98
108
|
|
|
99
109
|
/**
|
package/package.json
CHANGED
package/plan.mjs
CHANGED
|
@@ -98,12 +98,19 @@ export function computePlan(registryDirs, workspaceDir, includePatterns = [], {
|
|
|
98
98
|
});
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
// Orphans: in manifest but not in plan
|
|
101
|
+
// Orphans: in manifest but not in plan.
|
|
102
|
+
// Scoped to namespaces actually walked — only entries whose namespace was
|
|
103
|
+
// part of this registry scan can be orphaned. This prevents a "platform" pull
|
|
104
|
+
// from orphaning "platform/ai" entries (different pull, different sparse checkout).
|
|
102
105
|
if (manifest.files && !skipOrphans) {
|
|
106
|
+
const planTargets = new Set(actions.map(a => a.targetFilename));
|
|
107
|
+
const walkedNamespaces = new Set(actions.map(a => a.namespacePath));
|
|
108
|
+
|
|
103
109
|
for (const [manifestKey, manifestEntry] of Object.entries(manifest.files)) {
|
|
104
|
-
|
|
110
|
+
if (planTargets.has(manifestKey)) continue;
|
|
111
|
+
|
|
112
|
+
// Extract namespace from manifest key
|
|
105
113
|
const parts = manifestKey.split('/');
|
|
106
|
-
// Find the type segment
|
|
107
114
|
let typeIdx = -1;
|
|
108
115
|
for (let i = 0; i < parts.length; i++) {
|
|
109
116
|
if (['agents', 'skills', 'commands', 'evals'].includes(parts[i])) {
|
|
@@ -113,24 +120,26 @@ export function computePlan(registryDirs, workspaceDir, includePatterns = [], {
|
|
|
113
120
|
}
|
|
114
121
|
if (typeIdx === -1) continue;
|
|
115
122
|
const namespace = parts.slice(0, typeIdx).join('/');
|
|
123
|
+
|
|
124
|
+
// Only orphan entries whose exact namespace was walked in this scan
|
|
125
|
+
if (!walkedNamespaces.has(namespace)) continue;
|
|
126
|
+
|
|
116
127
|
const type = parts[typeIdx];
|
|
117
128
|
const slug = parts[typeIdx + 1]?.replace(/\.md$/, '');
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
});
|
|
133
|
-
}
|
|
129
|
+
|
|
130
|
+
actions.push({
|
|
131
|
+
slug,
|
|
132
|
+
source: manifestEntry.source || 'unknown',
|
|
133
|
+
type,
|
|
134
|
+
action: 'ORPHAN',
|
|
135
|
+
sourcePath: '',
|
|
136
|
+
targetPath: join(workspaceDir, manifestKey),
|
|
137
|
+
targetFilename: manifestKey,
|
|
138
|
+
isDirectory: false,
|
|
139
|
+
registryHash: '',
|
|
140
|
+
namespacePath: namespace,
|
|
141
|
+
registryPath: '',
|
|
142
|
+
});
|
|
134
143
|
}
|
|
135
144
|
}
|
|
136
145
|
|