@zucker-framework/cli 1.0.0 → 1.0.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.
@@ -0,0 +1,145 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync } from 'node:fs';
3
+ import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
4
+
5
+ function within(parent, child) {
6
+ const path = relative(parent, child);
7
+ return path !== '..' && !path.startsWith('../') && !isAbsolute(path);
8
+ }
9
+
10
+ function markdownFiles(directory, includeLinks = false) {
11
+ if (!existsSync(directory)) return [];
12
+ return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
13
+ const path = join(directory, entry.name);
14
+ if (entry.isDirectory()) return markdownFiles(path, includeLinks);
15
+ return entry.name.endsWith('.md') || (includeLinks && entry.isSymbolicLink()) ? [path] : [];
16
+ });
17
+ }
18
+
19
+ // Repo-owned entrypoints keep these two routing fields on one line. Full YAML
20
+ // validation remains available through the official skill-creator validator.
21
+ function routingField(frontmatter, key) {
22
+ const matches = [...frontmatter.matchAll(new RegExp(`^${key}: (.+)$`, 'gm'))];
23
+ if (matches.length !== 1) return '';
24
+ const value = matches[0][1].trim();
25
+ if (value.startsWith('"')) {
26
+ try {
27
+ const parsed = JSON.parse(value);
28
+ return typeof parsed === 'string' ? parsed : '';
29
+ } catch {
30
+ return '';
31
+ }
32
+ }
33
+ return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value) ? value : '';
34
+ }
35
+
36
+ export function auditAgentInstructions(repository, policy) {
37
+ const { checkGit = true, allowSkillReferenceAliases = false } = policy;
38
+ const root = realpathSync(repository);
39
+ const skillsRoot = join(root, policy.skillsDirectory);
40
+ const errors = [];
41
+ const skillFiles = markdownFiles(skillsRoot);
42
+ const configuredAliases = policy.compatibilityLinks.filter(
43
+ ({ path, optional }) => !optional || lstatSync(join(root, path), { throwIfNoEntry: false }),
44
+ );
45
+ const aliases = [
46
+ ...configuredAliases.map(({ path }) => join(root, path)),
47
+ ...policy.skillAliasDirectories.flatMap((directory) =>
48
+ markdownFiles(join(root, directory), true),
49
+ ),
50
+ ];
51
+ const documentAliasTargets = new Map(
52
+ configuredAliases
53
+ .filter(({ target }) => target !== undefined)
54
+ .map(({ path, target }) => [path, target]),
55
+ );
56
+ const documents = [...policy.documents.map((path) => join(root, path)), ...skillFiles];
57
+ const names = new Set();
58
+ const entrypoints = skillFiles.filter((path) => path.endsWith('/SKILL.md'));
59
+ if (entrypoints.length === 0) errors.push('No discoverable repository skills');
60
+
61
+ for (const path of documents) {
62
+ const label = relative(root, path);
63
+ if (!existsSync(path)) {
64
+ errors.push(`${label}: missing document`);
65
+ continue;
66
+ }
67
+ if (!within(root, realpathSync(path))) {
68
+ errors.push(`${label}: document resolves outside repository`);
69
+ continue;
70
+ }
71
+ const source = readFileSync(path, 'utf8');
72
+ if (label === policy.rootDocument && Buffer.byteLength(source) > 32 * 1024) {
73
+ errors.push(`${label}: exceeds Codex's default 32 KiB project-doc limit`);
74
+ }
75
+ if (entrypoints.includes(path)) {
76
+ const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(source)?.[1];
77
+ const name = routingField(frontmatter ?? '', 'name');
78
+ const description = routingField(frontmatter ?? '', 'description');
79
+ if (!name || name.length > 64 || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) {
80
+ errors.push(`${label}: invalid or missing name`);
81
+ }
82
+ if (name !== relative(skillsRoot, dirname(path))) {
83
+ errors.push(`${label}: name must match the skill directory`);
84
+ }
85
+ if (!description.trim() || description.length > 1024 || /[<>]/.test(description)) {
86
+ errors.push(
87
+ `${label}: use a nonempty single-line JSON-quoted description (max 1024 characters)`,
88
+ );
89
+ }
90
+ if (names.has(name)) errors.push(`${label}: duplicate skill name ${name}`);
91
+ names.add(name);
92
+ }
93
+
94
+ // Check local Markdown destinations, not example code or external websites.
95
+ const prose = source.replace(/^(`{3,}|~{3,})[^\n]*\n[\s\S]*?^\1\s*$/gm, '');
96
+ for (const match of prose.matchAll(/\[[^\]\n]*\]\(([^)\s]+)\)/g)) {
97
+ const target = match[1];
98
+ if (/^(?:[a-z][a-z0-9+.-]*:|#)/i.test(target)) continue;
99
+ const destination = resolve(dirname(path), target.split('#')[0]);
100
+ if (!within(root, destination)) {
101
+ errors.push(`${label}: link escapes repository: ${target}`);
102
+ } else if (!existsSync(destination)) {
103
+ errors.push(`${label}: broken local link: ${target}`);
104
+ } else if (!within(root, realpathSync(destination))) {
105
+ errors.push(`${label}: link resolves outside repository: ${target}`);
106
+ }
107
+ }
108
+ }
109
+
110
+ for (const path of aliases) {
111
+ const label = relative(root, path);
112
+ if (!existsSync(path) && !lstatSync(path, { throwIfNoEntry: false })) {
113
+ errors.push(`${label}: missing compatibility link`);
114
+ continue;
115
+ }
116
+ if (!lstatSync(path).isSymbolicLink()) {
117
+ errors.push(`${label}: legacy skill must link to its canonical entrypoint`);
118
+ } else if (!existsSync(path)) {
119
+ errors.push(`${label}: broken compatibility link`);
120
+ } else if (
121
+ documentAliasTargets.has(label)
122
+ ? realpathSync(path) !== join(root, documentAliasTargets.get(label))
123
+ : !(allowSkillReferenceAliases ? skillFiles : entrypoints).some((entrypoint) =>
124
+ [entrypoint, dirname(entrypoint)].includes(realpathSync(path)),
125
+ )
126
+ ) {
127
+ const expected = documentAliasTargets.get(label) ?? 'a repository SKILL.md';
128
+ errors.push(`${label}: compatibility target must be ${expected}`);
129
+ }
130
+ }
131
+
132
+ if (checkGit) {
133
+ const ignored = spawnSync('git', ['check-ignore', '--no-index', '--stdin'], {
134
+ cwd: root,
135
+ input: [...documents, ...aliases].map((path) => relative(root, path)).join('\n') + '\n',
136
+ encoding: 'utf8',
137
+ });
138
+ if (ignored.error || ![0, 1].includes(ignored.status)) {
139
+ errors.push('Unable to verify instruction visibility with git check-ignore');
140
+ } else if (ignored.status === 0) {
141
+ errors.push(`Instruction files are ignored by Git:\n${ignored.stdout.trim()}`);
142
+ }
143
+ }
144
+ return { errors, skills: entrypoints.length, aliases: aliases.length };
145
+ }