@ghl-ai/aw 0.1.58 → 0.1.60-beta.0
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 +77 -38
- package/package.json +1 -1
- package/registry.mjs +27 -8
package/commands/push.mjs
CHANGED
|
@@ -662,6 +662,10 @@ async function publishProjectAwDocs(cwd, home, dryRun, scope = null) {
|
|
|
662
662
|
// Auto-generate a branch name from the files being pushed.
|
|
663
663
|
function generateBranchName(files, extraPaths = []) {
|
|
664
664
|
const shortId = Date.now().toString(36).slice(-5);
|
|
665
|
+
const branchSafe = (value) => String(value || '')
|
|
666
|
+
.replace(/\//g, '-')
|
|
667
|
+
.replace(/[^A-Za-z0-9._-]+/g, '-')
|
|
668
|
+
.replace(/^-+|-+$/g, '') || 'item';
|
|
665
669
|
|
|
666
670
|
if (files.length === 0 && extraPaths.length > 0) {
|
|
667
671
|
return `${managedBranchPrefix(extraPaths)}-${shortId}`;
|
|
@@ -675,8 +679,8 @@ function generateBranchName(files, extraPaths = []) {
|
|
|
675
679
|
|
|
676
680
|
if (files.length === 1) {
|
|
677
681
|
const f = files[0];
|
|
678
|
-
const nsSlug = f.namespace
|
|
679
|
-
return `${prefix}/${nsSlug}-${f.type}-${f.slug}-${shortId}`;
|
|
682
|
+
const nsSlug = branchSafe(f.namespace);
|
|
683
|
+
return `${prefix}/${nsSlug}-${f.type}-${branchSafe(f.slug)}-${shortId}`;
|
|
680
684
|
}
|
|
681
685
|
|
|
682
686
|
if (namespaces.length === 1) {
|
|
@@ -698,7 +702,7 @@ function singular(type, count) {
|
|
|
698
702
|
function parseCommitFiles(commits) {
|
|
699
703
|
const result = [];
|
|
700
704
|
for (const c of commits) {
|
|
701
|
-
const m = c.message.match(/registry:\s+(add|remove|sync)\s+(agents|skills|commands|evals)\/([\w
|
|
705
|
+
const m = c.message.match(/registry:\s+(add|remove|sync)\s+(agents|skills|commands|evals)\/([\w/-]+)\s+(?:to|from)\s+([\w/]+)/);
|
|
702
706
|
if (m) result.push({ verb: m[1], type: m[2], slug: m[3], namespace: m[4] });
|
|
703
707
|
}
|
|
704
708
|
return result;
|
|
@@ -860,16 +864,14 @@ function collectBatchFiles(folderAbsPath, registrySubDir) {
|
|
|
860
864
|
let walkDir = folderAbsPath;
|
|
861
865
|
let walkBaseName = relPath;
|
|
862
866
|
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
}
|
|
872
|
-
break;
|
|
867
|
+
const typeIdx = findPushableTypeIndex(segments);
|
|
868
|
+
if (typeIdx !== -1) {
|
|
869
|
+
const namespaceParts = segments.slice(0, typeIdx);
|
|
870
|
+
walkBaseName = namespaceParts.join('/');
|
|
871
|
+
walkDir = join(registrySubDir, ...namespaceParts);
|
|
872
|
+
typeFilter = segments[typeIdx];
|
|
873
|
+
if (typeIdx + 1 < segments.length) {
|
|
874
|
+
subPathFilter = segments.slice(typeIdx + 1).join('/');
|
|
873
875
|
}
|
|
874
876
|
}
|
|
875
877
|
|
|
@@ -877,7 +879,10 @@ function collectBatchFiles(folderAbsPath, registrySubDir) {
|
|
|
877
879
|
return entries
|
|
878
880
|
.filter(entry => {
|
|
879
881
|
if (typeFilter && entry.type !== typeFilter) return false;
|
|
880
|
-
if (subPathFilter
|
|
882
|
+
if (subPathFilter) {
|
|
883
|
+
const prefix = `${subPathFilter}/`;
|
|
884
|
+
if (entry.slug !== subPathFilter && !entry.slug.startsWith(prefix)) return false;
|
|
885
|
+
}
|
|
881
886
|
return true;
|
|
882
887
|
})
|
|
883
888
|
.map(entry => {
|
|
@@ -900,18 +905,53 @@ function collectBatchFiles(folderAbsPath, registrySubDir) {
|
|
|
900
905
|
|
|
901
906
|
// ── Parse type/namespace/slug from a registry-relative path ──────────
|
|
902
907
|
|
|
903
|
-
function
|
|
904
|
-
|
|
908
|
+
function findSkillSlugParts(namespaceParts, skillParts, registrySubDir) {
|
|
909
|
+
if (skillParts.length === 0) return [];
|
|
910
|
+
|
|
911
|
+
if (registrySubDir) {
|
|
912
|
+
for (let i = skillParts.length; i > 0; i--) {
|
|
913
|
+
const candidate = join(registrySubDir, ...namespaceParts, 'skills', ...skillParts.slice(0, i));
|
|
914
|
+
if (existsSync(join(candidate, 'SKILL.md'))) return skillParts.slice(0, i);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
const skillMdIdx = skillParts.findIndex(part => part === 'SKILL' || part === 'SKILL.md');
|
|
919
|
+
if (skillMdIdx > 0) return skillParts.slice(0, skillMdIdx);
|
|
920
|
+
|
|
921
|
+
return [skillParts[0]];
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function findPushableTypeIndex(parts) {
|
|
905
925
|
for (let i = 0; i < parts.length; i++) {
|
|
906
|
-
if (
|
|
907
|
-
return
|
|
908
|
-
type: parts[i],
|
|
909
|
-
namespace: parts.slice(0, i).join('/'),
|
|
910
|
-
slug: parts[i + 1].replace(/\.md$/, ''),
|
|
911
|
-
};
|
|
926
|
+
if ((parts[i] === 'agents' || parts[i] === 'commands') && parts[i + 1] === 'evals') {
|
|
927
|
+
return i + 1;
|
|
912
928
|
}
|
|
929
|
+
if (PUSHABLE_TYPES.includes(parts[i])) return i;
|
|
913
930
|
}
|
|
914
|
-
return
|
|
931
|
+
return -1;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
function parseRegistryPath(relPath, registrySubDir = null) {
|
|
935
|
+
const parts = relPath.split('/');
|
|
936
|
+
const typeIdx = findPushableTypeIndex(parts);
|
|
937
|
+
if (typeIdx === -1 || typeIdx + 1 >= parts.length) return null;
|
|
938
|
+
|
|
939
|
+
const namespaceParts = parts.slice(0, typeIdx);
|
|
940
|
+
if (parts[typeIdx] === 'skills') {
|
|
941
|
+
const skillParts = parts.slice(typeIdx + 1).map(part => part.replace(/\.md$/, ''));
|
|
942
|
+
const slugParts = findSkillSlugParts(namespaceParts, skillParts, registrySubDir);
|
|
943
|
+
return {
|
|
944
|
+
type: parts[typeIdx],
|
|
945
|
+
namespace: namespaceParts.join('/'),
|
|
946
|
+
slug: slugParts.join('/'),
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
return {
|
|
951
|
+
type: parts[typeIdx],
|
|
952
|
+
namespace: namespaceParts.join('/'),
|
|
953
|
+
slug: parts[typeIdx + 1].replace(/\.md$/, ''),
|
|
954
|
+
};
|
|
915
955
|
}
|
|
916
956
|
|
|
917
957
|
// ── Colocated eval resolution ─────────────────────────────────────────
|
|
@@ -1198,7 +1238,7 @@ export async function pushCommand(args) {
|
|
|
1198
1238
|
|
|
1199
1239
|
if (staged.length > 0 || extraStaged.length > 0) {
|
|
1200
1240
|
const files = staged.map(f => {
|
|
1201
|
-
const meta = parseRegistryPath(f.registryPath);
|
|
1241
|
+
const meta = parseRegistryPath(f.registryPath, registrySubDir);
|
|
1202
1242
|
const parts = f.registryPath.split('/');
|
|
1203
1243
|
return {
|
|
1204
1244
|
absPath: join(awHome, f.path),
|
|
@@ -1264,7 +1304,7 @@ export async function pushCommand(args) {
|
|
|
1264
1304
|
|
|
1265
1305
|
const files = allEntries
|
|
1266
1306
|
.map(f => {
|
|
1267
|
-
const meta = parseRegistryPath(f.registryPath);
|
|
1307
|
+
const meta = parseRegistryPath(f.registryPath, registrySubDir);
|
|
1268
1308
|
const parts = f.registryPath.split('/');
|
|
1269
1309
|
return {
|
|
1270
1310
|
absPath: join(awHome, f.path),
|
|
@@ -1369,7 +1409,7 @@ export async function pushCommand(args) {
|
|
|
1369
1409
|
const deletedInFolder = folderChanges.deleted
|
|
1370
1410
|
.filter(e => !folderPrefix || e.registryPath.startsWith(folderPrefix))
|
|
1371
1411
|
.map(e => {
|
|
1372
|
-
const meta = parseRegistryPath(e.registryPath);
|
|
1412
|
+
const meta = parseRegistryPath(e.registryPath, registrySubDir);
|
|
1373
1413
|
const parts = e.registryPath.split('/');
|
|
1374
1414
|
return {
|
|
1375
1415
|
absPath: join(awHome, e.path),
|
|
@@ -1396,13 +1436,8 @@ export async function pushCommand(args) {
|
|
|
1396
1436
|
}
|
|
1397
1437
|
|
|
1398
1438
|
// Single file input
|
|
1399
|
-
const
|
|
1400
|
-
|
|
1401
|
-
for (let i = regParts.length - 1; i >= 0; i--) {
|
|
1402
|
-
if (PUSHABLE_TYPES.includes(regParts[i])) { typeIdx = i; break; }
|
|
1403
|
-
}
|
|
1404
|
-
|
|
1405
|
-
if (typeIdx === -1 || typeIdx + 1 >= regParts.length) {
|
|
1439
|
+
const meta = parseRegistryPath(resolved.registryPath, registrySubDir);
|
|
1440
|
+
if (!meta?.type || !meta.slug) {
|
|
1406
1441
|
fmt.cancel([
|
|
1407
1442
|
`Invalid push path: ${chalk.red(resolved.registryPath)}`,
|
|
1408
1443
|
'',
|
|
@@ -1416,14 +1451,18 @@ export async function pushCommand(args) {
|
|
|
1416
1451
|
return;
|
|
1417
1452
|
}
|
|
1418
1453
|
|
|
1419
|
-
const
|
|
1420
|
-
const
|
|
1421
|
-
const
|
|
1422
|
-
const namespacePath = namespaceParts.join('/');
|
|
1454
|
+
const parentDir = meta.type;
|
|
1455
|
+
const slug = meta.slug;
|
|
1456
|
+
const namespacePath = meta.namespace;
|
|
1423
1457
|
const isDir = !isDeletedFile && statSync(absPath).isDirectory();
|
|
1458
|
+
const registryPathWithExt = resolved.registryPath.endsWith('.md')
|
|
1459
|
+
|| (!isDeletedFile && absPath && !absPath.endsWith('.md'))
|
|
1460
|
+
|| (isDeletedFile && /\.[^/]+$/.test(resolved.registryPath))
|
|
1461
|
+
? resolved.registryPath
|
|
1462
|
+
: `${resolved.registryPath}.md`;
|
|
1424
1463
|
const registryTarget = isDir
|
|
1425
1464
|
? `${REGISTRY_DIR}/${namespacePath}/${parentDir}/${slug}`
|
|
1426
|
-
: `${REGISTRY_DIR}/${
|
|
1465
|
+
: `${REGISTRY_DIR}/${registryPathWithExt}`;
|
|
1427
1466
|
|
|
1428
1467
|
// Check if the file actually has changes before trying to commit
|
|
1429
1468
|
const singleFileChanges = detectChanges(awHome, REGISTRY_DIR);
|
package/package.json
CHANGED
package/registry.mjs
CHANGED
|
@@ -28,10 +28,33 @@ export function getAllFiles(dirPath) {
|
|
|
28
28
|
return results;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
function findSkillRootDirs(skillsDir) {
|
|
32
|
+
const results = [];
|
|
33
|
+
|
|
34
|
+
function walk(dir, segments) {
|
|
35
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
36
|
+
if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
|
|
37
|
+
|
|
38
|
+
const childDir = join(dir, entry.name);
|
|
39
|
+
const childSegments = [...segments, entry.name];
|
|
40
|
+
if (existsSync(join(childDir, 'SKILL.md'))) {
|
|
41
|
+
results.push({ skillPath: childDir, slug: childSegments.join('/') });
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
walk(childDir, childSegments);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
walk(skillsDir, []);
|
|
50
|
+
return results;
|
|
51
|
+
}
|
|
52
|
+
|
|
31
53
|
/**
|
|
32
54
|
* Walk a registry directory tree and collect file-level entries.
|
|
33
55
|
* Every file gets its own entry with its full registry path.
|
|
34
|
-
* Skills are NOT atomic — each file inside a skill is registered individually.
|
|
56
|
+
* Skills are NOT atomic — each file inside a skill root is registered individually.
|
|
57
|
+
* A skill root is the nearest directory below skills/ that contains SKILL.md.
|
|
35
58
|
*/
|
|
36
59
|
export function walkRegistryTree(baseDir, baseName) {
|
|
37
60
|
const entries = [];
|
|
@@ -47,13 +70,9 @@ export function walkRegistryTree(baseDir, baseName) {
|
|
|
47
70
|
const namespace = pathSegments.join('/');
|
|
48
71
|
|
|
49
72
|
if (typeDir === 'skills') {
|
|
50
|
-
// Skills
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const skillPath = join(fullPath, skillEntry.name);
|
|
54
|
-
if (!statSync(skillPath).isDirectory()) continue;
|
|
55
|
-
const slug = skillEntry.name;
|
|
56
|
-
|
|
73
|
+
// Skills can be grouped in nested folders. The actual skill root is
|
|
74
|
+
// the first nested directory that contains SKILL.md.
|
|
75
|
+
for (const { skillPath, slug } of findSkillRootDirs(fullPath)) {
|
|
57
76
|
for (const file of getAllFiles(skillPath)) {
|
|
58
77
|
const relPath = relative(skillPath, file);
|
|
59
78
|
const relNoExt = relPath.replace(/\.md$/, '');
|