@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 +5 -0
- package/commands/push.mjs +715 -29
- package/constants.mjs +23 -0
- package/git.mjs +6 -4
- package/package.json +1 -1
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 {
|
|
4
|
-
|
|
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 {
|
|
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 (
|
|
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
|
-
|
|
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 --
|
|
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 --
|
|
519
|
-
|
|
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:
|
|
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
|
|
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
|
}
|