@ghl-ai/aw 0.1.50-beta.1 → 0.1.50

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/push.mjs CHANGED
@@ -1,46 +1,17 @@
1
1
  // commands/push.mjs — Push local agents/skills to registry via PR using persistent git clone
2
2
 
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';
3
+ import { existsSync, statSync, readFileSync, appendFileSync } from 'node:fs';
4
+ import { join, dirname } from 'node:path';
16
5
  import { fileURLToPath } from 'node:url';
17
6
  import { exec as execCb, execFile as execFileCb } from 'node:child_process';
18
7
  import { promisify } from 'node:util';
19
8
  import { homedir } from 'node:os';
20
- import { createHash } from 'node:crypto';
21
9
 
22
10
  const exec = promisify(execCb);
23
11
  const execFile = promisify(execFileCb);
24
12
  import * as fmt from '../fmt.mjs';
25
13
  import { chalk } from '../fmt.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';
14
+ import { REGISTRY_REPO, REGISTRY_URL, REGISTRY_BASE_BRANCH, REGISTRY_DIR, AW_CO_AUTHOR } from '../constants.mjs';
44
15
  import { resolveInput } from '../paths.mjs';
45
16
  import { walkRegistryTree, getAllFiles } from '../registry.mjs';
46
17
  import {
@@ -50,7 +21,6 @@ import {
50
21
  checkoutMain,
51
22
  isValidClone,
52
23
  getLocalRegistryDir,
53
- findNearestWorktree,
54
24
  commitsAheadOfMain,
55
25
  logAheadOfMain,
56
26
  } from '../git.mjs';
@@ -60,612 +30,13 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
60
30
  const VERSION = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')).version;
61
31
 
62
32
  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
- }
659
33
 
660
34
  // ── PR content generation ────────────────────────────────────────────
661
35
 
662
36
  // Auto-generate a branch name from the files being pushed.
