@ghl-ai/aw 0.1.26-beta.2 → 0.1.26-beta.4

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 CHANGED
@@ -5,6 +5,7 @@ import { join, dirname } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import * as fmt from './fmt.mjs';
7
7
  import { chalk } from './fmt.mjs';
8
+ import { checkForUpdate, notifyUpdate } from './update.mjs';
8
9
 
9
10
  const __dirname = dirname(fileURLToPath(import.meta.url));
10
11
  const VERSION = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf8')).version;
@@ -127,6 +128,9 @@ function printHelp() {
127
128
  export async function run(argv) {
128
129
  const { command, args } = parseArgs(argv);
129
130
 
131
+ // Fire background npm version check (runs in parallel with the command)
132
+ const updateCheck = checkForUpdate();
133
+
130
134
  if (args['--version'] || (!command && args['-v'])) {
131
135
  console.log(`aw v${VERSION}`);
132
136
  process.exit(0);
@@ -138,8 +142,10 @@ export async function run(argv) {
138
142
  }
139
143
 
140
144
  if (command && COMMANDS[command]) {
145
+ args._updateCheck = updateCheck;
141
146
  const handler = await COMMANDS[command]();
142
147
  await handler(args);
148
+ notifyUpdate(await updateCheck);
143
149
  return;
144
150
  }
145
151
 
package/commands/init.mjs CHANGED
@@ -17,6 +17,7 @@ import { pullCommand, pullAsync } from './pull.mjs';
17
17
  import { linkWorkspace } from '../link.mjs';
18
18
  import { generateCommands, copyInstructions, initAwDocs } from '../integrate.mjs';
19
19
  import { setupMcp } from '../mcp.mjs';
20
+ import { autoUpdate, promptUpdate } from '../update.mjs';
20
21
 
21
22
  const __dirname = dirname(fileURLToPath(import.meta.url));
22
23
  const VERSION = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')).version;
@@ -251,7 +252,9 @@ export async function initCommand(args) {
251
252
  } catch { /* best effort */ }
252
253
  }
253
254
 
254
- if (!silent) {
255
+ if (silent) {
256
+ autoUpdate(await args._updateCheck);
257
+ } else {
255
258
  fmt.outro([
256
259
  isNewSubTeam ? `Sub-team ${chalk.cyan(folderName)} added` : 'Sync complete',
257
260
  '',
@@ -342,6 +345,9 @@ export async function initCommand(args) {
342
345
  };
343
346
  saveManifest(manifest);
344
347
 
348
+ // Offer to update if a newer version is available
349
+ await promptUpdate(await args._updateCheck);
350
+
345
351
  // Done
346
352
  fmt.outro([
347
353
  'Install complete',
package/commands/pull.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  // commands/pull.mjs — Pull content from registry
2
2
 
3
- import { mkdirSync, existsSync, readdirSync, copyFileSync, cpSync } from 'node:fs';
3
+ import { mkdirSync, existsSync, readdirSync, copyFileSync } from 'node:fs';
4
4
  import { join } from 'node:path';
5
5
  import { homedir } from 'node:os';
6
6
  import { execSync } from 'node:child_process';
@@ -8,7 +8,7 @@ import * as config from '../config.mjs';
8
8
  import * as fmt from '../fmt.mjs';
9
9
  import { chalk } from '../fmt.mjs';
10
10
  import { sparseCheckout, sparseCheckoutAsync, cleanup, includeToSparsePaths } from '../git.mjs';
11
- import { REGISTRY_DIR } from '../constants.mjs';
11
+ import { REGISTRY_DIR, DOCS_SOURCE_DIR } from '../constants.mjs';
12
12
  import { walkRegistryTree } from '../registry.mjs';
13
13
  import { matchesAny } from '../glob.mjs';
14
14
  import { computePlan } from '../plan.mjs';
@@ -191,14 +191,11 @@ export async function pullCommand(args) {
191
191
  }
192
192
  }
193
193
 
194
- // Bulk-copy docs/ directories (not a TYPE_DIR raw recursive copy)
195
- for (const rd of registryDirs) {
196
- const docsDir = join(rd.path, 'docs');
197
- if (existsSync(docsDir)) {
198
- const dest = join(workspaceDir, rd.name, 'docs');
199
- mkdirSync(dest, { recursive: true });
200
- cpSync(docsDir, dest, { recursive: true });
201
- }
194
+ // Sync docs from repo content/ into platform/docs/ (markdown only, skip images)
195
+ const contentSrc = join(tempDir, DOCS_SOURCE_DIR);
196
+ if (existsSync(contentSrc)) {
197
+ const docsDest = join(workspaceDir, 'platform', 'docs');
198
+ copyMarkdownTree(contentSrc, docsDest);
202
199
  }
203
200
 
204
201
  // MCP registration (second-class — skip if not available)
@@ -293,14 +290,11 @@ export async function pullAsync(args) {
293
290
  if (existsSync(src)) copyFileSync(src, join(workspaceDir, fname));
294
291
  }
295
292
 
296
- // Bulk-copy docs/ directories (not a TYPE_DIR raw recursive copy)
297
- for (const rd of registryDirs) {
298
- const docsDir = join(rd.path, 'docs');
299
- if (existsSync(docsDir)) {
300
- const dest = join(workspaceDir, rd.name, 'docs');
301
- mkdirSync(dest, { recursive: true });
302
- cpSync(docsDir, dest, { recursive: true });
303
- }
293
+ // Sync docs from repo content/ into platform/docs/ (markdown only, skip images)
294
+ const contentSrc = join(tempDir, DOCS_SOURCE_DIR);
295
+ if (existsSync(contentSrc)) {
296
+ const docsDest = join(workspaceDir, 'platform', 'docs');
297
+ copyMarkdownTree(contentSrc, docsDest);
304
298
  }
305
299
 
306
300
  return { pattern, actions, conflictCount };
@@ -315,6 +309,24 @@ function listDirs(dir) {
315
309
  .map(d => d.name);
316
310
  }
317
311
 
312
+ /**
313
+ * Recursively copy only .md files from src to dest, preserving directory structure.
314
+ * Skips images and other non-markdown content.
315
+ */
316
+ function copyMarkdownTree(src, dest) {
317
+ mkdirSync(dest, { recursive: true });
318
+ for (const entry of readdirSync(src, { withFileTypes: true })) {
319
+ if (entry.name.startsWith('.')) continue;
320
+ const srcPath = join(src, entry.name);
321
+ const destPath = join(dest, entry.name);
322
+ if (entry.isDirectory()) {
323
+ copyMarkdownTree(srcPath, destPath);
324
+ } else if (entry.name.endsWith('.md')) {
325
+ copyFileSync(srcPath, destPath);
326
+ }
327
+ }
328
+ }
329
+
318
330
  function registerMcp(namespace) {
319
331
  const mcpUrl = process.env.GHL_MCP_URL;
320
332
  if (!mcpUrl) return;
package/constants.mjs CHANGED
@@ -8,3 +8,6 @@ export const REGISTRY_REPO = 'GoHighLevel/platform-docs';
8
8
 
9
9
  /** Directory inside the registry repo that holds platform/ and [template]/ */
10
10
  export const REGISTRY_DIR = '.aw_registry';
11
+
12
+ /** Directory in platform-docs repo containing documentation (pulled into platform/docs/) */
13
+ export const DOCS_SOURCE_DIR = 'content';
package/git.mjs CHANGED
@@ -5,7 +5,7 @@ import { mkdtempSync, existsSync } from 'node:fs';
5
5
  import { join } from 'node:path';
6
6
  import { tmpdir } from 'node:os';
7
7
  import { promisify } from 'node:util';
8
- import { REGISTRY_BASE_BRANCH, REGISTRY_DIR } from './constants.mjs';
8
+ import { REGISTRY_BASE_BRANCH, REGISTRY_DIR, DOCS_SOURCE_DIR } from './constants.mjs';
9
9
 
10
10
  const exec = promisify(execCb);
11
11
 
@@ -77,6 +77,9 @@ export function cleanup(tempDir) {
77
77
  /**
78
78
  * Compute sparse checkout paths from include paths.
79
79
  * e.g., ["platform", "dev/agents/debugger"] -> [".aw_registry/platform", ".aw_registry/dev/agents/debugger"]
80
+ *
81
+ * When "platform" is in the paths, also includes the repo's docs source
82
+ * directory (content/) so docs are pulled on-the-fly into platform/docs/.
80
83
  */
81
84
  export function includeToSparsePaths(paths) {
82
85
  const result = new Set();
@@ -84,5 +87,8 @@ export function includeToSparsePaths(paths) {
84
87
  result.add(`${REGISTRY_DIR}/${p}`);
85
88
  }
86
89
  result.add(`${REGISTRY_DIR}/AW-PROTOCOL.md`);
90
+ if (paths.includes('platform')) {
91
+ result.add(DOCS_SOURCE_DIR);
92
+ }
87
93
  return [...result];
88
94
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.26-beta.2",
3
+ "version": "0.1.26-beta.4",
4
4
  "description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
5
5
  "type": "module",
6
6
  "bin": {
@@ -22,7 +22,8 @@
22
22
  "paths.mjs",
23
23
  "plan.mjs",
24
24
  "registry.mjs",
25
- "apply.mjs"
25
+ "apply.mjs",
26
+ "update.mjs"
26
27
  ],
27
28
  "engines": {
28
29
  "node": ">=18.0.0"
package/update.mjs ADDED
@@ -0,0 +1,126 @@
1
+ // update.mjs — Version check + auto-update for @ghl-ai/aw CLI.
2
+ //
3
+ // checkForUpdate() → fires npm view in background, returns { current, latest, hasUpdate }
4
+ // notifyUpdate(r) → prints banner if r says update available
5
+ // autoUpdate(r) → spawns npm install in background (--silent mode)
6
+ // promptUpdate(r) → asks user interactively (normal init)
7
+
8
+ import { readFileSync } from 'node:fs';
9
+ import { exec as execCb, execSync, spawn } from 'node:child_process';
10
+ import { promisify } from 'node:util';
11
+ import * as fmt from './fmt.mjs';
12
+ import { chalk } from './fmt.mjs';
13
+
14
+ const exec = promisify(execCb);
15
+
16
+ const PKG_NAME = '@ghl-ai/aw';
17
+
18
+ function getLocalVersion() {
19
+ try {
20
+ return JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8')).version;
21
+ } catch {
22
+ return '0.0.0';
23
+ }
24
+ }
25
+
26
+ function isPrerelease(version) {
27
+ return /-(alpha|beta|rc|dev|canary)/.test(version);
28
+ }
29
+
30
+ function compareVersions(a, b) {
31
+ const pa = a.replace(/^v/, '').split(/[-.]/).map(p => (isNaN(p) ? p : Number(p)));
32
+ const pb = b.replace(/^v/, '').split(/[-.]/).map(p => (isNaN(p) ? p : Number(p)));
33
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
34
+ const va = pa[i] ?? 0;
35
+ const vb = pb[i] ?? 0;
36
+ if (typeof va === 'string' || typeof vb === 'string') {
37
+ const sa = String(va), sb = String(vb);
38
+ if (sa < sb) return -1;
39
+ if (sa > sb) return 1;
40
+ } else {
41
+ if (va < vb) return -1;
42
+ if (va > vb) return 1;
43
+ }
44
+ }
45
+ return 0;
46
+ }
47
+
48
+ /**
49
+ * Fetch the latest version from npm (non-blocking, 5s timeout).
50
+ * Call at the start of any command, await at the end.
51
+ */
52
+ export async function checkForUpdate() {
53
+ try {
54
+ const { stdout } = await exec(`npm view ${PKG_NAME} version`, { timeout: 5000 });
55
+ const latest = stdout.trim();
56
+ const current = getLocalVersion();
57
+ return { current, latest, hasUpdate: compareVersions(latest, current) > 0 };
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Print a banner nudge if a newer version is available.
65
+ * Shows for all users (including beta).
66
+ */
67
+ export function notifyUpdate(result) {
68
+ if (!result?.hasUpdate) return;
69
+ const msg = [
70
+ '',
71
+ ` ${chalk.bgHex('#FF6B35').bold(' UPDATE ')} New version available: ${chalk.dim(result.current)} → ${chalk.green(result.latest)}`,
72
+ ` ${chalk.dim('Run')} ${chalk.bold(`npm i -g ${PKG_NAME}`)} ${chalk.dim('to update')}`,
73
+ '',
74
+ ].join('\n');
75
+ console.log(msg);
76
+ }
77
+
78
+ /**
79
+ * Auto-update in background (fire-and-forget, detached process).
80
+ * Used by `aw init --silent`. Skipped for prerelease versions.
81
+ */
82
+ export function autoUpdate(result) {
83
+ if (!result?.hasUpdate) return;
84
+ if (isPrerelease(result.current)) return;
85
+ try {
86
+ const child = spawn('npm', ['i', '-g', `${PKG_NAME}@latest`], {
87
+ detached: true,
88
+ stdio: 'ignore',
89
+ });
90
+ child.unref();
91
+ } catch { /* best effort */ }
92
+ }
93
+
94
+ /**
95
+ * Interactive prompt to update during normal init.
96
+ * Skipped for prerelease versions.
97
+ */
98
+ export async function promptUpdate(result) {
99
+ if (!result?.hasUpdate) return false;
100
+ if (isPrerelease(result.current)) return false;
101
+
102
+ fmt.logWarn(`Update available: ${chalk.dim(result.current)} → ${chalk.green(result.latest)}`);
103
+
104
+ const { default: clack } = await import('@clack/prompts');
105
+ const shouldUpdate = await clack.confirm({
106
+ message: `Update ${PKG_NAME} to ${result.latest}?`,
107
+ initialValue: true,
108
+ });
109
+
110
+ if (clack.isCancel(shouldUpdate) || !shouldUpdate) {
111
+ fmt.logStep(chalk.dim('Skipped update'));
112
+ return false;
113
+ }
114
+
115
+ const s = fmt.spinner();
116
+ s.start(`Updating to ${result.latest}...`);
117
+ try {
118
+ execSync(`npm i -g ${PKG_NAME}@latest`, { stdio: 'pipe', timeout: 30000 });
119
+ s.stop(`Updated to ${chalk.green(result.latest)}`);
120
+ return true;
121
+ } catch {
122
+ s.stop(chalk.red('Update failed'));
123
+ fmt.logWarn(`Manual update: ${chalk.bold(`npm i -g ${PKG_NAME}@latest`)}`);
124
+ return false;
125
+ }
126
+ }