@ghl-ai/aw 0.1.36-beta.12 → 0.1.36-beta.13
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 +105 -74
- package/git.mjs +28 -10
- package/package.json +1 -1
package/commands/push.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
isValidClone,
|
|
18
18
|
isWorktree,
|
|
19
19
|
getLocalRegistryDir,
|
|
20
|
+
commitsAheadOfMain,
|
|
20
21
|
} from '../git.mjs';
|
|
21
22
|
|
|
22
23
|
const PUSHABLE_TYPES = ['agents', 'skills', 'commands', 'evals'];
|
|
@@ -26,6 +27,9 @@ const PUSHABLE_TYPES = ['agents', 'skills', 'commands', 'evals'];
|
|
|
26
27
|
// Auto-generate a branch name from the files being pushed.
|
|
27
28
|
function generateBranchName(files) {
|
|
28
29
|
const shortId = Date.now().toString(36).slice(-5);
|
|
30
|
+
|
|
31
|
+
if (files.length === 0) return `sync/state-${shortId}`;
|
|
32
|
+
|
|
29
33
|
const namespaces = [...new Set(files.map(f => f.namespace))];
|
|
30
34
|
const hasDeletes = files.some(f => f.deleted);
|
|
31
35
|
const allDeletes = files.every(f => f.deleted);
|
|
@@ -44,92 +48,96 @@ function generateBranchName(files) {
|
|
|
44
48
|
return `${prefix}/batch-${shortId}`;
|
|
45
49
|
}
|
|
46
50
|
|
|
51
|
+
const TYPE_SINGULAR = { agents: 'agent', skills: 'skill', commands: 'command', evals: 'eval' };
|
|
52
|
+
|
|
53
|
+
function singular(type, count) {
|
|
54
|
+
const s = TYPE_SINGULAR[type] ?? type;
|
|
55
|
+
return count === 1 ? s : `${s}s`;
|
|
56
|
+
}
|
|
57
|
+
|
|
47
58
|
function generatePrTitle(files) {
|
|
59
|
+
if (files.length === 0) return 'feat(registry): sync current state';
|
|
60
|
+
|
|
61
|
+
const namespaces = [...new Set(files.map(f => f.namespace))];
|
|
62
|
+
|
|
48
63
|
if (files.length === 1) {
|
|
49
64
|
const f = files[0];
|
|
50
|
-
const verb = f.deleted ? '
|
|
51
|
-
const
|
|
52
|
-
return
|
|
65
|
+
const verb = f.deleted ? 'remove' : 'update';
|
|
66
|
+
const scope = f.namespace;
|
|
67
|
+
return `feat(${scope}): ${verb} ${f.slug} ${TYPE_SINGULAR[f.type] ?? f.type}`;
|
|
53
68
|
}
|
|
54
69
|
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
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))];
|
|
70
|
+
const added = files.filter(f => !f.deleted);
|
|
71
|
+
const deleted = files.filter(f => f.deleted);
|
|
72
|
+
const countsByType = groupBy(added.length ? added : deleted, 'type');
|
|
73
|
+
const summary = Object.entries(countsByType)
|
|
74
|
+
.map(([t, items]) => `${items.length} ${singular(t, items.length)}`)
|
|
75
|
+
.join(', ');
|
|
76
|
+
const verb = added.length === 0 ? 'remove' : deleted.length === 0 ? 'sync' : 'sync';
|
|
69
77
|
|
|
70
78
|
if (namespaces.length === 1) {
|
|
71
|
-
return `
|
|
79
|
+
return `feat(${namespaces[0]}): ${verb} ${summary}`;
|
|
72
80
|
}
|
|
73
|
-
return `
|
|
81
|
+
return `feat(registry): ${verb} ${files.length} files across ${namespaces.length} namespaces`;
|
|
74
82
|
}
|
|
75
83
|
|
|
76
|
-
|
|
77
|
-
if (files.length === 1) {
|
|
78
|
-
const f = files[0];
|
|
79
|
-
const action = f.deleted ? 'Registry Removal' : 'Registry Upload';
|
|
80
|
-
const bodyParts = [
|
|
81
|
-
`## ${action}`,
|
|
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');
|
|
96
|
-
}
|
|
84
|
+
const AW_BRANDING = `---\nGenerated by [AW CLI](https://github.com/GoHighLevel/ghl-agentic-workspace)`;
|
|
97
85
|
|
|
86
|
+
function generatePrBody(files, newNamespaces) {
|
|
98
87
|
const added = files.filter(f => !f.deleted);
|
|
99
88
|
const deleted = files.filter(f => f.deleted);
|
|
89
|
+
const bodyParts = [];
|
|
100
90
|
|
|
101
|
-
|
|
91
|
+
if (files.length === 0) {
|
|
92
|
+
bodyParts.push(
|
|
93
|
+
'## Registry Sync',
|
|
94
|
+
'',
|
|
95
|
+
'No new file changes — branching current state to capture all unpublished commits.',
|
|
96
|
+
'',
|
|
97
|
+
AW_BRANDING,
|
|
98
|
+
);
|
|
99
|
+
return bodyParts.join('\n');
|
|
100
|
+
}
|
|
102
101
|
|
|
103
|
-
if (
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
102
|
+
if (files.length === 1) {
|
|
103
|
+
const f = files[0];
|
|
104
|
+
const verb = f.deleted ? 'Removed' : 'Updated';
|
|
105
|
+
bodyParts.push(
|
|
106
|
+
`## ${verb} \`${f.slug}\` ${TYPE_SINGULAR[f.type] ?? f.type}`,
|
|
107
|
+
'',
|
|
108
|
+
`| Field | Value |`,
|
|
109
|
+
`|-------|-------|`,
|
|
110
|
+
`| Namespace | \`${f.namespace}\` |`,
|
|
111
|
+
`| Type | ${f.type} |`,
|
|
112
|
+
`| Path | \`${f.registryTarget}\` |`,
|
|
113
|
+
);
|
|
114
|
+
} else {
|
|
115
|
+
bodyParts.push('## Registry Sync', '');
|
|
116
|
+
|
|
117
|
+
if (added.length > 0) {
|
|
118
|
+
bodyParts.push('### Added / Updated', '');
|
|
119
|
+
bodyParts.push('| Type | Slug | Namespace |', '|------|------|-----------|');
|
|
120
|
+
for (const f of added.sort((a, b) => a.slug.localeCompare(b.slug))) {
|
|
121
|
+
bodyParts.push(`| ${f.type} | \`${f.slug}\` | \`${f.namespace}\` |`);
|
|
110
122
|
}
|
|
123
|
+
bodyParts.push('');
|
|
111
124
|
}
|
|
112
|
-
bodyParts.push('');
|
|
113
|
-
}
|
|
114
125
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
for (const item of items.sort((a, b) => a.slug.localeCompare(b.slug))) {
|
|
121
|
-
bodyParts.push(`- ~~\`${item.slug}\`~~ (${item.namespace})`);
|
|
126
|
+
if (deleted.length > 0) {
|
|
127
|
+
bodyParts.push('### Removed', '');
|
|
128
|
+
bodyParts.push('| Type | Slug | Namespace |', '|------|------|-----------|');
|
|
129
|
+
for (const f of deleted.sort((a, b) => a.slug.localeCompare(b.slug))) {
|
|
130
|
+
bodyParts.push(`| ${f.type} | ~~\`${f.slug}\`~~ | \`${f.namespace}\` |`);
|
|
122
131
|
}
|
|
132
|
+
bodyParts.push('');
|
|
123
133
|
}
|
|
124
|
-
bodyParts.push('');
|
|
125
134
|
}
|
|
126
135
|
|
|
127
136
|
if (newNamespaces.length > 0) {
|
|
128
|
-
bodyParts.push(`> **New namespace${newNamespaces.length > 1 ? 's' : ''}:**
|
|
129
|
-
bodyParts.push('');
|
|
137
|
+
bodyParts.push('', `> **New namespace${newNamespaces.length > 1 ? 's' : ''}:** \`${newNamespaces.join('`, `')}\` — CODEOWNERS entr${newNamespaces.length > 1 ? 'ies' : 'y'} auto-added.`);
|
|
130
138
|
}
|
|
131
139
|
|
|
132
|
-
bodyParts.push(
|
|
140
|
+
bodyParts.push('', AW_BRANDING);
|
|
133
141
|
return bodyParts.join('\n');
|
|
134
142
|
}
|
|
135
143
|
|
|
@@ -277,7 +285,11 @@ function doPush(files, awHome, dryRun, worktreeFlow = false, preStaged = false)
|
|
|
277
285
|
const deletedParts = Object.entries(groupBy(deleted, 'type')).map(([t, items]) => `${items.length} ${t} removed`);
|
|
278
286
|
const countParts = [...addedParts, ...deletedParts];
|
|
279
287
|
|
|
280
|
-
|
|
288
|
+
if (files.length === 0) {
|
|
289
|
+
fmt.logInfo('Branching current state (no new changes)');
|
|
290
|
+
} else {
|
|
291
|
+
fmt.logInfo(`${chalk.bold(files.length)} file${files.length > 1 ? 's' : ''} to push (${countParts.join(', ')})`);
|
|
292
|
+
}
|
|
281
293
|
|
|
282
294
|
if (dryRun) {
|
|
283
295
|
for (const f of files) {
|
|
@@ -416,23 +428,42 @@ export function pushCommand(args) {
|
|
|
416
428
|
...changes.deleted.map(e => ({ ...e, deleted: true })),
|
|
417
429
|
];
|
|
418
430
|
|
|
431
|
+
if (allEntries.length === 0 && worktreeFlow && commitsAheadOfMain(awHome) > 0) {
|
|
432
|
+
fmt.logInfo(`${chalk.dim('mode:')} auto (no new changes — branching current state)`);
|
|
433
|
+
doPush([], awHome, dryRun, worktreeFlow, false);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
419
437
|
if (allEntries.length === 0) {
|
|
420
438
|
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
439
|
return;
|
|
422
440
|
}
|
|
423
441
|
|
|
424
|
-
const files = allEntries
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
442
|
+
const files = allEntries
|
|
443
|
+
.filter(f => parseRegistryPath(f.registryPath) !== null)
|
|
444
|
+
.map(f => {
|
|
445
|
+
const meta = parseRegistryPath(f.registryPath);
|
|
446
|
+
const parts = f.registryPath.split('/');
|
|
447
|
+
return {
|
|
448
|
+
absPath: join(awHome, f.path),
|
|
449
|
+
registryTarget: f.path,
|
|
450
|
+
type: meta?.type ?? 'file',
|
|
451
|
+
namespace: meta?.namespace ?? parts.slice(0, -1).join('/'),
|
|
452
|
+
slug: meta?.slug ?? parts[parts.length - 1].replace(/\.md$/, ''),
|
|
453
|
+
deleted: f.deleted,
|
|
454
|
+
};
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
if (files.length === 0) {
|
|
458
|
+
// In worktree flow, still push if there are commits ahead of main not yet in a PR
|
|
459
|
+
if (worktreeFlow && commitsAheadOfMain(awHome) > 0) {
|
|
460
|
+
fmt.logInfo(`${chalk.dim('mode:')} auto (no new changes — branching current state)`);
|
|
461
|
+
doPush([], awHome, dryRun, worktreeFlow, false);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
fmt.cancel('Nothing to push — no staged or modified files.\n\n Stage files in your IDE or use `aw status` to see changes.');
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
436
467
|
|
|
437
468
|
fmt.logInfo(`${chalk.dim('mode:')} auto (${files.length} file${files.length > 1 ? 's' : ''} — stage specific files to push a subset)`);
|
|
438
469
|
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,21 @@ 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
|
+
|
|
382
400
|
/**
|
|
383
401
|
* Get the current branch name in awHome.
|
|
384
402
|
*/
|