663
- function generateBranchName(files, extraPaths = []) {
37
+ function generateBranchName(files) {
664
38
  const shortId = Date.now().toString(36).slice(-5);
665
39
 
666
- if (files.length === 0 && extraPaths.length > 0) {
667
- return `${managedBranchPrefix(extraPaths)}-${shortId}`;
668
- }
669
40
  if (files.length === 0) return `sync/state-${shortId}`;
670
41
 
671
42
  const namespaces = [...new Set(files.map(f => f.namespace))];
@@ -704,10 +75,8 @@ function parseCommitFiles(commits) {
704
75
  return result;
705
76
  }
706
77
 
707
- function generatePrTitle(files, awHome = null, extraPaths = []) {
78
+ function generatePrTitle(files, awHome = null) {
708
79
  if (files.length === 0) {
709
- if (extraPaths.length > 0) return managedPrTitle(extraPaths);
710
-
711
80
  const commits = awHome ? logAheadOfMain(awHome) : [];
712
81
  const parsed = parseCommitFiles(commits);
713
82
 
@@ -754,7 +123,7 @@ function generatePrTitle(files, awHome = null, extraPaths = []) {
754
123
 
755
124
  const AW_BRANDING = `---\n⟁ Generated by [AW CLI](https://platform.docs/agentic-workspace/guides/cli)`;
756
125
 
757
- function generatePrBody(files, newNamespaces, awHome = null, extraPaths = []) {
126
+ function generatePrBody(files, newNamespaces, awHome = null) {
758
127
  const added = files.filter(f => !f.deleted);
759
128
  const deleted = files.filter(f => f.deleted);
760
129
  const bodyParts = [];
@@ -762,10 +131,7 @@ function generatePrBody(files, newNamespaces, awHome = null, extraPaths = []) {
762
131
  if (files.length === 0) {
763
132
  const commits = awHome ? logAheadOfMain(awHome) : [];
764
133
  bodyParts.push('## What\'s included', '');
765
- if (extraPaths.length > 0) {
766
- bodyParts.push(`Syncing ${managedPathSummary(extraPaths)}.`, '', '### Paths', '');
767
- bodyParts.push(...formatManagedPathList(extraPaths));
768
- } else if (commits.length > 0) {
134
+ if (commits.length > 0) {
769
135
  for (const c of commits) {
770
136
  bodyParts.push(`- \`${c.hash}\` ${c.message}`);
771
137
  }
@@ -814,16 +180,11 @@ function generatePrBody(files, newNamespaces, awHome = null, extraPaths = []) {
814
180
  bodyParts.push('', `> **New namespace${newNamespaces.length > 1 ? 's' : ''}:** \`${newNamespaces.join('`, `')}\` — CODEOWNERS entr${newNamespaces.length > 1 ? 'ies' : 'y'} auto-added.`);
815
181
  }
816
182
 
817
- if (extraPaths.length > 0) {
818
- bodyParts.push('', '### Managed Docs', '', `Also syncing ${managedPathSummary(extraPaths)}.`, '');
819
- bodyParts.push(...formatManagedPathList(extraPaths));
820
- }
821
-
822
183
  bodyParts.push('', AW_BRANDING);
823
184
  return bodyParts.join('\n');
824
185
  }
825
186
 
826
- function generateCommitMsg(files, extraPaths = []) {
187
+ function generateCommitMsg(files) {
827
188
  const added = files.filter(f => !f.deleted);
828
189
  const deleted = files.filter(f => f.deleted);
829
190
  const addedParts = Object.entries(groupBy(added, 'type')).map(([t, items]) => `${items.length} ${singular(t, items.length)}`);
@@ -833,16 +194,10 @@ function generateCommitMsg(files, extraPaths = []) {
833
194
  const version = VERSION;
834
195
  const trailer = `\n\nGenerated-By: aw/${version}\n${AW_CO_AUTHOR}`;
835
196
 
836
- if (files.length === 0 && extraPaths.length > 0) {
837
- return `docs: sync ${managedPathSummary(extraPaths)}${trailer}`;
838
- }
839
197
  if (files.length === 1) {
840
198
  const f = files[0];
841
199
  return `registry: ${f.deleted ? 'remove' : 'add'} ${f.type}/${f.slug} ${f.deleted ? 'from' : 'to'} ${f.namespace}${trailer}`;
842
200
  }
843
- if (extraPaths.length > 0) {
844
- return `registry: sync ${files.length} files and ${managedPathSummary(extraPaths)}${trailer}`;
845
- }
846
201
  return `registry: sync ${files.length} files (${countParts.join(', ')})${trailer}`;
847
202
  }
848
203
 
@@ -1013,22 +368,15 @@ async function doPush(files, awHome, dryRun, worktreeFlow = false, preStaged = f
1013
368
 
1014
369
  // ── Dry-run: just list files and exit ──────────────────────────────
1015
370
  if (dryRun) {
1016
- if (files.length === 0 && extraPaths.length === 0) {
371
+ if (files.length === 0) {
1017
372
  fmt.logInfo('No new changes — would branch current state');
1018
373
  } else {
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})`);
374
+ fmt.logInfo(`${chalk.bold(files.length)} file${files.length > 1 ? 's' : ''} to push (${countParts.join(', ')})`);
1024
375
  for (const f of files) {
1025
376
  const ns = chalk.dim(` [${f.namespace}]`);
1026
377
  const label = f.deleted ? chalk.red('DELETE') : chalk.yellow(f.type);
1027
378
  fmt.logMessage(` ${label}/${f.slug}${ns}`);
1028
379
  }
1029
- for (const p of extraPaths) {
1030
- fmt.logMessage(` ${chalk.yellow('DOC')}/${p}`);
1031
- }
1032
380
  }
1033
381
  fmt.logWarn('No changes made (--dry-run)');
1034
382
  fmt.outro(chalk.dim('Remove --dry-run to push'));
@@ -1037,7 +385,7 @@ async function doPush(files, awHome, dryRun, worktreeFlow = false, preStaged = f
1037
385
 
1038
386
  // ── Phase 1: Prepare commit ────────────────────────────────────────
1039
387
  const prepLabel = files.length === 0
1040
- ? (extraPaths.length > 0 ? `Preparing ${managedPathSummary(extraPaths)}...` : 'Creating upload branch from HEAD...')
388
+ ? 'Creating upload branch from HEAD...'
1041
389
  : `Preparing ${countParts.join(', ')}...`;
1042
390
 
1043
391
  const s = fmt.spinner();
@@ -1060,14 +408,14 @@ async function doPush(files, awHome, dryRun, worktreeFlow = false, preStaged = f
1060
408
  if (newNamespaces.length > 0 && existsSync(codeownersPath)) {
1061
409
  pathsToStage.push('CODEOWNERS');
1062
410
  }
1063
- // Also stage any extra platform-doc paths (content/, CODEOWNERS manual edits) passed from the caller.
411
+ // Also stage any extra paths (content/, CODEOWNERS manual edits) passed from the caller
1064
412
  for (const p of extraPaths) {
1065
413
  if (!pathsToStage.includes(p)) pathsToStage.push(p);
1066
414
  }
1067
415
 
1068
- const commitMsg = generateCommitMsg(files, extraPaths);
1069
- const prTitle = generatePrTitle(files, awHome, extraPaths);
1070
- const prBody = generatePrBody(files, newNamespaces, awHome, extraPaths);
416
+ const commitMsg = generateCommitMsg(files);
417
+ const prTitle = generatePrTitle(files, awHome);
418
+ const prBody = generatePrBody(files, newNamespaces, awHome);
1071
419
 
1072
420
  // ── Phase 2: Commit + push branch ──────────────────────────────────
1073
421
  s.message('Creating branch and pushing...');
@@ -1075,7 +423,7 @@ async function doPush(files, awHome, dryRun, worktreeFlow = false, preStaged = f
1075
423
  let finalBranch;
1076
424
  try {
1077
425
  if (worktreeFlow) {
1078
- finalBranch = await createPushBranch(awHome, generateBranchName(files, extraPaths), pathsToStage, commitMsg, preStaged);
426
+ finalBranch = await createPushBranch(awHome, generateBranchName(files), pathsToStage, commitMsg, preStaged);
1079
427
  } else {
1080
428
  // Only return to main if there are no unmerged commits ahead.
1081
429
  // If we're already on a push branch (previous PR not merged), create the
@@ -1087,11 +435,11 @@ async function doPush(files, awHome, dryRun, worktreeFlow = false, preStaged = f
1087
435
  return;
1088
436
  }
1089
437
  }
1090
- finalBranch = await createPushBranch(awHome, generateBranchName(files, extraPaths), pathsToStage, commitMsg, preStaged);
438
+ finalBranch = await createPushBranch(awHome, generateBranchName(files), pathsToStage, commitMsg, preStaged);
1091
439
  // Stay on the push branch — returning to main makes it look like changes disappeared in IDE
1092
440
  }
1093
441
  const branchLabel = files.length === 0
1094
- ? (extraPaths.length > 0 ? `Pushed ${managedPathSummary(extraPaths)}` : 'Branch created')
442
+ ? 'Branch created'
1095
443
  : `Pushed ${countParts.join(', ')}`;
1096
444
  s.stop(branchLabel);
1097
445
  } catch (e) {
@@ -1120,7 +468,6 @@ async function doPush(files, awHome, dryRun, worktreeFlow = false, preStaged = f
1120
468
  export async function pushCommand(args) {
1121
469
  const input = args._positional?.[0];
1122
470
  const dryRun = args['--dry-run'] === true;
1123
- const docsOnly = args['--aw-docs-only'] === true || args['--docs-only'] === true;
1124
471
  const cwd = process.cwd();
1125
472
 
1126
473
  const HOME = homedir();
@@ -1143,25 +490,6 @@ export async function pushCommand(args) {
1143
490
  return;
1144
491
  }
1145
492
 
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
-
1165
493
  const repoUrl = REGISTRY_URL;
1166
494
  if (!isValidClone(awHome, repoUrl)) {
1167
495
  if (!input && hasRulesChanges(cwd)) {
@@ -1177,18 +505,23 @@ export async function pushCommand(args) {
1177
505
  if (!input) {
1178
506
  const rulesChanged = hasRulesChanges(cwd);
1179
507
 
1180
- // Extra paths outside .aw_registry/ that aw also manages through the platform-docs PR flow: content/ and CODEOWNERS.
508
+ // Extra paths outside .aw_registry/ that aw also manages: content/ and CODEOWNERS.
1181
509
  // Detect staged variants for staged-mode and unstaged variants for auto-mode.
1182
510
  const getExtraStagedPaths = async () => {
1183
511
  try {
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))];
512
+ const { stdout } = await exec(`git -C "${awHome}" diff --cached --name-only -- content/ CODEOWNERS`);
513
+ return stdout.trim().split('\n').filter(Boolean);
1186
514
  } catch { return []; }
1187
515
  };
1188
516
  const getExtraChangedPaths = async () => {
1189
517
  try {
1190
- const { stdout } = await exec(`git -C "${awHome}" status --porcelain --ignored=matching -uall -- ${EXTRA_PUSH_PATHS_ARG}`);
1191
- return parseExtraStatusPaths(stdout);
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);
1192
525
  } catch { return []; }
1193
526
  };
1194
527
 
@@ -1218,15 +551,7 @@ export async function pushCommand(args) {
1218
551
  return;
1219
552
  }
1220
553
 
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
-
554
+ // ── Auto mode: stage all changes in .aw_registry/ ─────────────────
1230
555
  const changes = detectChanges(awHome, REGISTRY_DIR);
1231
556
  const extraChanged = await getExtraChangedPaths();
1232
557
  const allEntries = [
@@ -1235,17 +560,6 @@ export async function pushCommand(args) {
1235
560
  ...changes.deleted.map(e => ({ ...e, deleted: true })),
1236
561
  ];
1237
562
 
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
-
1249
563
  if (allEntries.length === 0 && extraChanged.length === 0 && commitsAheadOfMain(awHome) > 0) {
1250
564
  fmt.logInfo(`${chalk.dim('mode:')} auto (no new changes — branching current state)`);
1251
565
  await doPush([], awHome, dryRun, worktreeFlow, false);