@ghl-ai/aw 0.1.25-beta.2 → 0.1.25-beta.3

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
@@ -13,7 +13,7 @@ import { readFileSync } from 'node:fs';
13
13
  import * as config from '../config.mjs';
14
14
  import * as fmt from '../fmt.mjs';
15
15
  import { chalk } from '../fmt.mjs';
16
- import { pullCommand } from './pull.mjs';
16
+ 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';
@@ -112,6 +112,24 @@ function saveManifest(data) {
112
112
  }
113
113
 
114
114
 
115
+ function printPullSummary(pattern, actions) {
116
+ for (const type of ['agents', 'skills', 'commands', 'evals']) {
117
+ const typeActions = actions.filter(a => a.type === type);
118
+ if (typeActions.length === 0) continue;
119
+
120
+ const counts = { ADD: 0, UPDATE: 0, CONFLICT: 0 };
121
+ for (const a of typeActions) counts[a.action] = (counts[a.action] || 0) + 1;
122
+
123
+ const parts = [];
124
+ if (counts.ADD > 0) parts.push(chalk.green(`${counts.ADD} new`));
125
+ if (counts.UPDATE > 0) parts.push(chalk.cyan(`${counts.UPDATE} updated`));
126
+ if (counts.CONFLICT > 0) parts.push(chalk.red(`${counts.CONFLICT} conflict`));
127
+ const detail = parts.length > 0 ? ` (${parts.join(', ')})` : '';
128
+
129
+ fmt.logSuccess(`${chalk.cyan(pattern)}: ${typeActions.length} ${type}${detail}`);
130
+ }
131
+ }
132
+
115
133
  const ALLOWED_NAMESPACES = ['revex', 'mobile', 'commerce', 'leadgen', 'crm', 'marketplace', 'ai'];
116
134
 
117
135
  export async function initCommand(args) {
@@ -150,7 +168,9 @@ export async function initCommand(args) {
150
168
  fmt.cancel(`Invalid namespace '${namespace}' — must match: ^[a-z][a-z0-9-]{1,38}[a-z0-9]$`);
151
169
  }
152
170
 
153
- const isExisting = config.exists(GLOBAL_AW_DIR);
171
+ const hasConfig = config.exists(GLOBAL_AW_DIR);
172
+ const hasPlatform = existsSync(join(GLOBAL_AW_DIR, 'platform'));
173
+ const isExisting = hasConfig && hasPlatform;
154
174
  const cwd = process.cwd();
155
175
 
156
176
  // ── Fast path: already initialized → just pull + link ─────────────────
@@ -209,27 +229,46 @@ export async function initCommand(args) {
209
229
  `${chalk.dim('version:')} v${VERSION}`,
210
230
  ].filter(Boolean).join('\n'), 'Config created');
211
231
 
212
- // Step 2: Pull registry content
213
- pullCommand({ ...args, _positional: ['platform'], _workspaceDir: GLOBAL_AW_DIR });
232
+ // Step 2: Pull registry content (parallel)
233
+ const s = fmt.spinner();
234
+ const pullTargets = namespace ? `platform + ${namespace}` : 'platform';
235
+ s.start(`Pulling ${pullTargets}...`);
214
236
 
237
+ const pullJobs = [
238
+ pullAsync({ ...args, _positional: ['platform'], _workspaceDir: GLOBAL_AW_DIR, _skipIntegrate: true }),
239
+ ];
215
240
  if (namespace) {
216
- pullCommand({ ...args, _positional: ['[template]'], _renameNamespace: namespace, _workspaceDir: GLOBAL_AW_DIR });
241
+ pullJobs.push(
242
+ pullAsync({ ...args, _positional: ['[template]'], _renameNamespace: namespace, _workspaceDir: GLOBAL_AW_DIR, _skipIntegrate: true }),
243
+ );
217
244
  }
218
245
 
219
- // Step 3: Link to global IDE dirs
246
+ let pullResults;
247
+ try {
248
+ pullResults = await Promise.all(pullJobs);
249
+ s.stop(`Pulled ${pullTargets}`);
250
+ } catch (e) {
251
+ s.stop(chalk.red('Pull failed'));
252
+ fmt.cancel(e.message);
253
+ }
254
+
255
+ for (const { pattern, actions } of pullResults) {
256
+ printPullSummary(pattern, actions);
257
+ }
258
+
259
+ // Step 3: Link IDE dirs + parallel setup tasks
220
260
  linkWorkspace(HOME);
