@ghl-ai/aw 0.1.50 → 0.1.51

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
@@ -50,6 +50,9 @@ function parseArgs(argv) {
50
50
  if (arg === '--dry-run') {
51
51
  args['--dry-run'] = true;
52
52
  i++;
53
+ } else if (arg === '--aw-docs-only' || arg === '--docs-only') {
54
+ args[arg] = true;
55
+ i++;
53
56
  } else if (arg === '-v' || arg === '--verbose') {
54
57
  args['-v'] = true;
55
58
  i++;
@@ -97,6 +100,8 @@ function printHelp() {
97
100
 
98
101
  sec('Upload'),
99
102
  cmd('aw push', 'Push all modified files (creates one PR)'),
103
+ cmd('aw push --aw-docs-only', 'Publish generated .aw_docs companions and print share links'),
104
+ cmd('aw push --aw-docs-only --feature <slug>', 'Publish one .aw_docs feature folder and print share links'),
100
105
  cmd('aw push <path>', 'Push file, folder, or namespace to registry'),
101
106
  cmd('aw push-rules [path]', 'Push platform rules to platform-docs'),
102
107
  cmd('aw push --dry-run [path]', 'Preview what would be pushed'),
package/commands/push.mjs CHANGED
@@ -1,17 +1,46 @@
1
1
  // commands/push.mjs — Push local agents/skills to registry via PR using persistent git clone
2
2
 
3
- import { existsSync, statSync, readFileSync, appendFileSync } from 'node:fs';
4
- import { join, dirname } from 'node:path';
3
+ import {
4
+ existsSync,
5
+ lstatSync,
6
+ statSync,
7
+ readFileSync,
8
+ appendFileSync,
9
+ mkdirSync,
10
+ readdirSync,
11
+ copyFileSync,
12
+ rmSync,
13
+ writeFileSync,
14
+ } from 'node:fs';
15
+ import { join, dirname, basename } from 'node:path';
5
16
  import { fileURLToPath } from 'node:url';
6
17
  import { exec as execCb, execFile as execFileCb } from 'node:child_process';
7
18
  import { promisify } from 'node:util';
8
19
  import { homedir } from 'node:os';
20
+ import { createHash } from 'node:crypto';
9
21
 
10
22
  const exec = promisify(execCb);
11
23
  const execFile = promisify(execFileCb);
12
24
  import * as fmt from '../fmt.mjs';
13
25
  import { chalk } from '../fmt.mjs';
14
- import { REGISTRY_REPO, REGISTRY_URL, REGISTRY_BASE_BRANCH, REGISTRY_DIR, AW_CO_AUTHOR } from '../constants.mjs';
26
+ import {
27
+ REGISTRY_REPO,
28
+ REGISTRY_URL,
29
+ REGISTRY_BASE_BRANCH,
30
+ REGISTRY_DIR,
31
+ DOCS_SOURCE_DIR,
32
+ AW_DOCS_DIR,
33
+ AW_DOCS_REPO,
34
+ AW_DOCS_URL,
35
+ AW_DOCS_BASE_BRANCH,
36
+ AW_DOCS_SEED_BRANCH,
37
+ AW_DOCS_PUBLISH_DIR,
38
+ AW_DOCS_PUBLIC_BASE_URL,
39
+ AW_DOCS_TEAMOFONE_ORIGIN,
40
+ AW_DOCS_TEAMOFONE_BASE_URL,
41
+ AW_CO_AUTHOR,
42
+ defaultAwDocsGithubDocsConfig,
43
+ } from '../constants.mjs';
15
44
  import { resolveInput } from '../paths.mjs';
16
45
  import { walkRegistryTree, getAllFiles } from '../registry.mjs';
17
46
  import {
@@ -21,6 +50,7 @@ import {
21
50
  checkoutMain,
22
51
  isValidClone,
23
52
  getLocalRegistryDir,
53
+ findNearestWorktree,
24
54
  commitsAheadOfMain,
25
55
  logAheadOfMain,
26
56
  } from '../git.mjs';
@@ -30,13 +60,612 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
30
60
  const VERSION = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')).version;
31
61
 
32
62
  const PUSHABLE_TYPES = ['agents', 'skills', 'commands', 'evals', 'references'];
63
+ const EXTRA_PUSH_PATHS = [DOCS_SOURCE_DIR, 'CODEOWNERS'];
64
+ const EXTRA_PUSH_PATHS_ARG = EXTRA_PUSH_PATHS.map(p => `"${p}"`).join(' ');
65
+ const PROJECT_AW_DOCS_AUTO_ROOTS = ['features', 'html'];
66
+
67
+ function isAwDocsPath(p) {
68
+ return p === AW_DOCS_DIR || p.startsWith(`${AW_DOCS_DIR}/`);
69
+ }
70
+
71
+ function isContentPath(p) {
72
+ return p === DOCS_SOURCE_DIR || p.startsWith(`${DOCS_SOURCE_DIR}/`);
73
+ }
74
+
75
+ function managedPathSummary(paths) {
76
+ const unique = [...new Set(paths)];
77
+ const counts = {
78
+ awDocs: unique.filter(isAwDocsPath).length,
79
+ content: unique.filter(isContentPath).length,
80
+ codeowners: unique.filter(p => p === 'CODEOWNERS').length,
81
+ };
82
+ const known = counts.awDocs + counts.content + counts.codeowners;
83
+ const other = unique.length - known;
84
+ const parts = [];
85
+ if (counts.awDocs) parts.push(`${counts.awDocs} AW doc${counts.awDocs > 1 ? 's' : ''}`);
86
+ if (counts.content) parts.push(`${counts.content} platform doc${counts.content > 1 ? 's' : ''}`);
87
+ if (counts.codeowners) parts.push('CODEOWNERS');
88
+ if (other) parts.push(`${other} managed file${other > 1 ? 's' : ''}`);
89
+ return parts.join(', ') || 'managed files';
90
+ }
91
+
92
+ function managedBranchPrefix(paths) {
93
+ const unique = [...new Set(paths)];
94
+ if (unique.length > 0 && unique.every(isAwDocsPath)) return 'sync/aw-docs';
95
+ if (unique.length > 0 && unique.every(isContentPath)) return 'sync/platform-docs';
96
+ return 'sync/managed-docs';
97
+ }
98
+
99
+ function managedPrTitle(paths) {
100
+ const unique = [...new Set(paths)];
101
+ if (unique.length > 0 && unique.every(isAwDocsPath)) return 'docs(aw): sync AW docs';
102
+ if (unique.length > 0 && unique.every(isContentPath)) return 'docs(platform): sync platform docs';
103
+ return 'docs(aw): sync managed docs';
104
+ }
105
+
106
+ function formatManagedPathList(paths) {
107
+ const unique = [...new Set(paths)];
108
+ const lines = unique.slice(0, 40).map(p => `- \`${p}\``);
109
+ if (unique.length > 40) lines.push(`- ...and ${unique.length - 40} more`);
110
+ return lines;
111
+ }
112
+
113
+ function parseExtraStatusPaths(stdout) {
114
+ const paths = [];
115
+ for (const line of stdout.trim().split('\n').filter(Boolean)) {
116
+ const status = line.slice(0, 2);
117
+ const path = line.slice(2).trimStart();
118
+ if (!path) continue;
119
+ if (status === '!!' && !isAwDocsPath(path)) continue;
120
+ paths.push(path);
121
+ }
122
+ return [...new Set(paths)];
123
+ }
124
+
125
+ function getProjectRoot(cwd, home) {
126
+ if (existsSync(join(cwd, AW_DOCS_DIR))) return cwd;
127
+ const worktree = findNearestWorktree(cwd, home);
128
+ return worktree ? dirname(worktree) : cwd;
129
+ }
130
+
131
+ function collectFiles(root, base = root) {
132
+ if (!existsSync(root)) return [];
133
+
134
+ const files = [];
135
+ for (const entry of readdirSync(root, { withFileTypes: true })) {
136
+ if (entry.name.startsWith('.')) continue;
137
+ const fullPath = join(root, entry.name);
138
+ if (entry.isDirectory()) {
139
+ files.push(...collectFiles(fullPath, base));
140
+ } else if (entry.isFile()) {
141
+ files.push(fullPath.slice(base.length + 1));
142
+ }
143
+ }
144
+ return files;
145
+ }
146
+
147
+ function normalizeRelPath(value) {
148
+ return String(value || '')
149
+ .trim()
150
+ .replace(/\\/g, '/')
151
+ .replace(/^\.\//, '')
152
+ .replace(/\/+$/, '');
153
+ }
154
+
155
+ function featureScopeFromInput(input) {
156
+ const value = normalizeRelPath(input);
157
+ if (!value) return null;
158
+
159
+ const match = value.match(/^(?:\.aw_docs\/)?features\/([^/]+)$/);
160
+ if (!match) {
161
+ throw new Error('Docs-only publish path must be .aw_docs/features/<feature-slug> or use --feature <feature-slug>.');
162
+ }
163
+ return awDocsFeatureScope(match[1]);
164
+ }
165
+
166
+ function awDocsFeatureScope(featureSlug) {
167
+ const slug = String(featureSlug || '').trim();
168
+ if (!slug || slug === 'true') {
169
+ throw new Error('Missing feature slug. Use: aw push --aw-docs-only --feature <feature-slug>');
170
+ }
171
+ if (!/^[A-Za-z0-9._-]+$/.test(slug)) {
172
+ throw new Error(`Invalid feature slug "${slug}". Feature slugs may contain letters, numbers, dot, underscore, and dash only.`);
173
+ }
174
+ return {
175
+ type: 'feature',
176
+ slug,
177
+ relPrefix: `features/${slug}`,
178
+ };
179
+ }
180
+
181
+ function resolveAwDocsScope(input, featureFlag) {
182
+ const inputScope = featureScopeFromInput(input);
183
+ const flagScope = featureFlag ? awDocsFeatureScope(featureFlag) : null;
184
+ if (inputScope && flagScope && inputScope.relPrefix !== flagScope.relPrefix) {
185
+ throw new Error(`Docs-only publish received conflicting scopes: ${inputScope.relPrefix} and ${flagScope.relPrefix}.`);
186
+ }
187
+ return flagScope || inputScope;
188
+ }
189
+
190
+ function collectProjectAwDocs(cwd, home, scope = null) {
191
+ const projectRoot = getProjectRoot(cwd, home);
192
+ const source = join(projectRoot, AW_DOCS_DIR);
193
+
194
+ if (!existsSync(source)) return { projectRoot, files: [] };
195
+
196
+ const files = [];
197
+ if (scope) {
198
+ const sourceRoot = join(source, scope.relPrefix);
199
+ if (!existsSync(sourceRoot) || !statSync(sourceRoot).isDirectory()) {
200
+ throw new Error(`No publishable AW docs found under ${AW_DOCS_DIR}/${scope.relPrefix}.`);
201
+ }
202
+ for (const relFromRoot of collectFiles(sourceRoot)) {
203
+ const relPath = `${scope.relPrefix}/${relFromRoot}`.replace(/\\/g, '/');
204
+ files.push({
205
+ relPath,
206
+ absPath: join(source, relPath),
207
+ });
208
+ }
209
+ return { projectRoot, files };
210
+ }
211
+
212
+ for (const root of PROJECT_AW_DOCS_AUTO_ROOTS) {
213
+ const sourceRoot = join(source, root);
214
+ if (!existsSync(sourceRoot)) continue;
215
+ for (const relFromRoot of collectFiles(sourceRoot)) {
216
+ const relPath = `${root}/${relFromRoot}`.replace(/\\/g, '/');
217
+ files.push({
218
+ relPath,
219
+ absPath: join(source, relPath),
220
+ });
221
+ }
222
+ }
223
+ return { projectRoot, files };
224
+ }
225
+
226
+ function readAwDocsConfig(projectRoot) {
227
+ const configPath = join(projectRoot, AW_DOCS_DIR, 'config.json');
228
+ if (!existsSync(configPath)) return {};
229
+
230
+ try {
231
+ return JSON.parse(readFileSync(configPath, 'utf8'));
232
+ } catch (e) {
233
+ throw new Error(`Invalid ${AW_DOCS_DIR}/config.json: ${e.message}`);
234
+ }
235
+ }
236
+
237
+ function writeAwDocsConfigIfChanged(projectRoot, config) {
238
+ const configPath = join(projectRoot, AW_DOCS_DIR, 'config.json');
239
+ const nextText = JSON.stringify(config, null, 2) + '\n';
240
+ const existingText = existsSync(configPath) ? readFileSync(configPath, 'utf8') : '';
241
+ if (existingText !== nextText) {
242
+ mkdirSync(dirname(configPath), { recursive: true });
243
+ writeFileSync(configPath, nextText);
244
+ }
245
+ }
246
+
247
+ function ensureAwDocsPublishConfig(projectRoot) {
248
+ const config = readAwDocsConfig(projectRoot);
249
+ const defaultGithubDocs = defaultAwDocsGithubDocsConfig();
250
+ const next = {
251
+ ...config,
252
+ sync: {
253
+ ...(config.sync || {}),
254
+ github_docs: {
255
+ ...defaultGithubDocs,
256
+ ...(config.sync?.github_docs || {}),
257
+ },
258
+ },
259
+ };
260
+
261
+ writeAwDocsConfigIfChanged(projectRoot, next);
262
+ return next;
263
+ }
264
+
265
+ function repoCloneUrl(repo) {
266
+ const value = String(repo || '').trim();
267
+ if (!value) return AW_DOCS_URL;
268
+ if (/^(?:https?:\/\/|git@|ssh:\/\/|file:\/\/)/.test(value)) return value;
269
+ return `https://github.com/${value.replace(/\.git$/, '')}.git`;
270
+ }
271
+
272
+ function resolveAwDocsPublishConfig(projectRoot) {
273
+ const config = ensureAwDocsPublishConfig(projectRoot);
274
+ const githubDocs = config.sync?.github_docs || {};
275
+ const repo = process.env.AW_DOCS_REPO || githubDocs.repo || AW_DOCS_REPO;
276
+ const branch = AW_DOCS_BASE_BRANCH;
277
+ const repoUrl = process.env.AW_DOCS_REPO_URL || githubDocs.repo_url || repoCloneUrl(repo);
278
+ const dest = safePathSegment(process.env.AW_DOCS_PUBLISH_DIR || githubDocs.dest || AW_DOCS_PUBLISH_DIR, AW_DOCS_PUBLISH_DIR);
279
+ const teamofoneBaseUrl = String(
280
+ process.env.AW_DOCS_TEAMOFONE_BASE_URL
281
+ || githubDocs.teamofone_base_url
282
+ || AW_DOCS_TEAMOFONE_BASE_URL,
283
+ ).trim();
284
+ const publicBaseUrl = process.env.AW_DOCS_PUBLIC_BASE_URL
285
+ || githubDocs.public_base_url
286
+ || AW_DOCS_PUBLIC_BASE_URL
287
+ || `https://github.com/${String(repo).replace(/\.git$/, '')}/blob/${branch}`;
288
+
289
+ return {
290
+ enabled: githubDocs.enabled !== false,
291
+ repo,
292
+ repoUrl,
293
+ branch,
294
+ seedBranch: process.env.AW_DOCS_SEED_BRANCH || githubDocs.seed_branch || AW_DOCS_SEED_BRANCH,
295
+ dest,
296
+ teamofoneBaseUrl: normalizeTeamOfOneBaseUrl(teamofoneBaseUrl),
297
+ publicBaseUrl,
298
+ };
299
+ }
300
+
301
+ function isOnlyUntrackedAwSymlink(status, cloneDir) {
302
+ const lines = status.trim().split('\n').filter(Boolean);
303
+ if (lines.length !== 1 || !/^\?\?\s+\.aw\/?$/.test(lines[0])) return false;
304
+ try {
305
+ return lstatSync(join(cloneDir, '.aw')).isSymbolicLink();
306
+ } catch {
307
+ return false;
308
+ }
309
+ }
310
+
311
+ async function removeTrackedAwSymlink(cloneDir) {
312
+ try {
313
+ if (!lstatSync(join(cloneDir, '.aw')).isSymbolicLink()) return false;
314
+ } catch {
315
+ return false;
316
+ }
317
+
318
+ try {
319
+ await execFile('git', ['ls-files', '--error-unmatch', '.aw'], {
320
+ cwd: cloneDir,
321
+ encoding: 'utf8',
322
+ });
323
+ } catch {
324
+ return false;
325
+ }
326
+
327
+ await execFile('git', ['rm', '-f', '.aw'], {
328
+ cwd: cloneDir,
329
+ encoding: 'utf8',
330
+ });
331
+ return true;
332
+ }
333
+
334
+ async function getGitStatus(repoDir) {
335
+ const { stdout } = await execFile('git', ['status', '--porcelain'], {
336
+ cwd: repoDir,
337
+ encoding: 'utf8',
338
+ });
339
+ return stdout;
340
+ }
341
+
342
+ function parseGitHubRepo(remoteUrl) {
343
+ const match = remoteUrl.trim().match(/github\.com[:/]([^/]+)\/(.+?)(?:\.git)?$/);
344
+ if (!match) return null;
345
+ return `${match[1]}/${match[2].replace(/\.git$/, '')}`;
346
+ }
347
+
348
+ function repoSlugFromSource(sourceRepo, projectRoot) {
349
+ const repoName = sourceRepo?.split('/').pop() || basename(projectRoot);
350
+ return repoName
351
+ .replace(/\.git$/, '')
352
+ .toLowerCase()
353
+ .replace(/[^a-z0-9-]+/g, '-')
354
+ .replace(/^-+|-+$/g, '') || 'project';
355
+ }
356
+
357
+ function safePathSegment(value, fallback) {
358
+ return String(value || fallback)
359
+ .trim()
360
+ .replace(/[^A-Za-z0-9._-]+/g, '-')
361
+ .replace(/^-+|-+$/g, '') || fallback;
362
+ }
363
+
364
+ async function getProjectSourceRepo(projectRoot) {
365
+ if (process.env.AW_DOCS_SOURCE_REPO) return process.env.AW_DOCS_SOURCE_REPO;
366
+
367
+ try {
368
+ const { stdout } = await execFile('git', ['config', '--get', 'remote.origin.url'], {
369
+ cwd: projectRoot,
370
+ encoding: 'utf8',
371
+ });
372
+ const parsed = parseGitHubRepo(stdout);
373
+ if (parsed) return parsed;
374
+ } catch (e) {
375
+ if (process.env.AW_DEBUG) fmt.logWarn(`Could not resolve project git remote for AW docs: ${e.message}`);
376
+ }
377
+
378
+ return `local/${basename(projectRoot)}`;
379
+ }
380
+
381
+ async function ensureAwDocsRepoClone(home, publishConfig) {
382
+ const cloneDir = process.env.AW_DOCS_WORKTREE || join(home, '.aw-ghl-aw-docs');
383
+
384
+ if (!existsSync(join(cloneDir, '.git'))) {
385
+ await execFile('git', ['clone', publishConfig.repoUrl, cloneDir], {
386
+ encoding: 'utf8',
387
+ });
388
+ }
389
+
390
+ let status = await getGitStatus(cloneDir);
391
+ if (status.trim() && isOnlyUntrackedAwSymlink(status, cloneDir)) {
392
+ rmSync(join(cloneDir, '.aw'), { force: true });
393
+ status = await getGitStatus(cloneDir);
394
+ }
395
+ if (status.trim()) {
396
+ throw new Error([
397
+ `AW docs repo worktree is dirty: ${cloneDir}`,
398
+ status.trim(),
399
+ 'Clean the cached docs repo or set AW_DOCS_WORKTREE to a clean clone before publishing.',
400
+ ].join('\n'));
401
+ }
402
+
403
+ await execFile('git', ['fetch', 'origin'], {
404
+ cwd: cloneDir,
405
+ encoding: 'utf8',
406
+ });
407
+
408
+ const hasPublishBranch = await remoteBranchExists(cloneDir, publishConfig.branch);
409
+ const checkoutStart = hasPublishBranch
410
+ ? `origin/${publishConfig.branch}`
411
+ : `origin/${publishConfig.seedBranch}`;
412
+
413
+ await execFile('git', ['checkout', '-B', publishConfig.branch, checkoutStart], {
414
+ cwd: cloneDir,
415
+ encoding: 'utf8',
416
+ });
417
+
418
+ if (hasPublishBranch) {
419
+ await execFile('git', ['pull', '--ff-only', 'origin', publishConfig.branch], {
420
+ cwd: cloneDir,
421
+ encoding: 'utf8',
422
+ });
423
+ }
424
+ await removeTrackedAwSymlink(cloneDir);
425
+ return cloneDir;
426
+ }
427
+
428
+ async function remoteBranchExists(repoDir, branchName) {
429
+ try {
430
+ await execFile('git', ['rev-parse', '--verify', `origin/${branchName}`], {
431
+ cwd: repoDir,
432
+ encoding: 'utf8',
433
+ });
434
+ return true;
435
+ } catch {
436
+ return false;
437
+ }
438
+ }
439
+
440
+ function appendPathToUrl(baseUrl, path) {
441
+ const encodedPath = path.split('/').map(encodeURIComponent).join('/');
442
+ const hashIndex = baseUrl.indexOf('#');
443
+ const withoutHash = hashIndex === -1 ? baseUrl : baseUrl.slice(0, hashIndex);
444
+ const hash = hashIndex === -1 ? '' : baseUrl.slice(hashIndex);
445
+ const queryIndex = withoutHash.indexOf('?');
446
+ const basePath = queryIndex === -1 ? withoutHash : withoutHash.slice(0, queryIndex);
447
+ const query = queryIndex === -1 ? '' : withoutHash.slice(queryIndex);
448
+ return `${basePath.replace(/\/$/, '')}/${encodedPath}${query}${hash}`;
449
+ }
450
+
451
+ function normalizeTeamOfOneBaseUrl(baseUrl) {
452
+ const value = String(baseUrl || '').trim();
453
+ if (!value) return '';
454
+ if (/^https?:\/\//i.test(value)) return value;
455
+ if (value.startsWith('/')) return `${AW_DOCS_TEAMOFONE_ORIGIN.replace(/\/$/, '')}${value}`;
456
+ return `https://${value.replace(/^\/+/, '')}`;
457
+ }
458
+
459
+ function appendQueryParam(url, key, value) {
460
+ const hashIndex = url.indexOf('#');
461
+ const withoutHash = hashIndex === -1 ? url : url.slice(0, hashIndex);
462
+ const hash = hashIndex === -1 ? '' : url.slice(hashIndex);
463
+ const separator = withoutHash.includes('?') ? '&' : '?';
464
+ return `${withoutHash}${separator}${encodeURIComponent(key)}=${encodeURIComponent(value)}${hash}`;
465
+ }
466
+
467
+ function awDocsTeamOfOneUrl(publishedPath, publishConfig) {
468
+ if (!publishConfig.teamofoneBaseUrl) return null;
469
+ return appendQueryParam(
470
+ appendPathToUrl(publishConfig.teamofoneBaseUrl, publishedPath),
471
+ 'ref',
472
+ publishConfig.branch,
473
+ );
474
+ }
475
+
476
+ function awDocsPublicUrl(publishedPath, publishConfig) {
477
+ return appendPathToUrl(publishConfig.publicBaseUrl, publishedPath);
478
+ }
479
+
480
+ function awDocsRemoteUrl(publishedPath, publishConfig) {
481
+ return awDocsTeamOfOneUrl(publishedPath, publishConfig)
482
+ || awDocsPublicUrl(publishedPath, publishConfig);
483
+ }
484
+
485
+ function awDocsRepositoryUrl(publishedPath, publishConfig) {
486
+ const repo = String(publishConfig.repo || AW_DOCS_REPO).replace(/\.git$/, '');
487
+ if (!repo.includes('/')) return awDocsPublicUrl(publishedPath, publishConfig);
488
+ return appendPathToUrl(`https://github.com/${repo}/blob/${publishConfig.branch}`, publishedPath);
489
+ }
490
+
491
+ function printAwDocsLinks(links, limit = 10) {
492
+ fmt.logInfo(chalk.bold('Remote Docs'));
493
+ for (const link of links.slice(0, limit)) {
494
+ fmt.logInfo(` ${chalk.dim(link.relPath)}`);
495
+ fmt.logInfo(` TeamOfOne: ${chalk.cyan(link.remoteUrl)}`);
496
+ if (link.repositoryUrl && link.repositoryUrl !== link.remoteUrl) {
497
+ fmt.logInfo(` GitHub: ${chalk.cyan(link.repositoryUrl)}`);
498
+ }
499
+ }
500
+ if (links.length > limit) {
501
+ fmt.logInfo(chalk.dim(`...and ${links.length - limit} more`));
502
+ }
503
+ }
504
+
505
+ function writeAwDocsLinkSummary(projectRoot, links, publishConfig) {
506
+ const summaryPath = join(projectRoot, AW_DOCS_DIR, 'last-publish.json');
507
+ const payload = {
508
+ generatedAt: new Date().toISOString(),
509
+ branch: publishConfig.branch,
510
+ repo: publishConfig.repo,
511
+ targetPath: publishConfig.dest,
512
+ links,
513
+ };
514
+ try {
515
+ writeFileSync(summaryPath, JSON.stringify(payload, null, 2) + '\n');
516
+ } catch (e) {
517
+ if (process.env.AW_DEBUG) fmt.logWarn(`Could not write AW docs link summary: ${e.message}`);
518
+ }
519
+ }
520
+
521
+ async function commitAndPushAwDocsRepo(docsRepoDir, { message, branch }) {
522
+ await execFile('git', ['add', '-A'], { cwd: docsRepoDir, encoding: 'utf8' });
523
+ await execFile('git', ['commit', '-m', message], {
524
+ cwd: docsRepoDir,
525
+ encoding: 'utf8',
526
+ });
527
+ await execFile('git', ['push', '-u', 'origin', `HEAD:${branch}`], {
528
+ cwd: docsRepoDir,
529
+ encoding: 'utf8',
530
+ });
531
+ }
532
+
533
+ function titleForAwDoc(relPath) {
534
+ const name = basename(relPath);
535
+ const base = name.replace(/\.(md|html|json)$/i, '');
536
+ const label = base.charAt(0).toUpperCase() + base.slice(1);
537
+ const ext = name.includes('.') ? name.split('.').pop().toUpperCase() : 'DOC';
538
+ return `${label} ${ext}`;
539
+ }
540
+
541
+ function updateAwDocsManifest(docsRepoDir, { repoSlug, sourceRepo, githubUsername, docs, publishConfig, scope = null }) {
542
+ const manifestPath = join(docsRepoDir, 'manifest.json');
543
+ const manifest = existsSync(manifestPath)
544
+ ? JSON.parse(readFileSync(manifestPath, 'utf8'))
545
+ : { teams: {}, awDocs: { repos: {} } };
546
+ const now = new Date().toISOString();
547
+
548
+ manifest.updatedAt = now;
549
+ manifest.awDocs ||= { repos: {} };
550
+ manifest.awDocs.repos ||= {};
551
+ const repoEntry = manifest.awDocs.repos[repoSlug] || {};
552
+ repoEntry.slug = repoSlug;
553
+ repoEntry.sourceRepo = sourceRepo;
554
+ repoEntry.users ||= {};
555
+ const existingUser = repoEntry.users[githubUsername] || {};
556
+ const nextDocs = docs.map(doc => ({
557
+ relPath: doc.relPath,
558
+ publishedPath: doc.publishedPath,
559
+ remoteUrl: awDocsRemoteUrl(doc.publishedPath, publishConfig),
560
+ repositoryUrl: awDocsRepositoryUrl(doc.publishedPath, publishConfig),
561
+ sha: createHash('sha256').update(readFileSync(join(docsRepoDir, doc.publishedPath))).digest('hex'),
562
+ syncedAt: now,
563
+ title: titleForAwDoc(doc.relPath),
564
+ }));
565
+ const preservedDocs = scope?.relPrefix
566
+ ? (existingUser.docs || []).filter(doc => !String(doc.relPath || '').startsWith(`${scope.relPrefix}/`))
567
+ : [];
568
+
569
+ repoEntry.users[githubUsername] = {
570
+ ...existingUser,
571
+ githubUsername,
572
+ docs: scope?.relPrefix ? [...preservedDocs, ...nextDocs] : nextDocs,
573
+ };
574
+ manifest.awDocs.repos[repoSlug] = repoEntry;
575
+
576
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
577
+ }
578
+
579
+ async function publishProjectAwDocs(cwd, home, dryRun, scope = null) {
580
+ const { projectRoot, files } = collectProjectAwDocs(cwd, home, scope);
581
+ if (files.length === 0) return { hasDocs: false, publishedPaths: [] };
582
+
583
+ const publishConfig = resolveAwDocsPublishConfig(projectRoot);
584
+ if (!publishConfig.enabled) {
585
+ fmt.logWarn(`${AW_DOCS_DIR}/config.json sync.github_docs.enabled is false; skipping AW docs publish`);
586
+ return { hasDocs: true, skipped: true, publishedPaths: [], links: [] };
587
+ }
588
+
589
+ const sourceRepo = await getProjectSourceRepo(projectRoot);
590
+ const repoSlug = repoSlugFromSource(sourceRepo, projectRoot);
591
+ const githubUsername = safePathSegment(await getGitHubUser(), 'unknown');
592
+ const docs = files.map(file => ({
593
+ ...file,
594
+ publishedPath: `${publishConfig.dest}/${repoSlug}/${githubUsername}/${file.relPath}`,
595
+ }));
596
+ const publishedPaths = docs.map(doc => doc.publishedPath);
597
+ const links = docs.map(doc => ({
598
+ relPath: doc.relPath,
599
+ publishedPath: doc.publishedPath,
600
+ remoteUrl: awDocsRemoteUrl(doc.publishedPath, publishConfig),
601
+ repositoryUrl: awDocsRepositoryUrl(doc.publishedPath, publishConfig),
602
+ }));
603
+
604
+ if (dryRun) {
605
+ fmt.logInfo(`${chalk.bold(files.length)} AW doc${files.length > 1 ? 's' : ''} to publish directly to ${publishConfig.repo}`);
606
+ for (const link of links.slice(0, 40)) {
607
+ fmt.logMessage(` ${chalk.yellow('AWDOC')}/${link.publishedPath}`);
608
+ fmt.logMessage(` ${chalk.cyan(link.remoteUrl)}`);
609
+ }
610
+ if (links.length > 40) fmt.logMessage(` ...and ${links.length - 40} more`);
611
+ fmt.logWarn('No AW docs published (--dry-run)');
612
+ return { hasDocs: true, publishedPaths, links };
613
+ }
614
+
615
+ const s = fmt.spinner();
616
+ s.start(`Publishing ${files.length} AW doc${files.length > 1 ? 's' : ''} to ${publishConfig.repo}...`);
617
+ try {
618
+ const docsRepoDir = await ensureAwDocsRepoClone(home, publishConfig);
619
+ const deleteTarget = scope?.relPrefix
620
+ ? join(docsRepoDir, publishConfig.dest, repoSlug, githubUsername, scope.relPrefix)
621
+ : join(docsRepoDir, publishConfig.dest, repoSlug, githubUsername);
622
+ rmSync(deleteTarget, {
623
+ recursive: true,
624
+ force: true,
625
+ });
626
+ for (const doc of docs) {
627
+ const dest = join(docsRepoDir, doc.publishedPath);
628
+ mkdirSync(dirname(dest), { recursive: true });
629
+ copyFileSync(doc.absPath, dest);
630
+ }
631
+ updateAwDocsManifest(docsRepoDir, { repoSlug, sourceRepo, githubUsername, docs, publishConfig, scope });
632
+
633
+ const { stdout: status } = await execFile('git', ['status', '--porcelain'], {
634
+ cwd: docsRepoDir,
635
+ encoding: 'utf8',
636
+ });
637
+ if (!status.trim()) {
638
+ s.stop('AW docs already up to date');
639
+ writeAwDocsLinkSummary(projectRoot, links, publishConfig);
640
+ printAwDocsLinks(links);
641
+ return { hasDocs: true, publishedPaths, links };
642
+ }
643
+
644
+ await commitAndPushAwDocsRepo(docsRepoDir, {
645
+ message: scope?.relPrefix
646
+ ? `docs(aw): sync ${repoSlug}/${githubUsername} ${scope.relPrefix}`
647
+ : `docs(aw): sync ${repoSlug}/${githubUsername} AW docs`,
648
+ branch: publishConfig.branch,
649
+ });
650
+ writeAwDocsLinkSummary(projectRoot, links, publishConfig);
651
+ s.stop(`Published AW docs to ${publishConfig.repo}`);
652
+ printAwDocsLinks(links);
653
+ return { hasDocs: true, publishedPaths, links };
654
+ } catch (e) {
655
+ s.stop(chalk.red('AW docs publish failed'));
656
+ throw e;
657
+ }
658
+ }
33
659
 
34
660
  // ── PR content generation ────────────────────────────────────────────
35
661
 
36
662
  // Auto-generate a branch name from the files being pushed.
37
- function generateBranchName(files) {
663
+ function generateBranchName(files, extraPaths = []) {
38
664
  const shortId = Date.now().toString(36).slice(-5);
39
665
 
666
+ if (files.length === 0 && extraPaths.length > 0) {
667
+ return `${managedBranchPrefix(extraPaths)}-${shortId}`;
668
+ }
40
669
  if (files.length === 0) return `sync/state-${shortId}`;
41
670
 
42
671
  const namespaces = [...new Set(files.map(f => f.namespace))];
@@ -75,8 +704,10 @@ function parseCommitFiles(commits) {
75
704
  return result;
76
705
  }
77
706
 
78
- function generatePrTitle(files, awHome = null) {
707
+ function generatePrTitle(files, awHome = null, extraPaths = []) {
79
708
  if (files.length === 0) {
709
+ if (extraPaths.length > 0) return managedPrTitle(extraPaths);
710
+
80
711
  const commits = awHome ? logAheadOfMain(awHome) : [];
81
712
  const parsed = parseCommitFiles(commits);
82
713
 
@@ -123,7 +754,7 @@ function generatePrTitle(files, awHome = null) {
123
754
 
124
755
  const AW_BRANDING = `---\n⟁ Generated by [AW CLI](https://platform.docs/agentic-workspace/guides/cli)`;
125
756
 
126
- function generatePrBody(files, newNamespaces, awHome = null) {
757
+ function generatePrBody(files, newNamespaces, awHome = null, extraPaths = []) {
127
758
  const added = files.filter(f => !f.deleted);
128
759
  const deleted = files.filter(f => f.deleted);
129
760
  const bodyParts = [];
@@ -131,7 +762,10 @@ function generatePrBody(files, newNamespaces, awHome = null) {
131
762
  if (files.length === 0) {
132
763
  const commits = awHome ? logAheadOfMain(awHome) : [];
133
764
  bodyParts.push('## What\'s included', '');
134
- if (commits.length > 0) {
765
+ if (extraPaths.length > 0) {
766
+ bodyParts.push(`Syncing ${managedPathSummary(extraPaths)}.`, '', '### Paths', '');
767
+ bodyParts.push(...formatManagedPathList(extraPaths));
768
+ } else if (commits.length > 0) {
135
769
  for (const c of commits) {
136
770
  bodyParts.push(`- \`${c.hash}\` ${c.message}`);
137
771
  }
@@ -180,11 +814,16 @@ function generatePrBody(files, newNamespaces, awHome = null) {
180
814
  bodyParts.push('', `> **New namespace${newNamespaces.length > 1 ? 's' : ''}:** \`${newNamespaces.join('`, `')}\` — CODEOWNERS entr${newNamespaces.length > 1 ? 'ies' : 'y'} auto-added.`);
181
815
  }
182
816
 
817
+ if (extraPaths.length > 0) {
818
+ bodyParts.push('', '### Managed Docs', '', `Also syncing ${managedPathSummary(extraPaths)}.`, '');
819
+ bodyParts.push(...formatManagedPathList(extraPaths));
820
+ }
821
+
183
822
  bodyParts.push('', AW_BRANDING);
184
823
  return bodyParts.join('\n');
185
824
  }
186
825
 
187
- function generateCommitMsg(files) {
826
+ function generateCommitMsg(files, extraPaths = []) {
188
827
  const added = files.filter(f => !f.deleted);
189
828
  const deleted = files.filter(f => f.deleted);
190
829
  const addedParts = Object.entries(groupBy(added, 'type')).map(([t, items]) => `${items.length} ${singular(t, items.length)}`);
@@ -194,10 +833,16 @@ function generateCommitMsg(files) {
194
833
  const version = VERSION;
195
834
  const trailer = `\n\nGenerated-By: aw/${version}\n${AW_CO_AUTHOR}`;
196
835
 
836
+ if (files.length === 0 && extraPaths.length > 0) {
837
+ return `docs: sync ${managedPathSummary(extraPaths)}${trailer}`;
838
+ }
197
839
  if (files.length === 1) {
198
840
  const f = files[0];
199
841
  return `registry: ${f.deleted ? 'remove' : 'add'} ${f.type}/${f.slug} ${f.deleted ? 'from' : 'to'} ${f.namespace}${trailer}`;
200
842
  }
843
+ if (extraPaths.length > 0) {
844
+ return `registry: sync ${files.length} files and ${managedPathSummary(extraPaths)}${trailer}`;
845
+ }
201
846
  return `registry: sync ${files.length} files (${countParts.join(', ')})${trailer}`;
202
847
  }
203
848
 
@@ -368,15 +1013,22 @@ async function doPush(files, awHome, dryRun, worktreeFlow = false, preStaged = f
368
1013
 
369
1014
  // ── Dry-run: just list files and exit ──────────────────────────────
370
1015
  if (dryRun) {
371
- if (files.length === 0) {
1016
+ if (files.length === 0 && extraPaths.length === 0) {
372
1017
  fmt.logInfo('No new changes — would branch current state');
373
1018
  } else {
374
- fmt.logInfo(`${chalk.bold(files.length)} file${files.length > 1 ? 's' : ''} to push (${countParts.join(', ')})`);
1019
+ const total = files.length + extraPaths.length;
1020
+ const registrySummary = countParts.length > 0 ? countParts.join(', ') : null;
1021
+ const extraSummary = extraPaths.length > 0 ? managedPathSummary(extraPaths) : null;
1022
+ const summary = [registrySummary, extraSummary].filter(Boolean).join(', ');
1023
+ fmt.logInfo(`${chalk.bold(total)} file${total > 1 ? 's' : ''} to push (${summary})`);
375
1024
  for (const f of files) {
376
1025
  const ns = chalk.dim(` [${f.namespace}]`);
377
1026
  const label = f.deleted ? chalk.red('DELETE') : chalk.yellow(f.type);
378
1027
  fmt.logMessage(` ${label}/${f.slug}${ns}`);
379
1028
  }
1029
+ for (const p of extraPaths) {
1030
+ fmt.logMessage(` ${chalk.yellow('DOC')}/${p}`);
1031
+ }
380
1032
  }
381
1033
  fmt.logWarn('No changes made (--dry-run)');
382
1034
  fmt.outro(chalk.dim('Remove --dry-run to push'));
@@ -385,7 +1037,7 @@ async function doPush(files, awHome, dryRun, worktreeFlow = false, preStaged = f
385
1037
 
386
1038
  // ── Phase 1: Prepare commit ────────────────────────────────────────
387
1039
  const prepLabel = files.length === 0
388
- ? 'Creating upload branch from HEAD...'
1040
+ ? (extraPaths.length > 0 ? `Preparing ${managedPathSummary(extraPaths)}...` : 'Creating upload branch from HEAD...')
389
1041
  : `Preparing ${countParts.join(', ')}...`;
390
1042
 
391
1043
  const s = fmt.spinner();
@@ -408,14 +1060,14 @@ async function doPush(files, awHome, dryRun, worktreeFlow = false, preStaged = f
408
1060
  if (newNamespaces.length > 0 && existsSync(codeownersPath)) {
409
1061
  pathsToStage.push('CODEOWNERS');
410
1062
  }
411
- // Also stage any extra paths (content/, CODEOWNERS manual edits) passed from the caller
1063
+ // Also stage any extra platform-doc paths (content/, CODEOWNERS manual edits) passed from the caller.
412
1064
  for (const p of extraPaths) {
413
1065
  if (!pathsToStage.includes(p)) pathsToStage.push(p);
414
1066
  }
415
1067
 
416
- const commitMsg = generateCommitMsg(files);
417
- const prTitle = generatePrTitle(files, awHome);
418
- const prBody = generatePrBody(files, newNamespaces, awHome);
1068
+ const commitMsg = generateCommitMsg(files, extraPaths);
1069
+ const prTitle = generatePrTitle(files, awHome, extraPaths);
1070
+ const prBody = generatePrBody(files, newNamespaces, awHome, extraPaths);
419
1071
 
420
1072
  // ── Phase 2: Commit + push branch ──────────────────────────────────
421
1073
  s.message('Creating branch and pushing...');
@@ -423,7 +1075,7 @@ async function doPush(files, awHome, dryRun, worktreeFlow = false, preStaged = f
423
1075
  let finalBranch;
424
1076
  try {
425
1077
  if (worktreeFlow) {
426
- finalBranch = await createPushBranch(awHome, generateBranchName(files), pathsToStage, commitMsg, preStaged);
1078
+ finalBranch = await createPushBranch(awHome, generateBranchName(files, extraPaths), pathsToStage, commitMsg, preStaged);
427
1079
  } else {
428
1080
  // Only return to main if there are no unmerged commits ahead.
429
1081
  // If we're already on a push branch (previous PR not merged), create the
@@ -435,11 +1087,11 @@ async function doPush(files, awHome, dryRun, worktreeFlow = false, preStaged = f
435
1087
  return;
436
1088
  }
437
1089
  }
438
- finalBranch = await createPushBranch(awHome, generateBranchName(files), pathsToStage, commitMsg, preStaged);
1090
+ finalBranch = await createPushBranch(awHome, generateBranchName(files, extraPaths), pathsToStage, commitMsg, preStaged);
439
1091
  // Stay on the push branch — returning to main makes it look like changes disappeared in IDE
440
1092
  }
441
1093
  const branchLabel = files.length === 0
442
- ? 'Branch created'
1094
+ ? (extraPaths.length > 0 ? `Pushed ${managedPathSummary(extraPaths)}` : 'Branch created')
443
1095
  : `Pushed ${countParts.join(', ')}`;
444
1096
  s.stop(branchLabel);
445
1097
  } catch (e) {
@@ -468,6 +1120,7 @@ async function doPush(files, awHome, dryRun, worktreeFlow = false, preStaged = f
468
1120
  export async function pushCommand(args) {
469
1121
  const input = args._positional?.[0];
470
1122
  const dryRun = args['--dry-run'] === true;
1123
+ const docsOnly = args['--aw-docs-only'] === true || args['--docs-only'] === true;
471
1124
  const cwd = process.cwd();
472
1125
 
473
1126
  const HOME = homedir();
@@ -490,6 +1143,25 @@ export async function pushCommand(args) {
490
1143
  return;
491
1144
  }
492
1145
 
1146
+ if (docsOnly) {
1147
+ try {
1148
+ const scope = resolveAwDocsScope(input, args['--feature']);
1149
+ const result = await publishProjectAwDocs(cwd, HOME, dryRun, scope);
1150
+ if (!result.hasDocs) {
1151
+ fmt.cancel(scope
1152
+ ? `No publishable AW docs found under ${AW_DOCS_DIR}/${scope.relPrefix}.`
1153
+ : 'No publishable AW docs found under .aw_docs/features or .aw_docs/html.');
1154
+ return;
1155
+ }
1156
+ if (dryRun) fmt.outro(chalk.dim('Remove --dry-run to publish AW docs'));
1157
+ else fmt.outro('⟁ AW docs publish complete');
1158
+ return;
1159
+ } catch (e) {
1160
+ fmt.cancel(`AW docs publish failed: ${e.message}`);
1161
+ return;
1162
+ }
1163
+ }
1164
+
493
1165
  const repoUrl = REGISTRY_URL;
494
1166
  if (!isValidClone(awHome, repoUrl)) {
495
1167
  if (!input && hasRulesChanges(cwd)) {
@@ -505,23 +1177,18 @@ export async function pushCommand(args) {
505
1177
  if (!input) {
506
1178
  const rulesChanged = hasRulesChanges(cwd);
507
1179
 
508
- // Extra paths outside .aw_registry/ that aw also manages: content/ and CODEOWNERS.
1180
+ // Extra paths outside .aw_registry/ that aw also manages through the platform-docs PR flow: content/ and CODEOWNERS.
509
1181
  // Detect staged variants for staged-mode and unstaged variants for auto-mode.
510
1182
  const getExtraStagedPaths = async () => {
511
1183
  try {
512
- const { stdout } = await exec(`git -C "${awHome}" diff --cached --name-only -- content/ CODEOWNERS`);
513
- return stdout.trim().split('\n').filter(Boolean);
1184
+ const { stdout } = await exec(`git -C "${awHome}" diff --cached --name-only -- ${EXTRA_PUSH_PATHS_ARG}`);
1185
+ return [...new Set(stdout.trim().split('\n').filter(Boolean))];
514
1186
  } catch { return []; }
515
1187
  };
516
1188
  const getExtraChangedPaths = async () => {
517
1189
  try {
518
- const { stdout } = await exec(`git -C "${awHome}" status --porcelain -- content/ CODEOWNERS`);
519
- // git status --porcelain prefix is XY (2 chars) + optional space + path.
520
- // Staged-only files: `M path` (2-char prefix); unstaged files: ` M path` (3-char prefix).
521
- // slice(2).trimStart() handles both cases correctly.
522
- return stdout.trim().split('\n').filter(Boolean)
523
- .map(l => l.slice(2).trimStart())
524
- .filter(Boolean);
1190
+ const { stdout } = await exec(`git -C "${awHome}" status --porcelain --ignored=matching -uall -- ${EXTRA_PUSH_PATHS_ARG}`);
1191
+ return parseExtraStatusPaths(stdout);
525
1192
  } catch { return []; }
526
1193
  };
527
1194
 
@@ -551,7 +1218,15 @@ export async function pushCommand(args) {
551
1218
  return;
552
1219
  }
553
1220
 
554
- // ── Auto mode: stage all changes in .aw_registry/ ─────────────────
1221
+ // ── Auto mode: publish generated project .aw_docs/ directly, then handle registry/platform docs ─
1222
+ let awDocsPublish;
1223
+ try {
1224
+ awDocsPublish = await publishProjectAwDocs(cwd, HOME, dryRun);
1225
+ } catch (e) {
1226
+ fmt.cancel(`AW docs publish failed: ${e.message}`);
1227
+ return;
1228
+ }
1229
+
555
1230
  const changes = detectChanges(awHome, REGISTRY_DIR);
556
1231
  const extraChanged = await getExtraChangedPaths();
557
1232
  const allEntries = [
@@ -560,6 +1235,17 @@ export async function pushCommand(args) {
560
1235
  ...changes.deleted.map(e => ({ ...e, deleted: true })),
561
1236
  ];
562
1237
 
1238
+ if (allEntries.length === 0 && extraChanged.length === 0 && awDocsPublish?.hasDocs) {
1239
+ if (rulesChanged) {
1240
+ fmt.logInfo('Detected changes under platform rules — redirecting to `aw push-rules`.');
1241
+ pushRulesCommand(args);
1242
+ return;
1243
+ }
1244
+ if (dryRun) fmt.outro(chalk.dim('Remove --dry-run to publish'));
1245
+ else fmt.outro('⟁ Push complete');
1246
+ return;
1247
+ }
1248
+
563
1249
  if (allEntries.length === 0 && extraChanged.length === 0 && commitsAheadOfMain(awHome) > 0) {
564
1250
  fmt.logInfo(`${chalk.dim('mode:')} auto (no new changes — branching current state)`);
565
1251
  await doPush([], awHome, dryRun, worktreeFlow, false);
package/constants.mjs CHANGED
@@ -22,6 +22,29 @@ export const REGISTRY_DIR = '.aw_registry';
22
22
  /** Directory in platform-docs repo containing documentation (pulled into platform/docs/) */
23
23
  export const DOCS_SOURCE_DIR = 'content';
24
24
 
25
+ /** Project-local directory containing generated AW planning/spec artifacts */
26
+ export const AW_DOCS_DIR = '.aw_docs';
27
+
28
+ /** Generated AW docs aggregation repository */
29
+ export const AW_DOCS_REPO = process.env.AW_DOCS_REPO || 'GoHighLevel/ghl-aw-docs';
30
+ export const AW_DOCS_URL = process.env.AW_DOCS_REPO_URL || `https://github.com/${AW_DOCS_REPO}.git`;
31
+ /** Canonical branch for all generated AW docs publishes. Intentionally not configurable per run. */
32
+ export const AW_DOCS_BASE_BRANCH = 'master-sync';
33
+ export const AW_DOCS_SEED_BRANCH = process.env.AW_DOCS_SEED_BRANCH || 'scaffold';
34
+ export const AW_DOCS_PUBLISH_DIR = 'aw_docs';
35
+ export const AW_DOCS_PUBLIC_BASE_URL = process.env.AW_DOCS_PUBLIC_BASE_URL || `https://github.com/${AW_DOCS_REPO}/blob/${AW_DOCS_BASE_BRANCH}`;
36
+ export const AW_DOCS_TEAMOFONE_ORIGIN = process.env.AW_DOCS_TEAMOFONE_ORIGIN || 'https://teamofone.msgsndr.net';
37
+ export const AW_DOCS_TEAMOFONE_BASE_URL = process.env.AW_DOCS_TEAMOFONE_BASE_URL || `${AW_DOCS_TEAMOFONE_ORIGIN}/too/docs/GoHighLevel/ghl-aw-docs`;
38
+
39
+ export function defaultAwDocsGithubDocsConfig() {
40
+ return {
41
+ enabled: true,
42
+ repo: AW_DOCS_REPO,
43
+ dest: AW_DOCS_PUBLISH_DIR,
44
+ teamofone_base_url: AW_DOCS_TEAMOFONE_BASE_URL,
45
+ };
46
+ }
47
+
25
48
  /** Persistent git clone root — ~/.aw/ */
26
49
  export const AW_HOME = join(homedir(), '.aw');
27
50
 
package/git.mjs CHANGED
@@ -5,7 +5,7 @@ import { mkdtempSync, existsSync, lstatSync, rmSync, readFileSync, symlinkSync,
5
5
  import { join, basename, dirname } from 'node:path';
6
6
  import { homedir, tmpdir } from 'node:os';
7
7
  import { promisify } from 'node:util';
8
- import { REGISTRY_BASE_BRANCH, REGISTRY_DIR, DOCS_SOURCE_DIR, RULES_SOURCE_DIR } from './constants.mjs';
8
+ import { REGISTRY_BASE_BRANCH, REGISTRY_DIR, DOCS_SOURCE_DIR, AW_DOCS_DIR, RULES_SOURCE_DIR } from './constants.mjs';
9
9
 
10
10
  const exec = promisify(execCb);
11
11
 
@@ -145,7 +145,8 @@ export function cleanup(tempDir) {
145
145
  * e.g., ["platform", "dev/agents/debugger"] -> [".aw_registry/platform", ".aw_registry/dev/agents/debugger"]
146
146
  *
147
147
  * When "platform" is in the paths, also includes the repo's docs source
148
- * directory (content/) so docs are pulled on-the-fly into platform/docs/.
148
+ * directory (content/) and generated AW docs (.aw_docs/) so docs are pulled
149
+ * on-the-fly into platform/docs/ and the local AW docs cache.
149
150
  */
150
151
  export function includeToSparsePaths(paths) {
151
152
  const result = new Set();
@@ -155,6 +156,7 @@ export function includeToSparsePaths(paths) {
155
156
  result.add(`${REGISTRY_DIR}/AW-PROTOCOL.md`);
156
157
  if (paths.includes('platform')) {
157
158
  result.add(DOCS_SOURCE_DIR);
159
+ result.add(AW_DOCS_DIR);
158
160
  result.add(RULES_SOURCE_DIR);
159
161
  }
160
162
  return [...result];
@@ -497,7 +499,7 @@ export function commitToCurrentBranch(awHome, files, commitMsg, preStaged = fals
497
499
  if (!preStaged) {
498
500
  try {
499
501
  const quotedFiles = files.map(f => `"${f}"`).join(' ');
500
- execSync(`git -C "${awHome}" add ${quotedFiles}`, { stdio: 'pipe' });
502
+ execSync(`git -C "${awHome}" add --sparse -f ${quotedFiles}`, { stdio: 'pipe' });
501
503
  } catch (e) {
502
504
  throw new Error(`Failed to stage files: ${e.message}`);
503
505
  }
@@ -553,7 +555,7 @@ export async function createPushBranch(awHome, branchName, files, commitMsg, pre
553
555
  if (!preStaged) {
554
556
  try {
555
557
  const quotedFiles = files.map(f => `"${f}"`).join(' ');
556
- await exec(`git -C "${awHome}" add ${quotedFiles}`);
558
+ await exec(`git -C "${awHome}" add --sparse -f ${quotedFiles}`);
557
559
  } catch (e) {
558
560
  throw new Error(`Failed to stage files: ${e.message}`);
559
561
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.50",
3
+ "version": "0.1.51",
4
4
  "description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
5
5
  "type": "module",
6
6
  "bin": {