@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 +54 -15
- package/commands/pull.mjs +77 -1
- package/git.mjs +30 -2
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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
|
-
|
|
241
|
+
pullJobs.push(
|
|
242
|
+
pullAsync({ ...args, _positional: ['[template]'], _renameNamespace: namespace, _workspaceDir: GLOBAL_AW_DIR, _skipIntegrate: true }),
|
|
243
|
+
);
|
|
217
244
|
}
|
|
218
245
|
|
|
219
|
-
|
|
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
|
-
|
|
230
|
-
|
|
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
|
|
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
|
|
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
|
*/
|