@ghl-ai/aw 0.1.36-beta.12 ā 0.1.36-beta.14
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 +120 -78
- package/git.mjs +47 -10
- package/package.json +1 -1
package/commands/push.mjs
CHANGED
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
isValidClone,
|
|
18
18
|
isWorktree,
|
|
19
19
|
getLocalRegistryDir,
|
|
20
|
+
commitsAheadOfMain,
|
|
21
|
+
logAheadOfMain,
|
|
20
22
|
} from '../git.mjs';
|
|
21
23
|
|
|
22
24
|
const PUSHABLE_TYPES = ['agents', 'skills', 'commands', 'evals'];
|
|
@@ -26,6 +28,9 @@ const PUSHABLE_TYPES = ['agents', 'skills', 'commands', 'evals'];
|
|
|
26
28
|
// Auto-generate a branch name from the files being pushed.
|
|
27
29
|
function generateBranchName(files) {
|
|
28
30
|
const shortId = Date.now().toString(36).slice(-5);
|
|
31
|
+
|
|
32
|
+
if (files.length === 0) return `sync/state-${shortId}`;
|
|
33
|
+
|
|
29
34
|
const namespaces = [...new Set(files.map(f => f.namespace))];
|
|
30
35
|
const hasDeletes = files.some(f => f.deleted);
|
|
31
36
|
const allDeletes = files.every(f => f.deleted);
|
|
@@ -44,92 +49,106 @@ function generateBranchName(files) {
|
|
|
44
49
|
return `${prefix}/batch-${shortId}`;
|
|
45
50
|
}
|
|
46
51
|
|
|
47
|
-
|
|
48
|
-
if (files.length === 1) {
|
|
49
|
-
const f = files[0];
|
|
50
|
-
const verb = f.deleted ? 'Remove' : 'Add';
|
|
51
|
-
const prep = f.deleted ? 'from' : 'to';
|
|
52
|
-
return `${verb} ${f.slug} (${f.type}) ${prep} ${f.namespace}`;
|
|
53
|
-
}
|
|
52
|
+
const TYPE_SINGULAR = { agents: 'agent', skills: 'skill', commands: 'command', evals: 'eval' };
|
|
54
53
|
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
target[f.type] = (target[f.type] || 0) + 1;
|
|
60
|
-
}
|
|
61
|
-
const parts = [];
|
|
62
|
-
for (const [type, count] of Object.entries(addCounts)) {
|
|
63
|
-
parts.push(`+${count} ${type}`);
|
|
64
|
-
}
|
|
65
|
-
for (const [type, count] of Object.entries(deleteCounts)) {
|
|
66
|
-
parts.push(`-${count} ${type}`);
|
|
67
|
-
}
|
|
68
|
-
const namespaces = [...new Set(files.map(f => f.namespace))];
|
|
54
|
+
function singular(type, count) {
|
|
55
|
+
const s = TYPE_SINGULAR[type] ?? type;
|
|
56
|
+
return count === 1 ? s : `${s}s`;
|
|
57
|
+
}
|
|
69
58
|
|
|
70
|
-
|
|
71
|
-
|
|
59
|
+
function generatePrTitle(files, awHome = null) {
|
|
60
|
+
if (files.length === 0) {
|
|
61
|
+
const commits = awHome ? logAheadOfMain(awHome) : [];
|
|
62
|
+
const count = commits.length;
|
|
63
|
+
return count > 0
|
|
64
|
+
? `feat(registry): sync ${count} unpublished commit${count > 1 ? 's' : ''}`
|
|
65
|
+
: 'feat(registry): sync current state';
|
|
72
66
|
}
|
|
73
|
-
return `sync: ${parts.join(', ')} across ${namespaces.join(', ')}`;
|
|
74
|
-
}
|
|
75
67
|
|
|
76
|
-
|
|
68
|
+
const namespaces = [...new Set(files.map(f => f.namespace))];
|
|
69
|
+
|
|
77
70
|
if (files.length === 1) {
|
|
78
71
|
const f = files[0];
|
|
79
|
-
const
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
'',
|
|
83
|
-
`- **Type:** ${f.type}`,
|
|
84
|
-
`- **Slug:** ${f.slug}`,
|
|
85
|
-
`- **Namespace:** ${f.namespace}`,
|
|
86
|
-
`- **Path:** \`${f.registryTarget}\``,
|
|
87
|
-
];
|
|
88
|
-
if (f.deleted) {
|
|
89
|
-
bodyParts.push('', '> File was deleted locally and removed from registry.');
|
|
90
|
-
}
|
|
91
|
-
if (newNamespaces.length > 0) {
|
|
92
|
-
bodyParts.push('', '> **New namespace** ā CODEOWNERS entry auto-added.');
|
|
93
|
-
}
|
|
94
|
-
bodyParts.push('', `${f.deleted ? 'Removed' : 'Uploaded'} via \`aw push\``);
|
|
95
|
-
return bodyParts.join('\n');
|
|
72
|
+
const verb = f.deleted ? 'remove' : 'update';
|
|
73
|
+
const scope = f.namespace;
|
|
74
|
+
return `feat(${scope}): ${verb} ${f.slug} ${TYPE_SINGULAR[f.type] ?? f.type}`;
|
|
96
75
|
}
|
|
97
76
|
|
|
98
77
|
const added = files.filter(f => !f.deleted);
|
|
99
78
|
const deleted = files.filter(f => f.deleted);
|
|
79
|
+
const countsByType = groupBy(added.length ? added : deleted, 'type');
|
|
80
|
+
const summary = Object.entries(countsByType)
|
|
81
|
+
.map(([t, items]) => `${items.length} ${singular(t, items.length)}`)
|
|
82
|
+
.join(', ');
|
|
83
|
+
const verb = added.length === 0 ? 'remove' : deleted.length === 0 ? 'sync' : 'sync';
|
|
84
|
+
|
|
85
|
+
if (namespaces.length === 1) {
|
|
86
|
+
return `feat(${namespaces[0]}): ${verb} ${summary}`;
|
|
87
|
+
}
|
|
88
|
+
return `feat(registry): ${verb} ${files.length} files across ${namespaces.length} namespaces`;
|
|
89
|
+
}
|
|
100
90
|
|
|
101
|
-
|
|
91
|
+
const AW_BRANDING = `---\nš¤ Generated by [AW CLI](https://github.com/GoHighLevel/ghl-agentic-workspace)`;
|
|
102
92
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
93
|
+
function generatePrBody(files, newNamespaces, awHome = null) {
|
|
94
|
+
const added = files.filter(f => !f.deleted);
|
|
95
|
+
const deleted = files.filter(f => f.deleted);
|
|
96
|
+
const bodyParts = [];
|
|
97
|
+
|
|
98
|
+
if (files.length === 0) {
|
|
99
|
+
const commits = awHome ? logAheadOfMain(awHome) : [];
|
|
100
|
+
bodyParts.push('## Unpublished Commits', '');
|
|
101
|
+
if (commits.length > 0) {
|
|
102
|
+
bodyParts.push('| Commit | Message |', '|--------|---------|');
|
|
103
|
+
for (const c of commits) {
|
|
104
|
+
bodyParts.push(`| \`${c.hash}\` | ${c.message} |`);
|
|
110
105
|
}
|
|
106
|
+
} else {
|
|
107
|
+
bodyParts.push('Syncing current state to registry.');
|
|
111
108
|
}
|
|
112
|
-
bodyParts.push('');
|
|
109
|
+
bodyParts.push('', AW_BRANDING);
|
|
110
|
+
return bodyParts.join('\n');
|
|
113
111
|
}
|
|
114
112
|
|
|
115
|
-
if (
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
113
|
+
if (files.length === 1) {
|
|
114
|
+
const f = files[0];
|
|
115
|
+
const verb = f.deleted ? 'Removed' : 'Updated';
|
|
116
|
+
bodyParts.push(
|
|
117
|
+
`## ${verb} \`${f.slug}\` ${TYPE_SINGULAR[f.type] ?? f.type}`,
|
|
118
|
+
'',
|
|
119
|
+
`| Field | Value |`,
|
|
120
|
+
`|-------|-------|`,
|
|
121
|
+
`| Namespace | \`${f.namespace}\` |`,
|
|
122
|
+
`| Type | ${f.type} |`,
|
|
123
|
+
`| Path | \`${f.registryTarget}\` |`,
|
|
124
|
+
);
|
|
125
|
+
} else {
|
|
126
|
+
bodyParts.push('## Registry Sync', '');
|
|
127
|
+
|
|
128
|
+
if (added.length > 0) {
|
|
129
|
+
bodyParts.push('### Added / Updated', '');
|
|
130
|
+
bodyParts.push('| Type | Slug | Namespace |', '|------|------|-----------|');
|
|
131
|
+
for (const f of added.sort((a, b) => a.slug.localeCompare(b.slug))) {
|
|
132
|
+
bodyParts.push(`| ${f.type} | \`${f.slug}\` | \`${f.namespace}\` |`);
|
|
133
|
+
}
|
|
134
|
+
bodyParts.push('');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (deleted.length > 0) {
|
|
138
|
+
bodyParts.push('### Removed', '');
|
|
139
|
+
bodyParts.push('| Type | Slug | Namespace |', '|------|------|-----------|');
|
|
140
|
+
for (const f of deleted.sort((a, b) => a.slug.localeCompare(b.slug))) {
|
|
141
|
+
bodyParts.push(`| ${f.type} | ~~\`${f.slug}\`~~ | \`${f.namespace}\` |`);
|
|
122
142
|
}
|
|
143
|
+
bodyParts.push('');
|
|
123
144
|
}
|
|
124
|
-
bodyParts.push('');
|
|
125
145
|
}
|
|
126
146
|
|
|
127
147
|
if (newNamespaces.length > 0) {
|
|
128
|
-
bodyParts.push(`> **New namespace${newNamespaces.length > 1 ? 's' : ''}:**
|
|
129
|
-
bodyParts.push('');
|
|
148
|
+
bodyParts.push('', `> **New namespace${newNamespaces.length > 1 ? 's' : ''}:** \`${newNamespaces.join('`, `')}\` ā CODEOWNERS entr${newNamespaces.length > 1 ? 'ies' : 'y'} auto-added.`);
|
|
130
149
|
}
|
|
131
150
|
|
|
132
|
-
bodyParts.push(
|
|
151
|
+
bodyParts.push('', AW_BRANDING);
|
|
133
152
|
return bodyParts.join('\n');
|
|
134
153
|
}
|
|
135
154
|
|
|
@@ -277,7 +296,11 @@ function doPush(files, awHome, dryRun, worktreeFlow = false, preStaged = false)
|
|
|
277
296
|
const deletedParts = Object.entries(groupBy(deleted, 'type')).map(([t, items]) => `${items.length} ${t} removed`);
|
|
278
297
|
const countParts = [...addedParts, ...deletedParts];
|
|
279
298
|
|
|
280
|
-
|
|
299
|
+
if (files.length === 0) {
|
|
300
|
+
fmt.logInfo('Branching current state (no new changes)');
|
|
301
|
+
} else {
|
|
302
|
+
fmt.logInfo(`${chalk.bold(files.length)} file${files.length > 1 ? 's' : ''} to push (${countParts.join(', ')})`);
|
|
303
|
+
}
|
|
281
304
|
|
|
282
305
|
if (dryRun) {
|
|
283
306
|
for (const f of files) {
|
|
@@ -309,8 +332,8 @@ function doPush(files, awHome, dryRun, worktreeFlow = false, preStaged = false)
|
|
|
309
332
|
}
|
|
310
333
|
|
|
311
334
|
const commitMsg = generateCommitMsg(files);
|
|
312
|
-
const prTitle = generatePrTitle(files);
|
|
313
|
-
const prBody = generatePrBody(files, newNamespaces);
|
|
335
|
+
const prTitle = generatePrTitle(files, awHome);
|
|
336
|
+
const prBody = generatePrBody(files, newNamespaces, awHome);
|
|
314
337
|
|
|
315
338
|
const s = fmt.spinner();
|
|
316
339
|
s.start('Committing and pushing...');
|
|
@@ -416,23 +439,42 @@ export function pushCommand(args) {
|
|
|
416
439
|
...changes.deleted.map(e => ({ ...e, deleted: true })),
|
|
417
440
|
];
|
|
418
441
|
|
|
442
|
+
if (allEntries.length === 0 && worktreeFlow && commitsAheadOfMain(awHome) > 0) {
|
|
443
|
+
fmt.logInfo(`${chalk.dim('mode:')} auto (no new changes ā branching current state)`);
|
|
444
|
+
doPush([], awHome, dryRun, worktreeFlow, false);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
419
448
|
if (allEntries.length === 0) {
|
|
420
449
|
fmt.cancel('Nothing to push ā no staged or modified files.\n\n Stage files in your IDE or use `aw status` to see changes.');
|
|
421
450
|
return;
|
|
422
451
|
}
|
|
423
452
|
|
|
424
|
-
const files = allEntries
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
453
|
+
const files = allEntries
|
|
454
|
+
.filter(f => parseRegistryPath(f.registryPath) !== null)
|
|
455
|
+
.map(f => {
|
|
456
|
+
const meta = parseRegistryPath(f.registryPath);
|
|
457
|
+
const parts = f.registryPath.split('/');
|
|
458
|
+
return {
|
|
459
|
+
absPath: join(awHome, f.path),
|
|
460
|
+
registryTarget: f.path,
|
|
461
|
+
type: meta?.type ?? 'file',
|
|
462
|
+
namespace: meta?.namespace ?? parts.slice(0, -1).join('/'),
|
|
463
|
+
slug: meta?.slug ?? parts[parts.length - 1].replace(/\.md$/, ''),
|
|
464
|
+
deleted: f.deleted,
|
|
465
|
+
};
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
if (files.length === 0) {
|
|
469
|
+
// In worktree flow, still push if there are commits ahead of main not yet in a PR
|
|
470
|
+
if (worktreeFlow && commitsAheadOfMain(awHome) > 0) {
|
|
471
|
+
fmt.logInfo(`${chalk.dim('mode:')} auto (no new changes ā branching current state)`);
|
|
472
|
+
doPush([], awHome, dryRun, worktreeFlow, false);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
fmt.cancel('Nothing to push ā no staged or modified files.\n\n Stage files in your IDE or use `aw status` to see changes.');
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
436
478
|
|
|
437
479
|
fmt.logInfo(`${chalk.dim('mode:')} auto (${files.length} file${files.length > 1 ? 's' : ''} ā stage specific files to push a subset)`);
|
|
438
480
|
doPush(files, awHome, dryRun, worktreeFlow, false);
|
package/git.mjs
CHANGED
|
@@ -345,6 +345,7 @@ export function updatePushBranch(awHome, pushBranchName) {
|
|
|
345
345
|
*
|
|
346
346
|
* preStaged=true ā files are already in the index; skip `git add`
|
|
347
347
|
* preStaged=false ā explicitly stage `files` before committing
|
|
348
|
+
* files=[] ā skip staging+commit entirely (branch current HEAD and push)
|
|
348
349
|
*
|
|
349
350
|
* Branch stays on disk for iteration. Returns the branch name.
|
|
350
351
|
*/
|
|
@@ -355,21 +356,23 @@ export function createPushBranch(awHome, branchName, files, commitMsg, preStaged
|
|
|
355
356
|
throw new Error(`Failed to create branch ${branchName}: ${e.message}`);
|
|
356
357
|
}
|
|
357
358
|
|
|
358
|
-
if (
|
|
359
|
+
if (files.length > 0 || preStaged) {
|
|
360
|
+
if (!preStaged) {
|
|
361
|
+
try {
|
|
362
|
+
const quotedFiles = files.map(f => `"${f}"`).join(' ');
|
|
363
|
+
execSync(`git -C "${awHome}" add ${quotedFiles}`, { stdio: 'pipe' });
|
|
364
|
+
} catch (e) {
|
|
365
|
+
throw new Error(`Failed to stage files: ${e.message}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
359
369
|
try {
|
|
360
|
-
|
|
361
|
-
execSync(`git -C "${awHome}" add ${quotedFiles}`, { stdio: 'pipe' });
|
|
370
|
+
execSync(`git -C "${awHome}" commit -m "${commitMsg.replace(/"/g, '\\"')}"`, { stdio: 'pipe' });
|
|
362
371
|
} catch (e) {
|
|
363
|
-
throw new Error(`Failed to
|
|
372
|
+
throw new Error(`Failed to commit: ${e.message}`);
|
|
364
373
|
}
|
|
365
374
|
}
|
|
366
375
|
|
|
367
|
-
try {
|
|
368
|
-
execSync(`git -C "${awHome}" commit -m "${commitMsg.replace(/"/g, '\\"')}"`, { stdio: 'pipe' });
|
|
369
|
-
} catch (e) {
|
|
370
|
-
throw new Error(`Failed to commit: ${e.message}`);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
376
|
try {
|
|
374
377
|
execSync(`git -C "${awHome}" push -u origin "${branchName}"`, { stdio: 'pipe' });
|
|
375
378
|
} catch (e) {
|
|
@@ -379,6 +382,40 @@ export function createPushBranch(awHome, branchName, files, commitMsg, preStaged
|
|
|
379
382
|
return branchName;
|
|
380
383
|
}
|
|
381
384
|
|
|
385
|
+
/**
|
|
386
|
+
* Count commits on the current branch that are not yet in main.
|
|
387
|
+
*/
|
|
388
|
+
export function commitsAheadOfMain(awHome) {
|
|
389
|
+
try {
|
|
390
|
+
const out = execSync(
|
|
391
|
+
`git -C "${awHome}" rev-list --count ${REGISTRY_BASE_BRANCH}..HEAD`,
|
|
392
|
+
{ stdio: 'pipe', encoding: 'utf8' },
|
|
393
|
+
);
|
|
394
|
+
return parseInt(out.trim(), 10) || 0;
|
|
395
|
+
} catch {
|
|
396
|
+
return 0;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Get one-line log of commits ahead of main.
|
|
402
|
+
* Returns array of { hash, message } objects.
|
|
403
|
+
*/
|
|
404
|
+
export function logAheadOfMain(awHome) {
|
|
405
|
+
try {
|
|
406
|
+
const out = execSync(
|
|
407
|
+
`git -C "${awHome}" log ${REGISTRY_BASE_BRANCH}..HEAD --oneline`,
|
|
408
|
+
{ stdio: 'pipe', encoding: 'utf8' },
|
|
409
|
+
);
|
|
410
|
+
return out.trim().split('\n').filter(Boolean).map(line => {
|
|
411
|
+
const spaceIdx = line.indexOf(' ');
|
|
412
|
+
return { hash: line.slice(0, spaceIdx), message: line.slice(spaceIdx + 1) };
|
|
413
|
+
});
|
|
414
|
+
} catch {
|
|
415
|
+
return [];
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
382
419
|
/**
|
|
383
420
|
* Get the current branch name in awHome.
|
|
384
421
|
*/
|