@ghl-ai/aw 0.1.36-beta.26 → 0.1.36-beta.28

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/commands/init.mjs CHANGED
@@ -29,7 +29,7 @@ import {
29
29
  sparseCheckoutAsync,
30
30
  cleanup,
31
31
  } from '../git.mjs';
32
- import { REGISTRY_DIR, REGISTRY_REPO } from '../constants.mjs';
32
+ import { REGISTRY_DIR, REGISTRY_REPO, REGISTRY_URL } from '../constants.mjs';
33
33
 
34
34
  const __dirname = dirname(fileURLToPath(import.meta.url));
35
35
  const VERSION = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')).version;
@@ -124,7 +124,7 @@ export async function initCommand(args) {
124
124
 
125
125
  // ── Detect installation state ─────────────────────────────────────────
126
126
 
127
- const repoUrl = `https://github.com/${REGISTRY_REPO}.git`;
127
+ const repoUrl = REGISTRY_URL;
128
128
  const isGitNative = isValidClone(AW_HOME, repoUrl);
129
129
  const isLegacy = !isGitNative && existsSync(GLOBAL_AW_DIR) && !lstatSync(GLOBAL_AW_DIR).isSymbolicLink();
130
130
 
@@ -309,8 +309,8 @@ export async function initCommand(args) {
309
309
  await installAwEcc(cwd, { silent });
310
310
  const instructionFiles = copyInstructions(HOME, null, team) || [];
311
311
  initAwDocs(HOME);
312
- const mcpFiles = await setupMcp(HOME, team) || [];
313
- if (cwd !== HOME) await setupMcp(cwd, team);
312
+ const mcpFiles = await setupMcp(HOME, team, { silent }) || [];
313
+ if (cwd !== HOME) await setupMcp(cwd, team, { silent });
314
314
  const hooksInstalled = installGlobalHooks();
315
315
  installIdeTasks();
316
316
 
@@ -6,7 +6,7 @@ import { homedir } from 'node:os';
6
6
  import * as fmt from '../fmt.mjs';
7
7
  import { chalk } from '../fmt.mjs';
8
8
  import { addProjectWorktree, isWorktree, isValidClone } from '../git.mjs';
9
- import { REGISTRY_DIR, REGISTRY_REPO } from '../constants.mjs';
9
+ import { REGISTRY_DIR, REGISTRY_URL } from '../constants.mjs';
10
10
  import { linkWorkspace } from '../link.mjs';
11
11
  import { generateCommands } from '../integrate.mjs';
12
12
 
@@ -18,8 +18,7 @@ export function linkProjectCommand(args) {
18
18
 
19
19
  fmt.intro('aw link');
20
20
 
21
- const repoUrl = `https://github.com/${REGISTRY_REPO}.git`;
22
- if (!isValidClone(AW_HOME, repoUrl)) {
21
+ if (!isValidClone(AW_HOME, REGISTRY_URL)) {
23
22
  fmt.cancel('Registry not initialized. Run: aw init');
24
23
  return;
25
24
  }
@@ -39,9 +38,9 @@ export function linkProjectCommand(args) {
39
38
  // Worktree exists — refresh global IDE symlinks pointing to this project's registry
40
39
  const projectRegistryDir = join(worktreeDir, REGISTRY_DIR);
41
40
  const awDirForLinks = existsSync(projectRegistryDir) ? projectRegistryDir : null;
42
- linkWorkspace(HOME, awDirForLinks);
43
- generateCommands(HOME);
44
- fmt.logSuccess(`Already linked — refreshed IDE symlinks`);
41
+ const symlinks = linkWorkspace(HOME, awDirForLinks, { silent: true });
42
+ const commands = generateCommands(HOME, { silent: true });
43
+ fmt.logSuccess(`Already linked — refreshed ${chalk.bold(symlinks)} IDE symlinks · ${chalk.bold(commands)} commands`);
45
44
  return;
46
45
  }
47
46
 
@@ -49,10 +48,10 @@ export function linkProjectCommand(args) {
49
48
  addProjectWorktree(AW_HOME, cwd);
50
49
  const projectRegistryDir = join(worktreeDir, REGISTRY_DIR);
51
50
  const awDirForLinks = existsSync(projectRegistryDir) ? projectRegistryDir : null;
52
- linkWorkspace(HOME, awDirForLinks);
53
- generateCommands(HOME);
51
+ const symlinks = linkWorkspace(HOME, awDirForLinks, { silent: true });
52
+ const commands = generateCommands(HOME, { silent: true });
54
53
  fmt.logSuccess([
55
- `Linked project as git worktree`,
54
+ `Project linked ${chalk.bold(symlinks)} IDE symlinks · ${chalk.bold(commands)} commands`,
56
55
  '',
57
56
  ` ${chalk.green('✓')} ${chalk.dim('.aw/')} git worktree (IDE git panel enabled)`,
58
57
  ` ${chalk.green('✓')} ${chalk.dim(`.aw/${REGISTRY_DIR}/`)} registry content`,
@@ -62,5 +61,5 @@ export function linkProjectCommand(args) {
62
61
  fmt.cancel(`Failed to link project: ${e.message}`);
63
62
  }
64
63
 
65
- fmt.outro('Done');
64
+ fmt.outro('Done');
66
65
  }
package/commands/pull.mjs CHANGED
@@ -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 { fetchAndMerge, addToSparseCheckout, isValidClone, isWorktree, rebaseOntoOriginMain } from '../git.mjs';
11
- import { REGISTRY_DIR, REGISTRY_REPO, DOCS_SOURCE_DIR } from '../constants.mjs';
11
+ import { REGISTRY_DIR, REGISTRY_REPO, REGISTRY_URL, DOCS_SOURCE_DIR } from '../constants.mjs';
12
12
  import { linkWorkspace } from '../link.mjs';
13
13
  import { generateCommands, copyInstructions } from '../integrate.mjs';
14
14
 
@@ -33,7 +33,9 @@ export async function pullCommand(args) {
33
33
  spinner: silent ? () => ({ start: () => {}, stop: () => {} }) : fmt.spinner,
34
34
  };
35
35
 
36
- const repoUrl = `https://github.com/${REGISTRY_REPO}.git`;
36
+ if (!silent) fmt.intro('aw pull');
37
+
38
+ const repoUrl = REGISTRY_URL;
37
39
  const hasClone = isValidClone(AW_HOME, repoUrl);
38
40
 
39
41
  if (!hasClone) {
@@ -128,14 +130,14 @@ export async function pullCommand(args) {
128
130
  // so edits to project/.aw/.aw_registry/ are instantly visible in global IDE dirs.
129
131
  const projectRegistryDir = cwd !== HOME ? join(cwd, '.aw', REGISTRY_DIR) : null;
130
132
  const awDirForLinks = (projectRegistryDir && existsSync(projectRegistryDir)) ? projectRegistryDir : null;
131
- linkWorkspace(HOME, awDirForLinks);
132
- generateCommands(HOME);
133
+ linkWorkspace(HOME, awDirForLinks, { silent });
134
+ generateCommands(HOME, { silent });
133
135
  copyInstructions(HOME, null, cfg.namespace);
134
136
  }
135
137
 
136
138
  if (!silent) {
137
139
  registerMcp(cfg.namespace);
138
- log.outro('Pull complete');
140
+ log.outro('Pull complete');
139
141
  }
140
142
  }
141
143
 
package/commands/push.mjs CHANGED
@@ -6,7 +6,7 @@ import { execSync, execFileSync } from 'node:child_process';
6
6
  import { homedir } from 'node:os';
7
7
  import * as fmt from '../fmt.mjs';
8
8
  import { chalk } from '../fmt.mjs';
9
- import { REGISTRY_REPO, REGISTRY_BASE_BRANCH, REGISTRY_DIR } from '../constants.mjs';
9
+ import { REGISTRY_REPO, REGISTRY_URL, REGISTRY_BASE_BRANCH, REGISTRY_DIR } from '../constants.mjs';
10
10
  import { resolveInput } from '../paths.mjs';
11
11
  import { walkRegistryTree } from '../registry.mjs';
12
12
  import {
@@ -179,8 +179,8 @@ function generatePrBody(files, newNamespaces, awHome = null) {
179
179
  function generateCommitMsg(files) {
180
180
  const added = files.filter(f => !f.deleted);
181
181
  const deleted = files.filter(f => f.deleted);
182
- const addedParts = Object.entries(groupBy(added, 'type')).map(([t, items]) => `${items.length} ${t}`);
183
- const deletedParts = Object.entries(groupBy(deleted, 'type')).map(([t, items]) => `${items.length} ${t} removed`);
182
+ const addedParts = Object.entries(groupBy(added, 'type')).map(([t, items]) => `${items.length} ${singular(t, items.length)}`);
183
+ const deletedParts = Object.entries(groupBy(deleted, 'type')).map(([t, items]) => `${items.length} ${singular(t, items.length)} removed`);
184
184
  const countParts = [...addedParts, ...deletedParts];
185
185
 
186
186
  if (files.length === 1) {
@@ -284,7 +284,7 @@ function createOrUpdatePR(awHome, branch, prTitle, prBody) {
284
284
  try {
285
285
  const url = execFileSync('gh', [
286
286
  'pr', 'view', branch, '--json', 'url', '--jq', '.url',
287
- ], { cwd: awHome, encoding: 'utf8' }).trim();
287
+ ], { cwd: awHome, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
288
288
  if (url) return { url, updated: true };
289
289
  } catch { /* no existing PR */ }
290
290
 
@@ -316,8 +316,8 @@ function doPush(files, awHome, dryRun, worktreeFlow = false, preStaged = false)
316
316
  const added = files.filter(f => !f.deleted);
317
317
  const deleted = files.filter(f => f.deleted);
318
318
 
319
- const addedParts = Object.entries(groupBy(added, 'type')).map(([t, items]) => `${items.length} ${t}`);
320
- const deletedParts = Object.entries(groupBy(deleted, 'type')).map(([t, items]) => `${items.length} ${t} removed`);
319
+ const addedParts = Object.entries(groupBy(added, 'type')).map(([t, items]) => `${items.length} ${singular(t, items.length)}`);
320
+ const deletedParts = Object.entries(groupBy(deleted, 'type')).map(([t, items]) => `${items.length} ${singular(t, items.length)} removed`);
321
321
  const countParts = [...addedParts, ...deletedParts];
322
322
 
323
323
  if (files.length === 0) {
@@ -401,7 +401,7 @@ function doPush(files, awHome, dryRun, worktreeFlow = false, preStaged = false)
401
401
  fmt.logInfo(chalk.dim(`On branch ${finalBranch} — run aw push again to open a new PR`));
402
402
  }
403
403
  fmt.logSuccess(`PR: ${chalk.cyan(prUrl)}`);
404
- fmt.outro('Push complete');
404
+ fmt.outro('Push complete');
405
405
  }
406
406
 
407
407
  // ── Main command ─────────────────────────────────────────────────────
@@ -426,7 +426,7 @@ export function pushCommand(args) {
426
426
 
427
427
  fmt.intro('aw push');
428
428
 
429
- const repoUrl = `https://github.com/${REGISTRY_REPO}.git`;
429
+ const repoUrl = REGISTRY_URL;
430
430
  if (!hasWorktree && !isValidClone(awHome, repoUrl)) {
431
431
  fmt.cancel('Registry not initialized. Run: aw init');
432
432
  return;
@@ -475,7 +475,6 @@ export function pushCommand(args) {
475
475
  }
476
476
 
477
477
  const files = allEntries
478
- .filter(f => parseRegistryPath(f.registryPath) !== null)
479
478
  .map(f => {
480
479
  const meta = parseRegistryPath(f.registryPath);
481
480
  const parts = f.registryPath.split('/');
@@ -5,8 +5,8 @@ import { homedir } from 'node:os';
5
5
  import * as config from '../config.mjs';
6
6
  import * as fmt from '../fmt.mjs';
7
7
  import { chalk } from '../fmt.mjs';
8
- import { detectChanges, getCurrentBranch, isValidClone, isWorktree } from '../git.mjs';
9
- import { REGISTRY_DIR, REGISTRY_REPO } from '../constants.mjs';
8
+ import { detectChanges, getCurrentBranch, commitsAheadOfMain, isValidClone, isWorktree } from '../git.mjs';
9
+ import { REGISTRY_DIR, REGISTRY_REPO, REGISTRY_URL } from '../constants.mjs';
10
10
 
11
11
  export function statusCommand(args) {
12
12
  const HOME = homedir();
@@ -20,8 +20,7 @@ export function statusCommand(args) {
20
20
 
21
21
  fmt.intro('aw status');
22
22
 
23
- const repoUrl = `https://github.com/${REGISTRY_REPO}.git`;
24
- if (!isWorktree(localAw) && !isValidClone(AW_HOME, repoUrl)) {
23
+ if (!isWorktree(localAw) && !isValidClone(AW_HOME, REGISTRY_URL)) {
25
24
  fmt.cancel('Registry not initialized. Run: aw init');
26
25
  return;
27
26
  }
@@ -93,7 +92,9 @@ export function statusCommand(args) {
93
92
  }
94
93
 
95
94
  if (!isOnMain) {
96
- fmt.logWarn(`On branch ${chalk.yellow(branch)} — run ${chalk.dim('aw pull')} to sync or checkout main first`);
95
+ const ahead = commitsAheadOfMain(AW_HOME);
96
+ const aheadStr = ahead > 0 ? ` (${chalk.yellow(`${ahead} commit${ahead !== 1 ? 's' : ''} ahead`)})` : '';
97
+ fmt.logWarn(`On branch ${chalk.yellow(branch)}${aheadStr} — run ${chalk.dim('aw push')} to open a PR or ${chalk.dim('aw pull')} to sync`);
97
98
  }
98
99
 
99
100
  // Hints
@@ -101,5 +102,5 @@ export function statusCommand(args) {
101
102
  fmt.logInfo(`Push changes: ${chalk.dim('aw push')} or ${chalk.dim('aw push <path>')}`);
102
103
  }
103
104
 
104
- fmt.outro(`${chalk.dim('aw pull')} to fetch latest`);
105
+ fmt.outro(`⟁ ${chalk.dim('aw pull')} to fetch latest`);
105
106
  }
package/constants.mjs CHANGED
@@ -9,6 +9,13 @@ export const REGISTRY_BASE_BRANCH = 'main';
9
9
  /** Default registry repository */
10
10
  export const REGISTRY_REPO = 'GoHighLevel/platform-docs';
11
11
 
12
+ /**
13
+ * Full registry clone URL.
14
+ * Tests override via AW_REGISTRY_URL env var (file:// bare repo).
15
+ */
16
+ export const REGISTRY_URL = process.env.AW_REGISTRY_URL
17
+ || `https://github.com/${REGISTRY_REPO}.git`;
18
+
12
19
  /** Directory inside the registry repo that holds platform/ and [template]/ */
13
20
  export const REGISTRY_DIR = '.aw_registry';
14
21
 
package/ecc.mjs CHANGED
@@ -61,6 +61,7 @@ export async function installAwEcc(
61
61
  cwd,
62
62
  { targets = ["cursor", "claude", "codex"], silent = false } = {},
63
63
  ) {
64
+ if (process.env.AW_NO_ECC === '1') return;
64
65
  if (!silent) fmt.logStep("Installing aw-ecc engine...");
65
66
 
66
67
  const repoDir = eccDir();
package/fmt.mjs CHANGED
@@ -33,7 +33,7 @@ export function banner(text, opts = {}) {
33
33
 
34
34
  // ─── Clack wrappers ───
35
35
 
36
- export const intro = (msg) => p.intro(chalk.bgCyan.black(` ${msg} `));
36
+ export const intro = (msg) => p.intro(chalk.bgHex('#FF6B35').black(` ${msg} `));
37
37
  export const outro = (msg) => p.outro(chalk.green(msg));
38
38
  export const spinner = () => p.spinner();
39
39
  export const select = p.select;
package/hooks.mjs CHANGED
@@ -111,6 +111,10 @@ exit 0
111
111
  * @returns {boolean} true if hooks were installed
112
112
  */
113
113
  export function installGlobalHooks() {
114
+ // AW_NO_HOOKS=1 lets test environments skip hook installation to prevent
115
+ // recursive aw init calls during git commits in tests.
116
+ if (process.env.AW_NO_HOOKS === '1') return false;
117
+
114
118
  try {
115
119
  mkdirSync(HOOKS_DIR, { recursive: true });
116
120
 
package/integrate.mjs CHANGED
@@ -11,7 +11,7 @@ import { getLocalRegistryDir } from './git.mjs';
11
11
  * Count hand-written commands already present in the registry.
12
12
  * No CLI stub generation — all commands come from the registry itself.
13
13
  */
14
- export function generateCommands(cwd) {
14
+ export function generateCommands(cwd, { silent = false } = {}) {
15
15
  const awDir = getLocalRegistryDir(cwd, join(homedir(), '.aw_registry'));
16
16
 
17
17
  // Clean old .generated-commands if it exists (migration)
@@ -29,7 +29,7 @@ export function generateCommands(cwd) {
29
29
  }
30
30
  }
31
31
 
32
- if (count > 0) {
32
+ if (count > 0 && !silent) {
33
33
  fmt.logSuccess(`Generated ${count} aw commands`);
34
34
  }
35
35
 
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, rmdirSync } from 'node:fs';
3
+ import { existsSync, lstatSync, mkdirSync, readdirSync, unlinkSync, symlinkSync, rmdirSync, realpathSync } 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';
@@ -129,9 +129,14 @@ function flatName(ns, name) {
129
129
  * @param {string} cwd - Directory where IDE dirs (.claude/.cursor/.codex) live
130
130
  * @param {string|null} awDirOverride - Explicit registry dir; overrides auto-detection
131
131
  */
132
- export function linkWorkspace(cwd, awDirOverride = null) {
132
+ export function linkWorkspace(cwd, awDirOverride = null, { silent = false } = {}) {
133
+ // Normalize cwd to real path so relative() produces valid symlink targets on macOS
134
+ // where $HOME may be /var/... but process.cwd() resolves to /private/var/...
135
+ try { cwd = realpathSync(cwd); } catch { /* use as-is */ }
136
+
133
137
  const GLOBAL_AW_DIR = join(homedir(), '.aw_registry');
134
- const awDir = awDirOverride || getLocalRegistryDir(cwd, GLOBAL_AW_DIR);
138
+ let awDir = awDirOverride || getLocalRegistryDir(cwd, GLOBAL_AW_DIR);
139
+ try { awDir = realpathSync(awDir); } catch { /* use as-is if it doesn't exist */ }
135
140
  if (!existsSync(awDir)) return 0;
136
141
 
137
142
  let created = 0;
@@ -255,7 +260,7 @@ export function linkWorkspace(cwd, awDirOverride = null) {
255
260
  }
256
261
  }
257
262
 
258
- if (created > 0) {
263
+ if (created > 0 && !silent) {
259
264
  fmt.logSuccess(`Linked ${created} IDE symlink${created > 1 ? 's' : ''}`);
260
265
  }
261
266
 
package/package.json CHANGED
@@ -1,11 +1,9 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.36-beta.26",
3
+ "version": "0.1.36-beta.28",
4
4
  "description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
5
5
  "type": "module",
6
- "bin": {
7
- "aw": "bin.js"
8
- },
6
+ "bin": "bin.js",
9
7
  "files": [
10
8
  "bin.js",
11
9
  "cli.mjs",
@@ -40,6 +38,8 @@
40
38
  "author": "GoHighLevel",
41
39
  "license": "MIT",
42
40
  "scripts": {
41
+ "test": "vitest run --reporter=verbose",
42
+ "test:watch": "vitest --reporter=verbose",
43
43
  "preuninstall": "node bin.js nuke 2>/dev/null || true"
44
44
  },
45
45
  "publishConfig": {
@@ -49,5 +49,8 @@
49
49
  "@clack/prompts": "0.8.2",
50
50
  "chalk": "^5.6.2",
51
51
  "figlet": "^1.11.0"
52
+ },
53
+ "devDependencies": {
54
+ "vitest": "^4.1.2"
52
55
  }
53
56
  }