@ghl-ai/aw 0.1.33 → 0.1.34-beta.2
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 +7 -5
- package/commands/pull.mjs +2 -2
- package/commands/push.mjs +327 -134
- package/commands/status.mjs +23 -1
- package/constants.mjs +1 -1
- package/manifest.mjs +4 -2
- package/package.json +1 -1
- package/paths.mjs +6 -1
- package/plan.mjs +4 -1
package/cli.mjs
CHANGED
|
@@ -82,9 +82,9 @@ function printHelp() {
|
|
|
82
82
|
cmd('aw pull --dry-run <path>', 'Preview what would be downloaded'),
|
|
83
83
|
|
|
84
84
|
sec('Upload'),
|
|
85
|
-
cmd('aw push', '
|
|
86
|
-
cmd('aw push <path>', '
|
|
87
|
-
cmd('aw push --dry-run
|
|
85
|
+
cmd('aw push', 'Push all modified files (creates one PR)'),
|
|
86
|
+
cmd('aw push <path>', 'Push file, folder, or namespace to registry'),
|
|
87
|
+
cmd('aw push --dry-run [path]', 'Preview what would be pushed'),
|
|
88
88
|
|
|
89
89
|
sec('Discover'),
|
|
90
90
|
cmd('aw search <query>', 'Find agents & skills (local + registry)'),
|
|
@@ -108,8 +108,10 @@ function printHelp() {
|
|
|
108
108
|
cmd('aw pull <team>/skills/<name>', 'One specific skill folder'),
|
|
109
109
|
'',
|
|
110
110
|
` ${chalk.dim('# Push your local changes to registry')}`,
|
|
111
|
-
cmd('aw push
|
|
112
|
-
cmd('aw push .aw_registry
|
|
111
|
+
cmd('aw push', 'Push all modified files (one PR)'),
|
|
112
|
+
cmd('aw push .aw_registry/<team>/', 'Push entire namespace (one PR)'),
|
|
113
|
+
cmd('aw push .aw_registry/agents/<name>.md', 'Push a single agent'),
|
|
114
|
+
cmd('aw push .aw_registry/skills/<name>/', 'Push a single skill folder'),
|
|
113
115
|
'',
|
|
114
116
|
` ${chalk.dim('# Remove content from workspace')}`,
|
|
115
117
|
cmd('aw drop <team>', 'Stop syncing a namespace (removes all files)'),
|
package/commands/pull.mjs
CHANGED
|
@@ -178,7 +178,7 @@ export async function pullCommand(args) {
|
|
|
178
178
|
const s2 = log.spinner();
|
|
179
179
|
s2.start('Applying changes...');
|
|
180
180
|
const conflictCount = applyActions(actions, { teamNS: teamNSOverride || renameNamespace || undefined });
|
|
181
|
-
updateManifest(workspaceDir, actions, cfg.namespace);
|
|
181
|
+
updateManifest(workspaceDir, actions, cfg.namespace, { fromTemplate: !!renameNamespace });
|
|
182
182
|
s2.stop('Changes applied');
|
|
183
183
|
|
|
184
184
|
// Copy root-level registry files (e.g. AW-PROTOCOL.md) that are fetched via
|
|
@@ -282,7 +282,7 @@ export async function pullAsync(args) {
|
|
|
282
282
|
const { actions: rawActions } = computePlan(registryDirs, workspaceDir, [pattern], { skipOrphans: true });
|
|
283
283
|
const actions = filterActions(rawActions, pattern);
|
|
284
284
|
const conflictCount = applyActions(actions, { teamNS: teamNSOverride || renameNamespace || undefined });
|
|
285
|
-
updateManifest(workspaceDir, actions, cfg.namespace);
|
|
285
|
+
updateManifest(workspaceDir, actions, cfg.namespace, { fromTemplate: !!renameNamespace });
|
|
286
286
|
|
|
287
287
|
const ROOT_REGISTRY_FILES = ['AW-PROTOCOL.md'];
|
|
288
288
|
for (const fname of ROOT_REGISTRY_FILES) {
|
package/commands/push.mjs
CHANGED
|
@@ -1,120 +1,193 @@
|
|
|
1
|
-
// commands/push.mjs — Push local
|
|
1
|
+
// commands/push.mjs — Push local agents/skills to registry via PR (single file or batch)
|
|
2
2
|
|
|
3
|
-
import { existsSync, statSync, mkdirSync, cpSync, mkdtempSync, readFileSync, appendFileSync } from 'node:fs';
|
|
4
|
-
import { basename, dirname, resolve, join } from 'node:path';
|
|
3
|
+
import { existsSync, statSync, mkdirSync, cpSync, mkdtempSync, readFileSync, appendFileSync, readdirSync } from 'node:fs';
|
|
4
|
+
import { basename, dirname, resolve, join, relative } from 'node:path';
|
|
5
5
|
import { execSync, execFileSync } from 'node:child_process';
|
|
6
6
|
import { tmpdir } from 'node:os';
|
|
7
7
|
import * as fmt from '../fmt.mjs';
|
|
8
8
|
import { chalk } from '../fmt.mjs';
|
|
9
9
|
import { REGISTRY_REPO, REGISTRY_BASE_BRANCH, REGISTRY_DIR } from '../constants.mjs';
|
|
10
10
|
import { resolveInput } from '../paths.mjs';
|
|
11
|
-
import { load as loadManifest } from '../manifest.mjs';
|
|
12
|
-
import { hashFile } from '../registry.mjs';
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
11
|
+
import { load as loadManifest, save as saveManifest } from '../manifest.mjs';
|
|
12
|
+
import { hashFile, walkRegistryTree } from '../registry.mjs';
|
|
13
|
+
|
|
14
|
+
const PUSHABLE_TYPES = ['agents', 'skills', 'commands', 'evals'];
|
|
15
|
+
|
|
16
|
+
// ── Batch file collection ────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Collect all pushable files under a local .aw_registry folder.
|
|
20
|
+
* Returns array of { absPath, registryTarget, type, namespace, slug, isDir }.
|
|
21
|
+
*/
|
|
22
|
+
function collectBatchFiles(folderAbsPath, workspaceDir) {
|
|
23
|
+
// Determine the namespace name from path relative to workspaceDir
|
|
24
|
+
const relPath = relative(workspaceDir, folderAbsPath);
|
|
25
|
+
const nameSegments = relPath.split('/');
|
|
26
|
+
const baseName = nameSegments[0];
|
|
27
|
+
|
|
28
|
+
const entries = walkRegistryTree(folderAbsPath, baseName);
|
|
29
|
+
return entries.map(entry => {
|
|
30
|
+
// Build the registry target path for git
|
|
31
|
+
const registryTarget = entry.type === 'skills'
|
|
32
|
+
? `${REGISTRY_DIR}/${entry.namespacePath}/${entry.type}/${entry.slug}/${entry.skillRelPath || entry.filename}`
|
|
33
|
+
: `${REGISTRY_DIR}/${entry.namespacePath}/${entry.type}/${entry.filename}`;
|
|
34
|
+
return {
|
|
35
|
+
absPath: entry.sourcePath,
|
|
36
|
+
registryTarget,
|
|
37
|
+
type: entry.type,
|
|
38
|
+
namespace: entry.namespacePath,
|
|
39
|
+
slug: entry.slug,
|
|
40
|
+
isDir: false,
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
}
|
|
22
44
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Collect all modified files from manifest (for no-args push).
|
|
47
|
+
* Returns array of { absPath, registryTarget, type, namespace, slug, isDir }.
|
|
48
|
+
*/
|
|
49
|
+
function collectModifiedFiles(workspaceDir) {
|
|
50
|
+
const manifest = loadManifest(workspaceDir);
|
|
51
|
+
const files = [];
|
|
52
|
+
for (const [key, entry] of Object.entries(manifest.files || {})) {
|
|
53
|
+
const filePath = join(workspaceDir, key);
|
|
54
|
+
if (!existsSync(filePath)) continue;
|
|
55
|
+
const currentHash = hashFile(filePath);
|
|
56
|
+
const isModified = currentHash !== entry.sha256;
|
|
57
|
+
const isNew = !entry.registry_sha256; // Template-derived, never pushed to remote
|
|
58
|
+
if (isModified || isNew) {
|
|
59
|
+
const meta = parseManifestKey(key);
|
|
60
|
+
if (meta) {
|
|
61
|
+
files.push({
|
|
62
|
+
absPath: filePath,
|
|
63
|
+
registryTarget: `${REGISTRY_DIR}/${key}`,
|
|
64
|
+
type: meta.type,
|
|
65
|
+
namespace: meta.namespace,
|
|
66
|
+
slug: meta.slug,
|
|
67
|
+
isDir: false,
|
|
68
|
+
});
|
|
33
69
|
}
|
|
34
70
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
71
|
+
}
|
|
72
|
+
return files;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Parse a manifest key like "commerce/agents/unit-tester.md" into { type, namespace, slug }.
|
|
77
|
+
*/
|
|
78
|
+
function parseManifestKey(key) {
|
|
79
|
+
const parts = key.split('/');
|
|
80
|
+
for (let i = 0; i < parts.length; i++) {
|
|
81
|
+
if (PUSHABLE_TYPES.includes(parts[i]) && i + 1 < parts.length) {
|
|
82
|
+
return {
|
|
83
|
+
type: parts[i],
|
|
84
|
+
namespace: parts.slice(0, i).join('/'),
|
|
85
|
+
slug: parts[i + 1].replace(/\.md$/, ''),
|
|
86
|
+
};
|
|
41
87
|
}
|
|
42
|
-
fmt.cancel(`\nSpecify which file to push:\n aw push .aw_registry/${modified[0]}`);
|
|
43
88
|
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
44
91
|
|
|
45
|
-
|
|
46
|
-
const resolved = resolveInput(input, workspaceDir);
|
|
92
|
+
// ── PR content generation ────────────────────────────────────────────
|
|
47
93
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
94
|
+
function generateBranchName(files) {
|
|
95
|
+
const shortId = Date.now().toString(36).slice(-5);
|
|
96
|
+
const namespaces = [...new Set(files.map(f => f.namespace))];
|
|
97
|
+
|
|
98
|
+
if (files.length === 1) {
|
|
99
|
+
const f = files[0];
|
|
100
|
+
const nsSlug = f.namespace.replace(/\//g, '-');
|
|
101
|
+
return `upload/${nsSlug}-${f.type}-${f.slug}-${shortId}`;
|
|
53
102
|
}
|
|
54
103
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (!absPath || !existsSync(absPath)) {
|
|
58
|
-
// Try resolving with .md extension for flat files
|
|
59
|
-
if (absPath && !absPath.endsWith('.md') && existsSync(absPath + '.md')) {
|
|
60
|
-
absPath = absPath + '.md';
|
|
61
|
-
} else {
|
|
62
|
-
fmt.cancel(`File not found: ${absPath || input}\n\n Only files inside .aw_registry/ can be pushed.\n Use ${chalk.dim('aw status')} to see modified files.`);
|
|
63
|
-
}
|
|
104
|
+
if (namespaces.length === 1) {
|
|
105
|
+
return `sync/${namespaces[0].replace(/\//g, '-')}-${shortId}`;
|
|
64
106
|
}
|
|
65
107
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (PUSHABLE_TYPES.includes(regParts[i])) { typeIdx = i; break; }
|
|
108
|
+
return `sync/batch-${shortId}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function generatePrTitle(files) {
|
|
112
|
+
if (files.length === 1) {
|
|
113
|
+
const f = files[0];
|
|
114
|
+
return `Add ${f.slug} (${f.type}) to ${f.namespace}`;
|
|
74
115
|
}
|
|
75
116
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
117
|
+
const counts = {};
|
|
118
|
+
for (const f of files) {
|
|
119
|
+
counts[f.type] = (counts[f.type] || 0) + 1;
|
|
120
|
+
}
|
|
121
|
+
const countParts = Object.entries(counts).map(([type, count]) => `${count} ${type}`);
|
|
122
|
+
const namespaces = [...new Set(files.map(f => f.namespace))];
|
|
123
|
+
|
|
124
|
+
if (namespaces.length === 1) {
|
|
125
|
+
return `sync: ${countParts.join(', ')} in ${namespaces[0]}`;
|
|
126
|
+
}
|
|
127
|
+
return `sync: ${countParts.join(', ')} across ${namespaces.join(', ')}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function generatePrBody(files, newNamespaces) {
|
|
131
|
+
if (files.length === 1) {
|
|
132
|
+
const f = files[0];
|
|
133
|
+
const bodyParts = [
|
|
134
|
+
'## Registry Upload',
|
|
81
135
|
'',
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
]
|
|
136
|
+
`- **Type:** ${f.type}`,
|
|
137
|
+
`- **Slug:** ${f.slug}`,
|
|
138
|
+
`- **Namespace:** ${f.namespace}`,
|
|
139
|
+
`- **Path:** \`${f.registryTarget}\``,
|
|
140
|
+
];
|
|
141
|
+
if (newNamespaces.length > 0) {
|
|
142
|
+
bodyParts.push('', '> **New namespace** — CODEOWNERS entry auto-added.');
|
|
143
|
+
}
|
|
144
|
+
bodyParts.push('', 'Uploaded via `aw push`');
|
|
145
|
+
return bodyParts.join('\n');
|
|
87
146
|
}
|
|
88
147
|
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
148
|
+
// Batch body — group by type
|
|
149
|
+
const grouped = {};
|
|
150
|
+
for (const f of files) {
|
|
151
|
+
if (!grouped[f.type]) grouped[f.type] = [];
|
|
152
|
+
grouped[f.type].push(f);
|
|
153
|
+
}
|
|
94
154
|
|
|
95
|
-
|
|
96
|
-
|
|
155
|
+
const bodyParts = ['## Registry Sync', ''];
|
|
156
|
+
for (const [type, items] of Object.entries(grouped)) {
|
|
157
|
+
bodyParts.push(`### ${type}`);
|
|
158
|
+
for (const item of items.sort((a, b) => a.slug.localeCompare(b.slug))) {
|
|
159
|
+
bodyParts.push(`- \`${item.slug}\` (${item.namespace})`);
|
|
160
|
+
}
|
|
161
|
+
bodyParts.push('');
|
|
97
162
|
}
|
|
98
163
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
164
|
+
if (newNamespaces.length > 0) {
|
|
165
|
+
bodyParts.push(`> **New namespace${newNamespaces.length > 1 ? 's' : ''}:** ${newNamespaces.join(', ')} — CODEOWNERS entr${newNamespaces.length > 1 ? 'ies' : 'y'} auto-added.`);
|
|
166
|
+
bodyParts.push('');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
bodyParts.push(`---`, `Synced via \`aw push\` (${files.length} files)`);
|
|
170
|
+
return bodyParts.join('\n');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── Git pipeline (works for single file or batch) ────────────────────
|
|
103
174
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
'Upload mapping'
|
|
113
|
-
);
|
|
175
|
+
function pushFiles(files, { repo, dryRun, workspaceDir }) {
|
|
176
|
+
// Summary
|
|
177
|
+
const counts = {};
|
|
178
|
+
for (const f of files) {
|
|
179
|
+
counts[f.type] = (counts[f.type] || 0) + 1;
|
|
180
|
+
}
|
|
181
|
+
const countParts = Object.entries(counts).map(([type, count]) => `${count} ${type}`);
|
|
182
|
+
fmt.logInfo(`${chalk.bold(files.length)} file${files.length > 1 ? 's' : ''} to push (${countParts.join(', ')})`);
|
|
114
183
|
|
|
115
184
|
if (dryRun) {
|
|
185
|
+
for (const f of files) {
|
|
186
|
+
const ns = chalk.dim(` [${f.namespace}]`);
|
|
187
|
+
fmt.logMessage(` ${chalk.yellow(f.type)}/${f.slug}${ns}`);
|
|
188
|
+
}
|
|
116
189
|
fmt.logWarn('No changes made (--dry-run)');
|
|
117
|
-
fmt.outro(chalk.dim('Remove --dry-run to
|
|
190
|
+
fmt.outro(chalk.dim('Remove --dry-run to push'));
|
|
118
191
|
return;
|
|
119
192
|
}
|
|
120
193
|
|
|
@@ -129,43 +202,59 @@ export function pushCommand(args) {
|
|
|
129
202
|
execSync(`git checkout ${REGISTRY_BASE_BRANCH}`, { cwd: tempDir, stdio: 'pipe' });
|
|
130
203
|
s.stop('Repository cloned');
|
|
131
204
|
|
|
132
|
-
const
|
|
133
|
-
const branch = `upload/${namespacePath.replace(/\//g, '-')}-${parentDir}-${slug}-${shortId}`;
|
|
205
|
+
const branch = generateBranchName(files);
|
|
134
206
|
execSync(`git checkout -b ${branch}`, { cwd: tempDir, stdio: 'pipe' });
|
|
135
207
|
|
|
136
208
|
const s2 = fmt.spinner();
|
|
137
|
-
s2.start('
|
|
209
|
+
s2.start(`Copying ${files.length} file${files.length > 1 ? 's' : ''} to registry...`);
|
|
210
|
+
|
|
211
|
+
// Copy each file
|
|
212
|
+
let copyErrors = [];
|
|
213
|
+
for (const file of files) {
|
|
214
|
+
try {
|
|
215
|
+
const targetFull = join(tempDir, file.registryTarget);
|
|
216
|
+
mkdirSync(dirname(targetFull), { recursive: true });
|
|
217
|
+
if (file.isDir) {
|
|
218
|
+
cpSync(file.absPath, targetFull, { recursive: true });
|
|
219
|
+
} else {
|
|
220
|
+
cpSync(file.absPath, targetFull);
|
|
221
|
+
}
|
|
222
|
+
} catch (e) {
|
|
223
|
+
copyErrors.push({ file: file.registryTarget, error: e.message });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
138
226
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
cpSync(absPath, targetFull, { recursive: true });
|
|
144
|
-
} else {
|
|
145
|
-
mkdirSync(dirname(targetFull), { recursive: true });
|
|
146
|
-
cpSync(absPath, targetFull);
|
|
227
|
+
if (copyErrors.length > 0) {
|
|
228
|
+
for (const err of copyErrors) {
|
|
229
|
+
fmt.logWarn(`Failed to copy ${err.file}: ${err.error}`);
|
|
230
|
+
}
|
|
147
231
|
}
|
|
148
232
|
|
|
149
|
-
// Check
|
|
150
|
-
|
|
151
|
-
const nsDir = join(tempDir, REGISTRY_DIR, topNamespace);
|
|
233
|
+
// Check for new namespaces — auto-add CODEOWNERS entries
|
|
234
|
+
const topNamespaces = [...new Set(files.map(f => f.namespace.split('/')[0]))];
|
|
152
235
|
const codeownersPath = join(tempDir, 'CODEOWNERS');
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
236
|
+
const newNamespaces = [];
|
|
237
|
+
for (const ns of topNamespaces) {
|
|
238
|
+
if (isNewNamespaceInCodeowners(codeownersPath, ns)) {
|
|
239
|
+
newNamespaces.push(ns);
|
|
240
|
+
const ghUser = getGitHubUser();
|
|
241
|
+
if (ghUser && existsSync(codeownersPath)) {
|
|
242
|
+
const line = `/${REGISTRY_DIR}/${ns}/ @${ghUser}\n`;
|
|
243
|
+
appendFileSync(codeownersPath, line);
|
|
244
|
+
}
|
|
160
245
|
}
|
|
161
246
|
}
|
|
247
|
+
if (newNamespaces.length > 0) {
|
|
248
|
+
execSync('git add CODEOWNERS', { cwd: tempDir, stdio: 'pipe' });
|
|
249
|
+
}
|
|
162
250
|
|
|
163
|
-
// Stage
|
|
164
|
-
execSync(`git add "${
|
|
251
|
+
// Stage all registry changes
|
|
252
|
+
execSync(`git add "${REGISTRY_DIR}"`, { cwd: tempDir, stdio: 'pipe' });
|
|
165
253
|
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
254
|
+
const prTitle = generatePrTitle(files);
|
|
255
|
+
const commitMsg = files.length === 1
|
|
256
|
+
? `registry: add ${files[0].type}/${files[0].slug} to ${files[0].namespace}`
|
|
257
|
+
: `registry: sync ${files.length} files (${countParts.join(', ')})`;
|
|
169
258
|
execSync(`git commit -m "${commitMsg}"`, { cwd: tempDir, stdio: 'pipe' });
|
|
170
259
|
|
|
171
260
|
s2.stop('Upload prepared');
|
|
@@ -175,21 +264,7 @@ export function pushCommand(args) {
|
|
|
175
264
|
|
|
176
265
|
execSync(`git push -u origin ${branch}`, { cwd: tempDir, stdio: 'pipe' });
|
|
177
266
|
|
|
178
|
-
const
|
|
179
|
-
'## Registry Upload',
|
|
180
|
-
'',
|
|
181
|
-
`- **Type:** ${parentDir}`,
|
|
182
|
-
`- **Slug:** ${slug}`,
|
|
183
|
-
`- **Namespace:** ${namespacePath}`,
|
|
184
|
-
`- **Path:** \`${registryTarget}\``,
|
|
185
|
-
];
|
|
186
|
-
if (newNamespace) {
|
|
187
|
-
bodyParts.push('', '> **New namespace** — CODEOWNERS entry auto-added.');
|
|
188
|
-
}
|
|
189
|
-
bodyParts.push('', 'Uploaded via `aw push`');
|
|
190
|
-
|
|
191
|
-
const prTitle = `Add ${slug} (${parentDir}) to ${namespacePath}`;
|
|
192
|
-
const prBody = bodyParts.join('\n');
|
|
267
|
+
const prBody = generatePrBody(files, newNamespaces);
|
|
193
268
|
|
|
194
269
|
// Try gh for PR creation, fall back to manual URL
|
|
195
270
|
let prUrl;
|
|
@@ -201,27 +276,145 @@ export function pushCommand(args) {
|
|
|
201
276
|
'--body', prBody,
|
|
202
277
|
], { cwd: tempDir, encoding: 'utf8' }).trim();
|
|
203
278
|
} catch {
|
|
204
|
-
// gh not available — construct PR URL manually
|
|
205
279
|
const repoBase = repo.replace(/\.git$/, '');
|
|
206
280
|
prUrl = `https://github.com/${repoBase}/compare/${REGISTRY_BASE_BRANCH}...${branch}?expand=1`;
|
|
207
281
|
}
|
|
208
282
|
|
|
209
283
|
s3.stop('Branch pushed');
|
|
210
284
|
|
|
211
|
-
|
|
212
|
-
|
|
285
|
+
// Print summary
|
|
286
|
+
if (newNamespaces.length > 0) {
|
|
287
|
+
fmt.logInfo(`New namespace${newNamespaces.length > 1 ? 's' : ''} ${chalk.cyan(newNamespaces.join(', '))} — CODEOWNERS entr${newNamespaces.length > 1 ? 'ies' : 'y'} added`);
|
|
213
288
|
}
|
|
289
|
+
if (files.length > 1) {
|
|
290
|
+
for (const [type, items] of Object.entries(groupBy(files, 'type'))) {
|
|
291
|
+
fmt.logSuccess(`${items.length} ${type} pushed`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// Update manifest — mark pushed files as synced (set registry_sha256 = sha256)
|
|
295
|
+
if (workspaceDir) {
|
|
296
|
+
const manifest = loadManifest(workspaceDir);
|
|
297
|
+
for (const file of files) {
|
|
298
|
+
// Convert registryTarget back to manifest key (strip REGISTRY_DIR/ prefix)
|
|
299
|
+
const manifestKey = file.registryTarget.replace(`${REGISTRY_DIR}/`, '');
|
|
300
|
+
if (manifest.files[manifestKey]) {
|
|
301
|
+
manifest.files[manifestKey].registry_sha256 = manifest.files[manifestKey].sha256;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
saveManifest(workspaceDir, manifest);
|
|
305
|
+
}
|
|
306
|
+
|
|
214
307
|
fmt.logSuccess(`PR: ${chalk.cyan(prUrl)}`);
|
|
215
|
-
fmt.outro('
|
|
308
|
+
fmt.outro('Push complete');
|
|
216
309
|
} catch (e) {
|
|
217
|
-
fmt.cancel(`
|
|
310
|
+
fmt.cancel(`Push failed: ${e.message}`);
|
|
218
311
|
} finally {
|
|
219
312
|
execSync(`rm -rf "${tempDir}"`, { stdio: 'pipe' });
|
|
220
313
|
}
|
|
221
314
|
}
|
|
222
315
|
|
|
316
|
+
function groupBy(arr, key) {
|
|
317
|
+
const result = {};
|
|
318
|
+
for (const item of arr) {
|
|
319
|
+
const k = item[key];
|
|
320
|
+
if (!result[k]) result[k] = [];
|
|
321
|
+
result[k].push(item);
|
|
322
|
+
}
|
|
323
|
+
return result;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ── Main command ─────────────────────────────────────────────────────
|
|
327
|
+
|
|
328
|
+
export function pushCommand(args) {
|
|
329
|
+
const input = args._positional?.[0];
|
|
330
|
+
const dryRun = args['--dry-run'] === true;
|
|
331
|
+
const repo = args['--repo'] || REGISTRY_REPO;
|
|
332
|
+
const cwd = process.cwd();
|
|
333
|
+
const workspaceDir = join(cwd, '.aw_registry');
|
|
334
|
+
|
|
335
|
+
fmt.intro('aw push');
|
|
336
|
+
|
|
337
|
+
// No args = push all modified + new (unpushed) files
|
|
338
|
+
if (!input) {
|
|
339
|
+
const files = collectModifiedFiles(workspaceDir);
|
|
340
|
+
if (files.length === 0) {
|
|
341
|
+
fmt.cancel('Nothing to push — no modified or new files.\n\n Use `aw status` to see synced files.');
|
|
342
|
+
}
|
|
343
|
+
pushFiles(files, { repo, dryRun, workspaceDir });
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Resolve input — accept both registry path and local path
|
|
348
|
+
const resolved = resolveInput(input, workspaceDir);
|
|
349
|
+
|
|
350
|
+
if (!resolved.registryPath) {
|
|
351
|
+
const hint = input.startsWith('.claude/') || input.startsWith('.cursor/') || input.startsWith('.codex/')
|
|
352
|
+
? `\n\n Tip: Use the .aw_registry/ path instead:\n aw push .aw_registry/${input.split('/').slice(1).join('/')}`
|
|
353
|
+
: '';
|
|
354
|
+
fmt.cancel(`Could not resolve "${input}" to a registry path.${hint}\n\n Only files inside .aw_registry/ can be pushed.`);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
let absPath = resolved.localAbsPath;
|
|
358
|
+
if (!absPath || !existsSync(absPath)) {
|
|
359
|
+
if (absPath && !absPath.endsWith('.md') && existsSync(absPath + '.md')) {
|
|
360
|
+
absPath = absPath + '.md';
|
|
361
|
+
} else {
|
|
362
|
+
fmt.cancel(`Path not found: ${absPath || input}\n\n Only files inside .aw_registry/ can be pushed.\n Use ${chalk.dim('aw status')} to see modified files.`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Folder/namespace input → batch push
|
|
367
|
+
if (statSync(absPath).isDirectory()) {
|
|
368
|
+
const files = collectBatchFiles(absPath, workspaceDir);
|
|
369
|
+
if (files.length === 0) {
|
|
370
|
+
fmt.cancel(`Nothing to push in ${chalk.cyan(input)} — no agents, skills, commands, or evals found.`);
|
|
371
|
+
}
|
|
372
|
+
pushFiles(files, { repo, dryRun, workspaceDir });
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Single file input → wrap in array and push
|
|
377
|
+
const regParts = resolved.registryPath.split('/');
|
|
378
|
+
let typeIdx = -1;
|
|
379
|
+
for (let i = regParts.length - 1; i >= 0; i--) {
|
|
380
|
+
if (PUSHABLE_TYPES.includes(regParts[i])) { typeIdx = i; break; }
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (typeIdx === -1 || typeIdx + 1 >= regParts.length) {
|
|
384
|
+
fmt.cancel([
|
|
385
|
+
`Invalid push path: ${chalk.red(resolved.registryPath)}`,
|
|
386
|
+
'',
|
|
387
|
+
` Content must live under an ${chalk.bold('agents/')}, ${chalk.bold('skills/')}, ${chalk.bold('commands/')}, or ${chalk.bold('evals/')} directory.`,
|
|
388
|
+
'',
|
|
389
|
+
` ${chalk.dim('Valid examples:')}`,
|
|
390
|
+
` aw push .aw_registry/commerce/quality/agents/unit-tester.md`,
|
|
391
|
+
` aw push .aw_registry/platform/services/skills/development`,
|
|
392
|
+
` aw push .aw_registry/commerce/shared/commands/ship.md`,
|
|
393
|
+
].join('\n'));
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const namespaceParts = regParts.slice(0, typeIdx);
|
|
397
|
+
const parentDir = regParts[typeIdx];
|
|
398
|
+
const slug = regParts[typeIdx + 1];
|
|
399
|
+
const namespacePath = namespaceParts.join('/');
|
|
400
|
+
const isDir = statSync(absPath).isDirectory();
|
|
401
|
+
const registryTarget = isDir
|
|
402
|
+
? `${REGISTRY_DIR}/${namespacePath}/${parentDir}/${slug}`
|
|
403
|
+
: `${REGISTRY_DIR}/${namespacePath}/${parentDir}/${slug}.md`;
|
|
404
|
+
|
|
405
|
+
pushFiles([{
|
|
406
|
+
absPath,
|
|
407
|
+
registryTarget,
|
|
408
|
+
type: parentDir,
|
|
409
|
+
namespace: namespacePath,
|
|
410
|
+
slug,
|
|
411
|
+
isDir,
|
|
412
|
+
}], { repo, dryRun, workspaceDir });
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ── Helpers ──────────────────────────────────────────────────────────
|
|
416
|
+
|
|
223
417
|
function getGitHubUser() {
|
|
224
|
-
// Try gh first, fall back to git config
|
|
225
418
|
try {
|
|
226
419
|
return execSync('gh api user --jq .login', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
227
420
|
} catch {
|
package/commands/status.mjs
CHANGED
|
@@ -39,6 +39,7 @@ export function statusCommand(args) {
|
|
|
39
39
|
|
|
40
40
|
const files = Object.entries(manifest.files || {});
|
|
41
41
|
const modified = [];
|
|
42
|
+
const unpushed = [];
|
|
42
43
|
const missing = [];
|
|
43
44
|
const conflicts = [];
|
|
44
45
|
|
|
@@ -51,6 +52,12 @@ export function statusCommand(args) {
|
|
|
51
52
|
|
|
52
53
|
const currentHash = hashFile(filePath);
|
|
53
54
|
|
|
55
|
+
// Template-derived files that were never pushed to the remote registry
|
|
56
|
+
if (!entry.registry_sha256) {
|
|
57
|
+
unpushed.push(key);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
54
61
|
if (currentHash !== entry.sha256) {
|
|
55
62
|
const content = readFileSync(filePath, 'utf8');
|
|
56
63
|
if (content.includes('<<<<<<<')) {
|
|
@@ -64,6 +71,7 @@ export function statusCommand(args) {
|
|
|
64
71
|
// Summary line
|
|
65
72
|
const summaryParts = [
|
|
66
73
|
`${files.length} synced`,
|
|
74
|
+
unpushed.length > 0 ? chalk.green(`${unpushed.length} new (unpushed)`) : null,
|
|
67
75
|
modified.length > 0 ? chalk.yellow(`${modified.length} modified`) : null,
|
|
68
76
|
conflicts.length > 0 ? chalk.red(`${conflicts.length} conflicts`) : null,
|
|
69
77
|
missing.length > 0 ? chalk.dim(`${missing.length} missing`) : null,
|
|
@@ -78,6 +86,17 @@ export function statusCommand(args) {
|
|
|
78
86
|
);
|
|
79
87
|
}
|
|
80
88
|
|
|
89
|
+
if (unpushed.length > 0) {
|
|
90
|
+
// Group by namespace for cleaner display
|
|
91
|
+
const nsCounts = {};
|
|
92
|
+
for (const key of unpushed) {
|
|
93
|
+
const ns = key.split('/').slice(0, 2).join('/');
|
|
94
|
+
nsCounts[ns] = (nsCounts[ns] || 0) + 1;
|
|
95
|
+
}
|
|
96
|
+
const nsLines = Object.entries(nsCounts).map(([ns, count]) => chalk.green(` ${ns}: ${count} files`)).join('\n');
|
|
97
|
+
fmt.note(nsLines, chalk.green('New (unpushed)'));
|
|
98
|
+
}
|
|
99
|
+
|
|
81
100
|
if (modified.length > 0) {
|
|
82
101
|
fmt.note(
|
|
83
102
|
modified.map(m => chalk.yellow(m)).join('\n'),
|
|
@@ -92,7 +111,7 @@ export function statusCommand(args) {
|
|
|
92
111
|
);
|
|
93
112
|
}
|
|
94
113
|
|
|
95
|
-
if (modified.length === 0 && conflicts.length === 0 && missing.length === 0) {
|
|
114
|
+
if (modified.length === 0 && conflicts.length === 0 && missing.length === 0 && unpushed.length === 0) {
|
|
96
115
|
fmt.logSuccess('Workspace is clean');
|
|
97
116
|
}
|
|
98
117
|
|
|
@@ -100,6 +119,9 @@ export function statusCommand(args) {
|
|
|
100
119
|
if (conflicts.length > 0) {
|
|
101
120
|
fmt.logWarn(`Fix conflicts: ${chalk.dim('grep -r "<<<<<<< " .aw_registry/')}`);
|
|
102
121
|
}
|
|
122
|
+
if (unpushed.length > 0) {
|
|
123
|
+
fmt.logInfo(`Push new files: ${chalk.dim('aw push')} or ${chalk.dim('aw push <namespace>')}`);
|
|
124
|
+
}
|
|
103
125
|
if (modified.length > 0) {
|
|
104
126
|
fmt.logInfo(`Push changes: ${chalk.dim('aw push <path>')}`);
|
|
105
127
|
}
|
package/constants.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// constants.mjs — Single source of truth for registry settings.
|
|
2
2
|
|
|
3
3
|
/** Base branch for PRs and sync checkout */
|
|
4
|
-
export const REGISTRY_BASE_BRANCH = '
|
|
4
|
+
export const REGISTRY_BASE_BRANCH = 'feat/aw-push-batch';
|
|
5
5
|
|
|
6
6
|
/** Default registry repository */
|
|
7
7
|
export const REGISTRY_REPO = 'GoHighLevel/platform-docs';
|
package/manifest.mjs
CHANGED
|
@@ -24,7 +24,7 @@ export function save(workspaceDir, manifest) {
|
|
|
24
24
|
writeFileSync(manifestPath(workspaceDir), JSON.stringify(manifest, null, 2) + '\n');
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
export function update(workspaceDir, actions, namespaceName) {
|
|
27
|
+
export function update(workspaceDir, actions, namespaceName, opts = {}) {
|
|
28
28
|
const manifest = load(workspaceDir);
|
|
29
29
|
manifest.synced_at = new Date().toISOString();
|
|
30
30
|
manifest.namespace = namespaceName || manifest.namespace;
|
|
@@ -38,7 +38,9 @@ export function update(workspaceDir, actions, namespaceName) {
|
|
|
38
38
|
manifest.files[manifestKey] = {
|
|
39
39
|
sha256: hashFile(act.targetPath),
|
|
40
40
|
source: act.source,
|
|
41
|
-
|
|
41
|
+
// When pulled from template (renamed namespace), these files don't exist
|
|
42
|
+
// in the remote registry yet — mark as null so push detects them as new.
|
|
43
|
+
registry_sha256: opts.fromTemplate ? null : act.registryHash,
|
|
42
44
|
};
|
|
43
45
|
}
|
|
44
46
|
} else if (act.action === 'CONFLICT') {
|
package/package.json
CHANGED
package/paths.mjs
CHANGED
|
@@ -89,20 +89,25 @@ export function resolveInput(input, workspaceDir) {
|
|
|
89
89
|
if (rewritten.startsWith('.aw_registry/') || rewritten.startsWith('.aw_registry\\')) {
|
|
90
90
|
const registryPath = localToRegistry(rewritten);
|
|
91
91
|
const absPath = resolve(rewritten);
|
|
92
|
+
const isDirectory = existsSync(absPath) && statSync(absPath).isDirectory();
|
|
92
93
|
return {
|
|
93
94
|
registryPath: registryPath?.replace(/\.md$/, '') || null,
|
|
94
95
|
localAbsPath: absPath,
|
|
95
96
|
isLocalPath: true,
|
|
97
|
+
isDirectory,
|
|
96
98
|
};
|
|
97
99
|
}
|
|
98
100
|
|
|
99
101
|
// Registry path
|
|
100
102
|
const cleanPath = rewritten.replace(/\.md$/, '');
|
|
101
103
|
const localRel = registryToLocal(cleanPath);
|
|
104
|
+
const absPath = localRel ? join(workspaceDir, localRel) : null;
|
|
105
|
+
const isDirectory = absPath && existsSync(absPath) && statSync(absPath).isDirectory();
|
|
102
106
|
return {
|
|
103
107
|
registryPath: cleanPath,
|
|
104
|
-
localAbsPath:
|
|
108
|
+
localAbsPath: absPath,
|
|
105
109
|
isLocalPath: false,
|
|
110
|
+
isDirectory: !!isDirectory,
|
|
106
111
|
};
|
|
107
112
|
}
|
|
108
113
|
|
package/plan.mjs
CHANGED
|
@@ -68,7 +68,10 @@ export function computePlan(registryDirs, workspaceDir, includePatterns = [], {
|
|
|
68
68
|
action = 'UPDATE';
|
|
69
69
|
} else {
|
|
70
70
|
const localChanged = localHash !== manifestEntry.sha256;
|
|
71
|
-
|
|
71
|
+
// registry_sha256 is null for template-derived files (never pushed).
|
|
72
|
+
// Treat null as "no known registry version" — can't conflict with unknown.
|
|
73
|
+
const registryChanged = manifestEntry.registry_sha256 != null
|
|
74
|
+
&& registryHash !== manifestEntry.registry_sha256;
|
|
72
75
|
if (localChanged && registryChanged) {
|
|
73
76
|
action = 'CONFLICT';
|
|
74
77
|
} else if (localChanged) {
|