@ghl-ai/aw 0.1.34-beta.9 → 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 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: true,
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
- copyMarkdownTree(contentSrc, docsDest);
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: true });
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
- copyMarkdownTree(contentSrc, docsDest);
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
- * Recursively copy only .md files from src to dest, preserving directory structure.
355
- * Skips images and other non-markdown content.
354
+ * Collect all .md file paths (relative) in a directory tree.
356
355
  */
357
- function copyMarkdownTree(src, dest) {
358
- mkdirSync(dest, { recursive: true });
359
- for (const entry of readdirSync(src, { withFileTypes: true })) {
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 srcPath = join(src, entry.name);
362
- const destPath = join(dest, entry.name);
361
+ const full = join(dir, entry.name);
363
362
  if (entry.isDirectory()) {
364
- copyMarkdownTree(srcPath, destPath);
363
+ for (const p of collectMarkdownPaths(full, base)) paths.add(p);
365
364
  } else if (entry.name.endsWith('.md')) {
366
- copyFileSync(srcPath, destPath);
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/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 = 'feat/aw-push-batch';
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.bgYellow.black(' ORP ');
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.yellow(`${counts.ORPHAN} orphan`));
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 (non-recursive into subdirs,
84
- * but does walk immediate child directories one level deep).
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
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.34-beta.9",
3
+ "version": "0.1.34",
4
4
  "description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
5
5
  "type": "module",
6
6
  "bin": {
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
- // manifestKey is now namespace/type/slug.md or namespace/type/slug/relPath
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
- const key = `${namespace}/${type}/${slug}`;
119
- if (!plan.has(key)) {
120
- actions.push({
121
- slug,
122
- source: manifestEntry.source || 'unknown',
123
- type,
124
- action: 'ORPHAN',
125
- sourcePath: '',
126
- targetPath: join(workspaceDir, manifestKey),
127
- targetFilename: manifestKey,
128
- isDirectory: false,
129
- registryHash: '',
130
- namespacePath: parts.slice(0, typeIdx).join('/'),
131
- registryPath: '',
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