agentfold 0.1.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/LICENSE +21 -0
- package/README.md +197 -0
- package/agentfold.mjs +1115 -0
- package/lib/compose-layers.mjs +23 -0
- package/lib/diff.mjs +47 -0
- package/lib/load-profile.mjs +112 -0
- package/lib/manifest.mjs +66 -0
- package/lib/pipeline-steps.mjs +853 -0
- package/lib/scope.mjs +158 -0
- package/lib/util.mjs +348 -0
- package/lib/validate.mjs +209 -0
- package/package.json +42 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { pathExists } from './util.mjs';
|
|
3
|
+
|
|
4
|
+
export async function resolveLayerPaths({ configRoot, preset }) {
|
|
5
|
+
const layers = [];
|
|
6
|
+
for (const layerConfig of preset.layers) {
|
|
7
|
+
const layerName = layerConfig.name;
|
|
8
|
+
const layerPath = path.join(configRoot, 'layers', layerName);
|
|
9
|
+
|
|
10
|
+
if (!(await pathExists(layerPath))) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
`Layer "${layerName}" does not exist at ${layerPath}`
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
layers.push({
|
|
17
|
+
name: layerName,
|
|
18
|
+
path: layerPath,
|
|
19
|
+
select: layerConfig.select,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return layers;
|
|
23
|
+
}
|
package/lib/diff.mjs
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { pathExists, sha256, toPosixPath } from './util.mjs';
|
|
4
|
+
|
|
5
|
+
async function readText(filePath) {
|
|
6
|
+
return fs.readFile(filePath, 'utf8');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function diffRenderMapAgainstProject({ renderMap, projectRoot, previousManifest }) {
|
|
10
|
+
const added = [];
|
|
11
|
+
const changed = [];
|
|
12
|
+
const removed = [];
|
|
13
|
+
|
|
14
|
+
for (const [relativePath, record] of renderMap.entries()) {
|
|
15
|
+
const destination = path.join(projectRoot, relativePath);
|
|
16
|
+
if (!(await pathExists(destination))) {
|
|
17
|
+
added.push(relativePath);
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const existingText = await readText(destination);
|
|
22
|
+
const existingHash = sha256(existingText);
|
|
23
|
+
const plannedHash = sha256(record.content);
|
|
24
|
+
if (existingHash !== plannedHash) {
|
|
25
|
+
changed.push(relativePath);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const previousFiles = Array.isArray(previousManifest?.files) ? previousManifest.files : [];
|
|
30
|
+
for (const entry of previousFiles) {
|
|
31
|
+
const relativePath = toPosixPath(entry.path);
|
|
32
|
+
if (renderMap.has(relativePath)) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const destination = path.join(projectRoot, relativePath);
|
|
37
|
+
if (await pathExists(destination)) {
|
|
38
|
+
removed.push(relativePath);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
added.sort();
|
|
43
|
+
changed.sort();
|
|
44
|
+
removed.sort();
|
|
45
|
+
|
|
46
|
+
return { added, changed, removed };
|
|
47
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { pathExists, readJson } from './util.mjs';
|
|
3
|
+
import {
|
|
4
|
+
loadScopeTagEnum,
|
|
5
|
+
normalizeScopeSelector,
|
|
6
|
+
normalizeProfileScopeSelectors,
|
|
7
|
+
selectableTypes,
|
|
8
|
+
} from './scope.mjs';
|
|
9
|
+
|
|
10
|
+
function normalizeTargets(rawTargets) {
|
|
11
|
+
const selected = Array.isArray(rawTargets)
|
|
12
|
+
? rawTargets.filter((target) => typeof target === 'string').map((target) => target.trim())
|
|
13
|
+
: [];
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
copilot: selected.includes('copilot'),
|
|
17
|
+
codex: selected.includes('codex'),
|
|
18
|
+
cursor: selected.includes('cursor'),
|
|
19
|
+
claude: selected.includes('claude'),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalizeLayers(rawLayers) {
|
|
24
|
+
const layers = Array.isArray(rawLayers) ? rawLayers : [];
|
|
25
|
+
return layers
|
|
26
|
+
.map((entry) => {
|
|
27
|
+
if (typeof entry === 'string') {
|
|
28
|
+
return {
|
|
29
|
+
name: entry,
|
|
30
|
+
select: normalizeProfileScopeSelectors({}),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!entry || typeof entry !== 'object') {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const name = typeof entry.name === 'string' ? entry.name.trim() : '';
|
|
39
|
+
if (!name) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const perLayerSelect = {};
|
|
44
|
+
for (const type of selectableTypes) {
|
|
45
|
+
perLayerSelect[type] = normalizeScopeSelector(entry.select?.[type]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return { name, select: perLayerSelect };
|
|
49
|
+
})
|
|
50
|
+
.filter(Boolean);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function loadProfile({ configRoot, profileNameOverride }) {
|
|
54
|
+
let profileName = profileNameOverride || 'default';
|
|
55
|
+
|
|
56
|
+
if (!profileName || typeof profileName !== 'string') {
|
|
57
|
+
throw new Error('Profile name is required. Pass --profile <name>.');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const profilePath = path.join(configRoot, 'config', 'profiles', `${profileName}.json`);
|
|
61
|
+
if (!(await pathExists(profilePath))) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Profile "${profileName}" not found at ${profilePath}`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const raw = await readJson(profilePath);
|
|
68
|
+
const layers = normalizeLayers(raw.layers);
|
|
69
|
+
if (layers.length === 0) {
|
|
70
|
+
throw new Error(`Profile ${profileName} must define a non-empty "layers" array.`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const targets = normalizeTargets(raw.targets);
|
|
74
|
+
|
|
75
|
+
const hasTarget = targets.copilot || targets.codex || targets.cursor || targets.claude;
|
|
76
|
+
if (!hasTarget) {
|
|
77
|
+
throw new Error(`Profile ${profileName} must enable at least one target.`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const scopeTags = await loadScopeTagEnum({ configRoot });
|
|
81
|
+
const scopeTagSet = new Set(scopeTags);
|
|
82
|
+
|
|
83
|
+
for (const layer of layers) {
|
|
84
|
+
for (const type of selectableTypes) {
|
|
85
|
+
const selector = layer.select[type];
|
|
86
|
+
for (const tag of selector.tags.values) {
|
|
87
|
+
if (!scopeTagSet.has(tag)) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`Profile ${profileName} has unknown tag "${tag}" in layers[${layer.name}].select.${type}. Add it to config/definitions/scope-tags.json or <project>/.agentfold/definitions/scope-tags.json first.`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
name: raw.name || profileName,
|
|
98
|
+
profileName,
|
|
99
|
+
profilePath,
|
|
100
|
+
layers,
|
|
101
|
+
targets,
|
|
102
|
+
scopeTags,
|
|
103
|
+
subagents: {
|
|
104
|
+
portable: true,
|
|
105
|
+
sourceMode: 'centralized',
|
|
106
|
+
},
|
|
107
|
+
organization: {
|
|
108
|
+
includeRuleSubdirs: true,
|
|
109
|
+
includeAgentSubdirs: true,
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
package/lib/manifest.mjs
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { pathExists, readJson, sha256, toPosixPath, writeJson } from './util.mjs';
|
|
4
|
+
|
|
5
|
+
export async function loadManifest(projectRoot) {
|
|
6
|
+
const manifestPath = path.join(projectRoot, '.agentfold', 'manifest.json');
|
|
7
|
+
if (!(await pathExists(manifestPath))) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
return readJson(manifestPath);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function writeManifest({ projectRoot, preset, renderMap }) {
|
|
14
|
+
const files = [];
|
|
15
|
+
const sortedEntries = [...renderMap.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
16
|
+
for (const [relativePath, record] of sortedEntries) {
|
|
17
|
+
const content = record.content;
|
|
18
|
+
files.push({
|
|
19
|
+
path: toPosixPath(relativePath),
|
|
20
|
+
size: Buffer.byteLength(content, 'utf8'),
|
|
21
|
+
sha256: sha256(content),
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const manifest = {
|
|
26
|
+
generatedAt: new Date().toISOString(),
|
|
27
|
+
profile: preset.profileName,
|
|
28
|
+
layers: preset.layers,
|
|
29
|
+
targets: preset.targets,
|
|
30
|
+
orgSubdirsIncluded: {
|
|
31
|
+
rules: preset.organization.includeRuleSubdirs,
|
|
32
|
+
agents: preset.organization.includeAgentSubdirs,
|
|
33
|
+
},
|
|
34
|
+
files,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
await writeJson(path.join(projectRoot, '.agentfold', 'manifest.json'), manifest);
|
|
38
|
+
return manifest;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function pruneManagedFiles({ previousManifest, renderMap, projectRoot, preservePaths = [] }) {
|
|
42
|
+
if (!previousManifest || !Array.isArray(previousManifest.files)) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const deleted = [];
|
|
47
|
+
const preserved = new Set(preservePaths.map((item) => toPosixPath(item)));
|
|
48
|
+
for (const entry of previousManifest.files) {
|
|
49
|
+
const relativePath = toPosixPath(entry.path);
|
|
50
|
+
if (renderMap.has(relativePath)) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (preserved.has(relativePath)) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const fullPath = path.join(projectRoot, relativePath);
|
|
58
|
+
if (await pathExists(fullPath)) {
|
|
59
|
+
await fs.rm(fullPath, { force: true });
|
|
60
|
+
deleted.push(relativePath);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
deleted.sort();
|
|
65
|
+
return deleted;
|
|
66
|
+
}
|