@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/c4/templates/scripts/aw-c4-bootstrap.sh +4 -5
- package/cli.mjs +9 -21
- package/commands/c4.mjs +9 -17
- package/commands/doctor.mjs +5 -2
- package/commands/init.mjs +54 -125
- package/commands/integration.mjs +111 -0
- package/commands/nuke.mjs +3 -1
- package/commands/pull.mjs +2 -3
- package/commands/push.mjs +29 -715
- package/commands/startup.mjs +3 -22
- package/constants.mjs +0 -23
- package/ecc.mjs +1 -1
- package/git.mjs +4 -19
- package/integrate.mjs +21 -94
- package/integrations/context-mode.mjs +514 -0
- package/integrations/index.mjs +31 -0
- package/link.mjs +33 -159
- package/mcp.mjs +16 -132
- package/package.json +4 -4
- package/render-rules.mjs +1 -25
- package/startup.mjs +8 -52
- package/commands/integrations.mjs +0 -254
- package/commands/mcp.mjs +0 -90
- package/integrations.mjs +0 -971
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
371
|
+
if (files.length === 0) {
|
|
1017
372
|
fmt.logInfo('No new changes — would branch current state');
|
|
1018
373
|
} else {
|
|
1019
|
-
|
|
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
|
-
?
|
|
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
|
|
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
|
|
1069
|
-
const prTitle = generatePrTitle(files, awHome
|
|
1070
|
-
const prBody = generatePrBody(files, newNamespaces, awHome
|
|
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
|
|
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
|
|
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
|
-
?
|
|
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
|
|
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 --
|
|
1185
|
-
return
|
|
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 --
|
|
1191
|
-
|
|
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:
|
|
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);
|