@ghl-ai/aw 0.1.33 → 0.1.34-beta.1
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/push.mjs +311 -133
- package/package.json +1 -1
- package/paths.mjs +6 -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/push.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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';
|
|
@@ -9,112 +9,183 @@ 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
11
|
import { load as loadManifest } from '../manifest.mjs';
|
|
12
|
-
import { hashFile } from '../registry.mjs';
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
+
if (currentHash !== entry.sha256) {
|
|
57
|
+
const meta = parseManifestKey(key);
|
|
58
|
+
if (meta) {
|
|
59
|
+
files.push({
|
|
60
|
+
absPath: filePath,
|
|
61
|
+
registryTarget: `${REGISTRY_DIR}/${key}`,
|
|
62
|
+
type: meta.type,
|
|
63
|
+
namespace: meta.namespace,
|
|
64
|
+
slug: meta.slug,
|
|
65
|
+
isDir: false,
|
|
66
|
+
});
|
|
33
67
|
}
|
|
34
68
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
69
|
+
}
|
|
70
|
+
return files;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Parse a manifest key like "commerce/agents/unit-tester.md" into { type, namespace, slug }.
|
|
75
|
+
*/
|
|
76
|
+
function parseManifestKey(key) {
|
|
77
|
+
const parts = key.split('/');
|
|
78
|
+
for (let i = 0; i < parts.length; i++) {
|
|
79
|
+
if (PUSHABLE_TYPES.includes(parts[i]) && i + 1 < parts.length) {
|
|
80
|
+
return {
|
|
81
|
+
type: parts[i],
|
|
82
|
+
namespace: parts.slice(0, i).join('/'),
|
|
83
|
+
slug: parts[i + 1].replace(/\.md$/, ''),
|
|
84
|
+
};
|
|
41
85
|
}
|
|
42
|
-
fmt.cancel(`\nSpecify which file to push:\n aw push .aw_registry/${modified[0]}`);
|
|
43
86
|
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
44
89
|
|
|
45
|
-
|
|
46
|
-
const resolved = resolveInput(input, workspaceDir);
|
|
90
|
+
// ── PR content generation ────────────────────────────────────────────
|
|
47
91
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
92
|
+
function generateBranchName(files) {
|
|
93
|
+
const shortId = Date.now().toString(36).slice(-5);
|
|
94
|
+
const namespaces = [...new Set(files.map(f => f.namespace))];
|
|
95
|
+
|
|
96
|
+
if (files.length === 1) {
|
|
97
|
+
const f = files[0];
|
|
98
|
+
const nsSlug = f.namespace.replace(/\//g, '-');
|
|
99
|
+
return `upload/${nsSlug}-${f.type}-${f.slug}-${shortId}`;
|
|
53
100
|
}
|
|
54
101
|
|
|
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
|
-
}
|
|
102
|
+
if (namespaces.length === 1) {
|
|
103
|
+
return `sync/${namespaces[0].replace(/\//g, '-')}-${shortId}`;
|
|
64
104
|
}
|
|
65
105
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (PUSHABLE_TYPES.includes(regParts[i])) { typeIdx = i; break; }
|
|
106
|
+
return `sync/batch-${shortId}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function generatePrTitle(files) {
|
|
110
|
+
if (files.length === 1) {
|
|
111
|
+
const f = files[0];
|
|
112
|
+
return `Add ${f.slug} (${f.type}) to ${f.namespace}`;
|
|
74
113
|
}
|
|
75
114
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
115
|
+
const counts = {};
|
|
116
|
+
for (const f of files) {
|
|
117
|
+
counts[f.type] = (counts[f.type] || 0) + 1;
|
|
118
|
+
}
|
|
119
|
+
const countParts = Object.entries(counts).map(([type, count]) => `${count} ${type}`);
|
|
120
|
+
const namespaces = [...new Set(files.map(f => f.namespace))];
|
|
121
|
+
|
|
122
|
+
if (namespaces.length === 1) {
|
|
123
|
+
return `sync: ${countParts.join(', ')} in ${namespaces[0]}`;
|
|
124
|
+
}
|
|
125
|
+
return `sync: ${countParts.join(', ')} across ${namespaces.join(', ')}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function generatePrBody(files, newNamespaces) {
|
|
129
|
+
if (files.length === 1) {
|
|
130
|
+
const f = files[0];
|
|
131
|
+
const bodyParts = [
|
|
132
|
+
'## Registry Upload',
|
|
81
133
|
'',
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
]
|
|
134
|
+
`- **Type:** ${f.type}`,
|
|
135
|
+
`- **Slug:** ${f.slug}`,
|
|
136
|
+
`- **Namespace:** ${f.namespace}`,
|
|
137
|
+
`- **Path:** \`${f.registryTarget}\``,
|
|
138
|
+
];
|
|
139
|
+
if (newNamespaces.length > 0) {
|
|
140
|
+
bodyParts.push('', '> **New namespace** — CODEOWNERS entry auto-added.');
|
|
141
|
+
}
|
|
142
|
+
bodyParts.push('', 'Uploaded via `aw push`');
|
|
143
|
+
return bodyParts.join('\n');
|
|
87
144
|
}
|
|
88
145
|
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
146
|
+
// Batch body — group by type
|
|
147
|
+
const grouped = {};
|
|
148
|
+
for (const f of files) {
|
|
149
|
+
if (!grouped[f.type]) grouped[f.type] = [];
|
|
150
|
+
grouped[f.type].push(f);
|
|
151
|
+
}
|
|
94
152
|
|
|
95
|
-
|
|
96
|
-
|
|
153
|
+
const bodyParts = ['## Registry Sync', ''];
|
|
154
|
+
for (const [type, items] of Object.entries(grouped)) {
|
|
155
|
+
bodyParts.push(`### ${type}`);
|
|
156
|
+
for (const item of items.sort((a, b) => a.slug.localeCompare(b.slug))) {
|
|
157
|
+
bodyParts.push(`- \`${item.slug}\` (${item.namespace})`);
|
|
158
|
+
}
|
|
159
|
+
bodyParts.push('');
|
|
97
160
|
}
|
|
98
161
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
162
|
+
if (newNamespaces.length > 0) {
|
|
163
|
+
bodyParts.push(`> **New namespace${newNamespaces.length > 1 ? 's' : ''}:** ${newNamespaces.join(', ')} — CODEOWNERS entr${newNamespaces.length > 1 ? 'ies' : 'y'} auto-added.`);
|
|
164
|
+
bodyParts.push('');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
bodyParts.push(`---`, `Synced via \`aw push\` (${files.length} files)`);
|
|
168
|
+
return bodyParts.join('\n');
|
|
169
|
+
}
|
|
103
170
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
);
|
|
171
|
+
// ── Git pipeline (works for single file or batch) ────────────────────
|
|
172
|
+
|
|
173
|
+
function pushFiles(files, { repo, dryRun }) {
|
|
174
|
+
// Summary
|
|
175
|
+
const counts = {};
|
|
176
|
+
for (const f of files) {
|
|
177
|
+
counts[f.type] = (counts[f.type] || 0) + 1;
|
|
178
|
+
}
|
|
179
|
+
const countParts = Object.entries(counts).map(([type, count]) => `${count} ${type}`);
|
|
180
|
+
fmt.logInfo(`${chalk.bold(files.length)} file${files.length > 1 ? 's' : ''} to push (${countParts.join(', ')})`);
|
|
114
181
|
|
|
115
182
|
if (dryRun) {
|
|
183
|
+
for (const f of files) {
|
|
184
|
+
const ns = chalk.dim(` [${f.namespace}]`);
|
|
185
|
+
fmt.logMessage(` ${chalk.yellow(f.type)}/${f.slug}${ns}`);
|
|
186
|
+
}
|
|
116
187
|
fmt.logWarn('No changes made (--dry-run)');
|
|
117
|
-
fmt.outro(chalk.dim('Remove --dry-run to
|
|
188
|
+
fmt.outro(chalk.dim('Remove --dry-run to push'));
|
|
118
189
|
return;
|
|
119
190
|
}
|
|
120
191
|
|
|
@@ -129,43 +200,59 @@ export function pushCommand(args) {
|
|
|
129
200
|
execSync(`git checkout ${REGISTRY_BASE_BRANCH}`, { cwd: tempDir, stdio: 'pipe' });
|
|
130
201
|
s.stop('Repository cloned');
|
|
131
202
|
|
|
132
|
-
const
|
|
133
|
-
const branch = `upload/${namespacePath.replace(/\//g, '-')}-${parentDir}-${slug}-${shortId}`;
|
|
203
|
+
const branch = generateBranchName(files);
|
|
134
204
|
execSync(`git checkout -b ${branch}`, { cwd: tempDir, stdio: 'pipe' });
|
|
135
205
|
|
|
136
206
|
const s2 = fmt.spinner();
|
|
137
|
-
s2.start('
|
|
207
|
+
s2.start(`Copying ${files.length} file${files.length > 1 ? 's' : ''} to registry...`);
|
|
208
|
+
|
|
209
|
+
// Copy each file
|
|
210
|
+
let copyErrors = [];
|
|
211
|
+
for (const file of files) {
|
|
212
|
+
try {
|
|
213
|
+
const targetFull = join(tempDir, file.registryTarget);
|
|
214
|
+
mkdirSync(dirname(targetFull), { recursive: true });
|
|
215
|
+
if (file.isDir) {
|
|
216
|
+
cpSync(file.absPath, targetFull, { recursive: true });
|
|
217
|
+
} else {
|
|
218
|
+
cpSync(file.absPath, targetFull);
|
|
219
|
+
}
|
|
220
|
+
} catch (e) {
|
|
221
|
+
copyErrors.push({ file: file.registryTarget, error: e.message });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
138
224
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
cpSync(absPath, targetFull, { recursive: true });
|
|
144
|
-
} else {
|
|
145
|
-
mkdirSync(dirname(targetFull), { recursive: true });
|
|
146
|
-
cpSync(absPath, targetFull);
|
|
225
|
+
if (copyErrors.length > 0) {
|
|
226
|
+
for (const err of copyErrors) {
|
|
227
|
+
fmt.logWarn(`Failed to copy ${err.file}: ${err.error}`);
|
|
228
|
+
}
|
|
147
229
|
}
|
|
148
230
|
|
|
149
|
-
// Check
|
|
150
|
-
|
|
151
|
-
const nsDir = join(tempDir, REGISTRY_DIR, topNamespace);
|
|
231
|
+
// Check for new namespaces — auto-add CODEOWNERS entries
|
|
232
|
+
const topNamespaces = [...new Set(files.map(f => f.namespace.split('/')[0]))];
|
|
152
233
|
const codeownersPath = join(tempDir, 'CODEOWNERS');
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
234
|
+
const newNamespaces = [];
|
|
235
|
+
for (const ns of topNamespaces) {
|
|
236
|
+
if (isNewNamespaceInCodeowners(codeownersPath, ns)) {
|
|
237
|
+
newNamespaces.push(ns);
|
|
238
|
+
const ghUser = getGitHubUser();
|
|
239
|
+
if (ghUser && existsSync(codeownersPath)) {
|
|
240
|
+
const line = `/${REGISTRY_DIR}/${ns}/ @${ghUser}\n`;
|
|
241
|
+
appendFileSync(codeownersPath, line);
|
|
242
|
+
}
|
|
160
243
|
}
|
|
161
244
|
}
|
|
245
|
+
if (newNamespaces.length > 0) {
|
|
246
|
+
execSync('git add CODEOWNERS', { cwd: tempDir, stdio: 'pipe' });
|
|
247
|
+
}
|
|
162
248
|
|
|
163
|
-
// Stage
|
|
164
|
-
execSync(`git add "${
|
|
249
|
+
// Stage all registry changes
|
|
250
|
+
execSync(`git add "${REGISTRY_DIR}"`, { cwd: tempDir, stdio: 'pipe' });
|
|
165
251
|
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
252
|
+
const prTitle = generatePrTitle(files);
|
|
253
|
+
const commitMsg = files.length === 1
|
|
254
|
+
? `registry: add ${files[0].type}/${files[0].slug} to ${files[0].namespace}`
|
|
255
|
+
: `registry: sync ${files.length} files (${countParts.join(', ')})`;
|
|
169
256
|
execSync(`git commit -m "${commitMsg}"`, { cwd: tempDir, stdio: 'pipe' });
|
|
170
257
|
|
|
171
258
|
s2.stop('Upload prepared');
|
|
@@ -175,21 +262,7 @@ export function pushCommand(args) {
|
|
|
175
262
|
|
|
176
263
|
execSync(`git push -u origin ${branch}`, { cwd: tempDir, stdio: 'pipe' });
|
|
177
264
|
|
|
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');
|
|
265
|
+
const prBody = generatePrBody(files, newNamespaces);
|
|
193
266
|
|
|
194
267
|
// Try gh for PR creation, fall back to manual URL
|
|
195
268
|
let prUrl;
|
|
@@ -201,27 +274,132 @@ export function pushCommand(args) {
|
|
|
201
274
|
'--body', prBody,
|
|
202
275
|
], { cwd: tempDir, encoding: 'utf8' }).trim();
|
|
203
276
|
} catch {
|
|
204
|
-
// gh not available — construct PR URL manually
|
|
205
277
|
const repoBase = repo.replace(/\.git$/, '');
|
|
206
278
|
prUrl = `https://github.com/${repoBase}/compare/${REGISTRY_BASE_BRANCH}...${branch}?expand=1`;
|
|
207
279
|
}
|
|
208
280
|
|
|
209
281
|
s3.stop('Branch pushed');
|
|
210
282
|
|
|
211
|
-
|
|
212
|
-
|
|
283
|
+
// Print summary
|
|
284
|
+
if (newNamespaces.length > 0) {
|
|
285
|
+
fmt.logInfo(`New namespace${newNamespaces.length > 1 ? 's' : ''} ${chalk.cyan(newNamespaces.join(', '))} — CODEOWNERS entr${newNamespaces.length > 1 ? 'ies' : 'y'} added`);
|
|
286
|
+
}
|
|
287
|
+
if (files.length > 1) {
|
|
288
|
+
for (const [type, items] of Object.entries(groupBy(files, 'type'))) {
|
|
289
|
+
fmt.logSuccess(`${items.length} ${type} pushed`);
|
|
290
|
+
}
|
|
213
291
|
}
|
|
214
292
|
fmt.logSuccess(`PR: ${chalk.cyan(prUrl)}`);
|
|
215
|
-
fmt.outro('
|
|
293
|
+
fmt.outro('Push complete');
|
|
216
294
|
} catch (e) {
|
|
217
|
-
fmt.cancel(`
|
|
295
|
+
fmt.cancel(`Push failed: ${e.message}`);
|
|
218
296
|
} finally {
|
|
219
297
|
execSync(`rm -rf "${tempDir}"`, { stdio: 'pipe' });
|
|
220
298
|
}
|
|
221
299
|
}
|
|
222
300
|
|
|
301
|
+
function groupBy(arr, key) {
|
|
302
|
+
const result = {};
|
|
303
|
+
for (const item of arr) {
|
|
304
|
+
const k = item[key];
|
|
305
|
+
if (!result[k]) result[k] = [];
|
|
306
|
+
result[k].push(item);
|
|
307
|
+
}
|
|
308
|
+
return result;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ── Main command ─────────────────────────────────────────────────────
|
|
312
|
+
|
|
313
|
+
export function pushCommand(args) {
|
|
314
|
+
const input = args._positional?.[0];
|
|
315
|
+
const dryRun = args['--dry-run'] === true;
|
|
316
|
+
const repo = args['--repo'] || REGISTRY_REPO;
|
|
317
|
+
const cwd = process.cwd();
|
|
318
|
+
const workspaceDir = join(cwd, '.aw_registry');
|
|
319
|
+
|
|
320
|
+
fmt.intro('aw push');
|
|
321
|
+
|
|
322
|
+
// No args = push all modified files
|
|
323
|
+
if (!input) {
|
|
324
|
+
const files = collectModifiedFiles(workspaceDir);
|
|
325
|
+
if (files.length === 0) {
|
|
326
|
+
fmt.cancel('Nothing to push — no modified files.\n\n Use `aw status` to see synced files.');
|
|
327
|
+
}
|
|
328
|
+
pushFiles(files, { repo, dryRun });
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Resolve input — accept both registry path and local path
|
|
333
|
+
const resolved = resolveInput(input, workspaceDir);
|
|
334
|
+
|
|
335
|
+
if (!resolved.registryPath) {
|
|
336
|
+
const hint = input.startsWith('.claude/') || input.startsWith('.cursor/') || input.startsWith('.codex/')
|
|
337
|
+
? `\n\n Tip: Use the .aw_registry/ path instead:\n aw push .aw_registry/${input.split('/').slice(1).join('/')}`
|
|
338
|
+
: '';
|
|
339
|
+
fmt.cancel(`Could not resolve "${input}" to a registry path.${hint}\n\n Only files inside .aw_registry/ can be pushed.`);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
let absPath = resolved.localAbsPath;
|
|
343
|
+
if (!absPath || !existsSync(absPath)) {
|
|
344
|
+
if (absPath && !absPath.endsWith('.md') && existsSync(absPath + '.md')) {
|
|
345
|
+
absPath = absPath + '.md';
|
|
346
|
+
} else {
|
|
347
|
+
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.`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Folder/namespace input → batch push
|
|
352
|
+
if (statSync(absPath).isDirectory()) {
|
|
353
|
+
const files = collectBatchFiles(absPath, workspaceDir);
|
|
354
|
+
if (files.length === 0) {
|
|
355
|
+
fmt.cancel(`Nothing to push in ${chalk.cyan(input)} — no agents, skills, commands, or evals found.`);
|
|
356
|
+
}
|
|
357
|
+
pushFiles(files, { repo, dryRun });
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Single file input → wrap in array and push
|
|
362
|
+
const regParts = resolved.registryPath.split('/');
|
|
363
|
+
let typeIdx = -1;
|
|
364
|
+
for (let i = regParts.length - 1; i >= 0; i--) {
|
|
365
|
+
if (PUSHABLE_TYPES.includes(regParts[i])) { typeIdx = i; break; }
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (typeIdx === -1 || typeIdx + 1 >= regParts.length) {
|
|
369
|
+
fmt.cancel([
|
|
370
|
+
`Invalid push path: ${chalk.red(resolved.registryPath)}`,
|
|
371
|
+
'',
|
|
372
|
+
` Content must live under an ${chalk.bold('agents/')}, ${chalk.bold('skills/')}, ${chalk.bold('commands/')}, or ${chalk.bold('evals/')} directory.`,
|
|
373
|
+
'',
|
|
374
|
+
` ${chalk.dim('Valid examples:')}`,
|
|
375
|
+
` aw push .aw_registry/commerce/quality/agents/unit-tester.md`,
|
|
376
|
+
` aw push .aw_registry/platform/services/skills/development`,
|
|
377
|
+
` aw push .aw_registry/commerce/shared/commands/ship.md`,
|
|
378
|
+
].join('\n'));
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const namespaceParts = regParts.slice(0, typeIdx);
|
|
382
|
+
const parentDir = regParts[typeIdx];
|
|
383
|
+
const slug = regParts[typeIdx + 1];
|
|
384
|
+
const namespacePath = namespaceParts.join('/');
|
|
385
|
+
const isDir = statSync(absPath).isDirectory();
|
|
386
|
+
const registryTarget = isDir
|
|
387
|
+
? `${REGISTRY_DIR}/${namespacePath}/${parentDir}/${slug}`
|
|
388
|
+
: `${REGISTRY_DIR}/${namespacePath}/${parentDir}/${slug}.md`;
|
|
389
|
+
|
|
390
|
+
pushFiles([{
|
|
391
|
+
absPath,
|
|
392
|
+
registryTarget,
|
|
393
|
+
type: parentDir,
|
|
394
|
+
namespace: namespacePath,
|
|
395
|
+
slug,
|
|
396
|
+
isDir,
|
|
397
|
+
}], { repo, dryRun });
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ── Helpers ──────────────────────────────────────────────────────────
|
|
401
|
+
|
|
223
402
|
function getGitHubUser() {
|
|
224
|
-
// Try gh first, fall back to git config
|
|
225
403
|
try {
|
|
226
404
|
return execSync('gh api user --jq .login', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
227
405
|
} catch {
|
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
|
|