221
261
  generateCommands(HOME);
222
262
  const instructionFiles = copyInstructions(HOME, null, namespace) || [];
223
- initAwDocs(HOME);
224
- const mcpFiles = setupMcp(HOME, namespace) || [];
225
-
226
- // Step 4: Git template hook (omnipresence)
227
- const gitTemplateInstalled = installGitTemplate();
228
263
 
229
- // Step 5: IDE auto-init tasks
230
- installIdeTasks();
264
+ const [, , mcpFiles, gitTemplateInstalled] = await Promise.all([
265
+ Promise.resolve(initAwDocs(HOME)),
266
+ Promise.resolve(installIdeTasks()),
267
+ Promise.resolve(setupMcp(HOME, namespace) || []),
268
+ Promise.resolve(installGitTemplate()),
269
+ ]);
231
270
 
232
- // Step 6: Symlink in current directory if it's a git repo
271
+ // Step 4: Symlink in current directory if it's a git repo
233
272
  if (cwd !== HOME && !existsSync(join(cwd, '.aw_registry'))) {
234
273
  try {
235
274
  symlinkSync(GLOBAL_AW_DIR, join(cwd, '.aw_registry'));
@@ -237,7 +276,7 @@ export async function initCommand(args) {
237
276
  } catch { /* best effort */ }
238
277
  }
239
278
 
240
- // Step 7: Write manifest for nuke cleanup
279
+ // Step 5: Write manifest for nuke cleanup
241
280
  const manifest = {
242
281
  version: 1,
243
282
  installedAt: new Date().toISOString(),
package/commands/pull.mjs CHANGED
@@ -7,7 +7,7 @@ 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 { sparseCheckout, cleanup, includeToSparsePaths } from '../git.mjs';
10
+ import { sparseCheckout, sparseCheckoutAsync, cleanup, includeToSparsePaths } from '../git.mjs';
11
11
  import { walkRegistryTree } from '../registry.mjs';
12
12
  import { matchesAny } from '../glob.mjs';
13
13
  import { computePlan } from '../plan.mjs';
@@ -191,6 +191,82 @@ export function pullCommand(args) {
191
191
  }
192
192
  }
193
193
 
194
+ /**
195
+ * Async pull for parallel use by init. Runs git fetch asynchronously,
196
+ * then applies changes synchronously. Returns a summary object instead
197
+ * of printing directly — the caller controls output.
198
+ */
199
+ export async function pullAsync(args) {
200
+ const input = args._positional?.[0] || '';
201
+ const workspaceDir = args._workspaceDir;
202
+ const renameNamespace = args._renameNamespace || null;
203
+
204
+ const resolved = resolveInput(input, workspaceDir);
205
+ let pattern = resolved.registryPath;
206
+ if (!pattern) throw new Error(`Could not resolve "${input}" to a registry path`);
207
+
208
+ if (!existsSync(workspaceDir)) mkdirSync(workspaceDir, { recursive: true });
209
+
210
+ const cfg = config.load(workspaceDir);
211
+ if (!cfg) throw new Error('No .sync-config.json found');
212
+
213
+ const sparsePaths = includeToSparsePaths([pattern]);
214
+ const tempDir = await sparseCheckoutAsync(cfg.repo, sparsePaths);
215
+
216
+ try {
217
+ const registryDirs = [];
218
+ const regBase = join(tempDir, 'registry');
219
+
220
+ if (existsSync(regBase)) {
221
+ for (const name of listDirs(regBase)) {
222
+ registryDirs.push({ name, path: join(regBase, name) });
223
+ }
224
+ }
225
+
226
+ if (registryDirs.length === 0) {
227
+ throw new Error(`Nothing found in registry for ${pattern}`);
228
+ }
229
+
230
+ if (renameNamespace) {
231
+ for (const rd of registryDirs) {
232
+ if (rd.name === pattern) rd.name = renameNamespace;
233
+ }
234
+ pattern = renameNamespace;
235
+ }
236
+
237
+ let hasMatch = false;
238
+ for (const { name, path } of registryDirs) {
239
+ const entries = walkRegistryTree(path, name);
240
+ if (entries.some(e => matchesAny(e.registryPath, [pattern]))) {
241
+ hasMatch = true;
242
+ break;
243
+ }
244
+ }
245
+
246
+ if (!hasMatch) {
247
+ throw new Error(`Nothing found in registry for ${pattern}`);
248
+ }
249
+
250
+ if (!cfg.include.includes(pattern)) {
251
+ config.addPattern(workspaceDir, pattern);
252
+ }
253
+
254
+ const { actions } = computePlan(registryDirs, workspaceDir, [pattern], { skipOrphans: true });
255
+ const conflictCount = applyActions(actions, { teamNS: renameNamespace || undefined });
256
+ updateManifest(workspaceDir, actions, cfg.namespace);
257
+
258
+ const ROOT_REGISTRY_FILES = ['AW-PROTOCOL.md'];
259
+ for (const fname of ROOT_REGISTRY_FILES) {
260
+ const src = join(regBase, fname);
261
+ if (existsSync(src)) copyFileSync(src, join(workspaceDir, fname));
262
+ }
263
+
264
+ return { pattern, actions, conflictCount };
265
+ } finally {
266
+ cleanup(tempDir);
267
+ }
268
+ }
269
+
194
270
  function listDirs(dir) {
195
271
  return readdirSync(dir, { withFileTypes: true })
196
272
  .filter(d => d.isDirectory() && !d.name.startsWith('.'))
package/git.mjs CHANGED
@@ -1,13 +1,16 @@
1
1
  // git.mjs — Git sparse checkout helpers. Zero dependencies.
2
2
 
3
- import { execSync } from 'node:child_process';
3
+ import { execSync, exec as execCb } from 'node:child_process';
4
4
  import { mkdtempSync, existsSync } from 'node:fs';
5
5
  import { join } from 'node:path';
6
6
  import { tmpdir } from 'node:os';
7
+ import { promisify } from 'node:util';
7
8
  import { REGISTRY_BASE_BRANCH } from './constants.mjs';
8
9
 
10
+ const exec = promisify(execCb);
11
+
9
12
  /**
10
- * Sparse-checkout registry paths from GitHub.
13
+ * Sparse-checkout registry paths from GitHub (sync).
11
14
  * Returns the temp directory path containing the checkout.
12
15
  */
13
16
  export function sparseCheckout(repo, paths) {
@@ -35,6 +38,31 @@ export function sparseCheckout(repo, paths) {
35
38
  return tempDir;
36
39
  }
37
40
 
41
+ /**
42
+ * Sparse-checkout registry paths from GitHub (async).
43
+ * Same as sparseCheckout but non-blocking — can run in parallel.
44
+ */
45
+ export async function sparseCheckoutAsync(repo, paths) {
46
+ const tempDir = mkdtempSync(join(tmpdir(), 'aw-'));
47
+
48
+ const repoUrl = repo.startsWith('http') ? repo : `https://github.com/${repo}.git`;
49
+ try {
50
+ await exec(`git clone --filter=blob:none --no-checkout "${repoUrl}" "${tempDir}"`);
51
+ } catch (e) {
52
+ throw new Error(`Failed to clone ${repo}. Check your git credentials and repo access.`);
53
+ }
54
+
55
+ try {
56
+ await exec('git sparse-checkout init --cone', { cwd: tempDir });
57
+ await exec(`git sparse-checkout set --skip-checks ${paths.map(p => `"${p}"`).join(' ')}`, { cwd: tempDir });
58
+ await exec(`git checkout ${REGISTRY_BASE_BRANCH}`, { cwd: tempDir });
59
+ } catch (e) {
60
+ throw new Error(`Failed sparse checkout: ${e.message}`);
61
+ }
62
+
63
+ return tempDir;
64
+ }
65
+
38
66
  /**
39
67
  * Clean up temp directory.
40
68
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.25-beta.2",
3
+ "version": "0.1.25-beta.3",
4
4
  "description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
5
5
  "type": "module",
6
6
  "bin": {