@ankhorage/devtools 1.8.5 → 1.9.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/README.md +29 -2
- package/dist/cli/commands.d.ts +2 -2
- package/dist/cli/commands.js +4 -0
- package/dist/cli/index.d.ts +2 -2
- package/dist/cli/index.js +5 -5
- package/dist/cli/runRepositoryCommand.js +20 -0
- package/dist/internal/readmeDocs.js +5 -0
- package/dist/tools/agents/index.d.ts +6 -0
- package/dist/tools/agents/index.js +70 -0
- package/dist/tools/shared/managedFiles.d.ts +2 -2
- package/dist/tools/skills/assets/ankhorage-coding-rules/SKILL.md +81 -0
- package/dist/tools/skills/assets/ankhorage-coding-rules/agents/openai.yaml +7 -0
- package/dist/tools/skills/assets/ankhorage-project-structure/SKILL.md +90 -0
- package/dist/tools/skills/assets/ankhorage-project-structure/agents/openai.yaml +7 -0
- package/dist/tools/skills/assets/ankhorage-project-structure/references/cli.md +117 -0
- package/dist/tools/skills/assets/ankhorage-project-structure/references/expo-apps.md +43 -0
- package/dist/tools/skills/assets/ankhorage-project-structure/references/hexagonal-architecture.md +120 -0
- package/dist/tools/skills/assets/ankhorage-project-structure/references/migration.md +61 -0
- package/dist/tools/skills/assets/ankhorage-project-structure/references/repository-profiles.md +91 -0
- package/dist/tools/skills/assets/ankhorage-project-structure/references/skill-distribution.md +115 -0
- package/dist/tools/skills/assets/ankhorage-project-structure/references/studio.md +134 -0
- package/dist/tools/skills/assets/ankhorage-project-structure/references/ui-libraries.md +79 -0
- package/dist/tools/skills/assets/ankhorage-project-structure/references/utilities.md +62 -0
- package/dist/tools/skills/managed.d.ts +5 -0
- package/dist/tools/skills/managed.js +224 -0
- package/dist/tools/skills/manifest.d.ts +16 -0
- package/dist/tools/skills/manifest.js +95 -0
- package/package.json +10 -6
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Utility Ownership and Unknown-Value Narrowing
|
|
2
|
+
|
|
3
|
+
Classify a helper before creating or moving it:
|
|
4
|
+
|
|
5
|
+
```text
|
|
6
|
+
cross-repository, framework-neutral -> @ankhorage/utility/<category>
|
|
7
|
+
cross-domain inside one package -> src/utils/<functionName>.ts
|
|
8
|
+
one domain only -> src/<domain>/utils/<functionName>.ts
|
|
9
|
+
semantic/domain behavior -> owning domain, not utils
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Do not retain local copies while waiting for a Utility release. Follow the Utility PR, merge,
|
|
13
|
+
release, dependency-update sequence defined by `ankhorage-package-structure`.
|
|
14
|
+
|
|
15
|
+
## Strong cross-repository candidates
|
|
16
|
+
|
|
17
|
+
Generic object and unknown-value operations commonly belong in focused Utility subpaths:
|
|
18
|
+
|
|
19
|
+
```text
|
|
20
|
+
@ankhorage/utility/object
|
|
21
|
+
readOwnProperty
|
|
22
|
+
setOwnProperty
|
|
23
|
+
deleteOwnProperty
|
|
24
|
+
isRecord
|
|
25
|
+
|
|
26
|
+
@ankhorage/utility/value
|
|
27
|
+
asString
|
|
28
|
+
asNumber
|
|
29
|
+
asRecord
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Create public utilities only after confirming repetition and stable semantics across repositories.
|
|
33
|
+
Do not move feature payload parsers, provider response semantics, or domain validation merely
|
|
34
|
+
because they contain small type guards.
|
|
35
|
+
|
|
36
|
+
## Absence and failure semantics
|
|
37
|
+
|
|
38
|
+
Use consistent meanings:
|
|
39
|
+
|
|
40
|
+
- `isRecord(value)` returns a type-guard boolean.
|
|
41
|
+
- `asRecord(value)` returns the narrowed record or `undefined`.
|
|
42
|
+
- `asString(value)` returns the string or `undefined`.
|
|
43
|
+
- `readOwnProperty(target, key)` returns the owned value or `undefined`.
|
|
44
|
+
- `parse<DomainValue>(value)` returns an explicit domain parse result when callers need to
|
|
45
|
+
distinguish absent, invalid, and valid values.
|
|
46
|
+
|
|
47
|
+
Reserve `null` for an intentional domain or serialized value. Do not use `null` as the generic
|
|
48
|
+
failure result for type narrowing when `undefined` expresses absence. When failure details matter,
|
|
49
|
+
use a discriminated result instead of alternating between `null` and `undefined`.
|
|
50
|
+
|
|
51
|
+
## Avoid false utilities
|
|
52
|
+
|
|
53
|
+
Keep these with their owner:
|
|
54
|
+
|
|
55
|
+
- `readProjectDeployConfig`
|
|
56
|
+
- `readSecretPayload`
|
|
57
|
+
- `parseStudioModuleState`
|
|
58
|
+
- API response validation with feature-specific error policy
|
|
59
|
+
- helpers that encode manifest, route, auth, deploy, or provider semantics
|
|
60
|
+
|
|
61
|
+
A utility is not a place to hide complexity or shorten a file. It must represent a reusable,
|
|
62
|
+
cohesive capability with stable behavior.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ManagedFileStatus, ManagedFileSyncResult } from '../shared/managedFiles.js';
|
|
2
|
+
export declare function inspectManagedSkills(targetDirectory: string, devtoolsVersion: string): Promise<readonly ManagedFileStatus[]>;
|
|
3
|
+
export declare function syncManagedSkills(targetDirectory: string, devtoolsVersion: string, options: {
|
|
4
|
+
readonly dryRun: boolean;
|
|
5
|
+
}): Promise<readonly ManagedFileSyncResult[]>;
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { lstat, mkdir, readdir, readFile, rmdir, unlink, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join, sep } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { assertManagedSkillPath, createManifestContents, isNodeError, MANIFEST_PATH, readManagedSkillsManifest, resolveManagedPath, SKILLS_ROOT, } from './manifest.js';
|
|
5
|
+
const CANONICAL_SKILL_NAMES = ['ankhorage-coding-rules', 'ankhorage-project-structure'];
|
|
6
|
+
export async function inspectManagedSkills(targetDirectory, devtoolsVersion) {
|
|
7
|
+
return (await createManagedSkillPlan(targetDirectory, devtoolsVersion)).statuses;
|
|
8
|
+
}
|
|
9
|
+
export async function syncManagedSkills(targetDirectory, devtoolsVersion, options) {
|
|
10
|
+
const plan = await createManagedSkillPlan(targetDirectory, devtoolsVersion);
|
|
11
|
+
const results = [];
|
|
12
|
+
for (const status of plan.statuses) {
|
|
13
|
+
const action = getSyncAction(status, options.dryRun);
|
|
14
|
+
if (!options.dryRun) {
|
|
15
|
+
await applySkillAction(targetDirectory, status, action, plan);
|
|
16
|
+
}
|
|
17
|
+
results.push({ relativePath: status.relativePath, action });
|
|
18
|
+
}
|
|
19
|
+
return results;
|
|
20
|
+
}
|
|
21
|
+
async function applySkillAction(targetDirectory, status, action, plan) {
|
|
22
|
+
if (action === 'unchanged') {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const targetPath = resolveManagedPath(targetDirectory, status.relativePath);
|
|
26
|
+
await assertNoSymlinkSegments(targetDirectory, status.relativePath);
|
|
27
|
+
if (action === 'removed') {
|
|
28
|
+
await unlink(targetPath);
|
|
29
|
+
await removeEmptyParentDirectories(targetDirectory, dirname(targetPath));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
await mkdir(dirname(targetPath), { recursive: true });
|
|
33
|
+
const contents = status.relativePath === MANIFEST_PATH
|
|
34
|
+
? plan.manifestContents
|
|
35
|
+
: plan.contentsByPath.get(status.relativePath);
|
|
36
|
+
if (contents === undefined) {
|
|
37
|
+
throw new Error(`Missing canonical skill contents for ${status.relativePath}.`);
|
|
38
|
+
}
|
|
39
|
+
await writeFile(targetPath, contents);
|
|
40
|
+
}
|
|
41
|
+
async function assertNoSymlinkSegments(targetDirectory, relativePath) {
|
|
42
|
+
let currentPath = targetDirectory;
|
|
43
|
+
for (const segment of relativePath.split('/')) {
|
|
44
|
+
currentPath = join(currentPath, segment);
|
|
45
|
+
try {
|
|
46
|
+
const stats = await lstat(currentPath);
|
|
47
|
+
if (stats.isSymbolicLink()) {
|
|
48
|
+
throw new Error(`Managed skill path must not contain symbolic links: ${relativePath}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (isNodeError(error) && error.code === 'ENOENT') {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function collectCanonicalSkillFiles() {
|
|
60
|
+
const contentsByPath = new Map();
|
|
61
|
+
for (const skillName of CANONICAL_SKILL_NAMES) {
|
|
62
|
+
const sourceDirectory = fileURLToPath(new URL(`./assets/${skillName}/`, import.meta.url));
|
|
63
|
+
const files = await readDirectoryFiles(sourceDirectory);
|
|
64
|
+
assertSkillName(skillName, files);
|
|
65
|
+
for (const [skillRelativePath, contents] of files) {
|
|
66
|
+
contentsByPath.set(`${SKILLS_ROOT}/${skillName}/${skillRelativePath}`, contents);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return contentsByPath;
|
|
70
|
+
}
|
|
71
|
+
async function collectObsoletePaths(targetDirectory, desiredPaths, previousManifest) {
|
|
72
|
+
const obsoletePaths = new Set();
|
|
73
|
+
for (const skillName of CANONICAL_SKILL_NAMES) {
|
|
74
|
+
const skillRoot = `${SKILLS_ROOT}/${skillName}`;
|
|
75
|
+
for (const existingPath of await readTargetDirectoryFiles(targetDirectory, skillRoot)) {
|
|
76
|
+
if (!desiredPaths.has(existingPath)) {
|
|
77
|
+
obsoletePaths.add(existingPath);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (previousManifest !== null) {
|
|
82
|
+
for (const [skillName, entry] of Object.entries(previousManifest.skills)) {
|
|
83
|
+
if (CANONICAL_SKILL_NAMES.includes(skillName)) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
for (const managedPath of Object.keys(entry.files)) {
|
|
87
|
+
if (await isRegularManagedFile(targetDirectory, managedPath)) {
|
|
88
|
+
obsoletePaths.add(managedPath);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return [...obsoletePaths].sort();
|
|
94
|
+
}
|
|
95
|
+
async function createManagedSkillPlan(targetDirectory, devtoolsVersion) {
|
|
96
|
+
const previousManifest = await readManagedSkillsManifest(targetDirectory);
|
|
97
|
+
const contentsByPath = await collectCanonicalSkillFiles();
|
|
98
|
+
const desiredPaths = new Set(contentsByPath.keys());
|
|
99
|
+
const manifestContents = createManifestContents(contentsByPath, devtoolsVersion, CANONICAL_SKILL_NAMES);
|
|
100
|
+
const statuses = [];
|
|
101
|
+
for (const [relativePath, contents] of [...contentsByPath.entries()].sort()) {
|
|
102
|
+
statuses.push(await inspectDesiredFile(targetDirectory, relativePath, contents));
|
|
103
|
+
}
|
|
104
|
+
for (const relativePath of await collectObsoletePaths(targetDirectory, desiredPaths, previousManifest)) {
|
|
105
|
+
statuses.push({ relativePath, state: 'obsolete' });
|
|
106
|
+
}
|
|
107
|
+
statuses.push(await inspectDesiredFile(targetDirectory, MANIFEST_PATH, Buffer.from(manifestContents)));
|
|
108
|
+
return { contentsByPath, manifestContents, statuses };
|
|
109
|
+
}
|
|
110
|
+
function getSyncAction(status, dryRun) {
|
|
111
|
+
if (status.state === 'current') {
|
|
112
|
+
return 'unchanged';
|
|
113
|
+
}
|
|
114
|
+
if (status.state === 'obsolete') {
|
|
115
|
+
return dryRun ? 'would-remove' : 'removed';
|
|
116
|
+
}
|
|
117
|
+
if (status.state === 'missing') {
|
|
118
|
+
return dryRun ? 'would-create' : 'created';
|
|
119
|
+
}
|
|
120
|
+
return dryRun ? 'would-update' : 'updated';
|
|
121
|
+
}
|
|
122
|
+
async function inspectDesiredFile(targetDirectory, relativePath, canonicalContents) {
|
|
123
|
+
await assertNoSymlinkSegments(targetDirectory, relativePath);
|
|
124
|
+
try {
|
|
125
|
+
const targetContents = await readFile(resolveManagedPath(targetDirectory, relativePath));
|
|
126
|
+
return {
|
|
127
|
+
relativePath,
|
|
128
|
+
state: Buffer.compare(targetContents, canonicalContents) === 0 ? 'current' : 'outdated',
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
if (isNodeError(error) && error.code === 'ENOENT') {
|
|
133
|
+
return { relativePath, state: 'missing' };
|
|
134
|
+
}
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async function isRegularManagedFile(targetDirectory, relativePath) {
|
|
139
|
+
assertManagedSkillPath(relativePath);
|
|
140
|
+
await assertNoSymlinkSegments(targetDirectory, relativePath);
|
|
141
|
+
try {
|
|
142
|
+
return (await lstat(resolveManagedPath(targetDirectory, relativePath))).isFile();
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
if (isNodeError(error) && error.code === 'ENOENT') {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async function readDirectoryFiles(directory, prefix = '') {
|
|
152
|
+
const files = new Map();
|
|
153
|
+
const entries = (await readdir(directory, { withFileTypes: true })).sort((left, right) => left.name.localeCompare(right.name));
|
|
154
|
+
for (const entry of entries) {
|
|
155
|
+
const entryPath = join(directory, entry.name);
|
|
156
|
+
const relativePath = prefix === '' ? entry.name : `${prefix}/${entry.name}`;
|
|
157
|
+
if (entry.isDirectory()) {
|
|
158
|
+
for (const nestedFile of await readDirectoryFiles(entryPath, relativePath)) {
|
|
159
|
+
files.set(...nestedFile);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else if (entry.isFile()) {
|
|
163
|
+
files.set(relativePath, await readFile(entryPath));
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
throw new Error(`Canonical skill assets must contain only files and directories: ${entryPath}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return files;
|
|
170
|
+
}
|
|
171
|
+
async function readTargetDirectoryFiles(targetDirectory, relativeDirectory) {
|
|
172
|
+
await assertNoSymlinkSegments(targetDirectory, relativeDirectory);
|
|
173
|
+
const directory = resolveManagedPath(targetDirectory, relativeDirectory);
|
|
174
|
+
let entries;
|
|
175
|
+
try {
|
|
176
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
if (isNodeError(error) && error.code === 'ENOENT') {
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
const files = [];
|
|
185
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
186
|
+
const relativePath = `${relativeDirectory}/${entry.name}`;
|
|
187
|
+
if (entry.isDirectory()) {
|
|
188
|
+
files.push(...(await readTargetDirectoryFiles(targetDirectory, relativePath)));
|
|
189
|
+
}
|
|
190
|
+
else if (entry.isFile()) {
|
|
191
|
+
files.push(relativePath);
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
throw new Error(`Managed skill trees must not contain symbolic links: ${relativePath}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return files;
|
|
198
|
+
}
|
|
199
|
+
async function removeEmptyParentDirectories(targetDirectory, initialDirectory) {
|
|
200
|
+
const stopDirectory = resolveManagedPath(targetDirectory, SKILLS_ROOT);
|
|
201
|
+
let directory = initialDirectory;
|
|
202
|
+
while (directory.startsWith(`${stopDirectory}${sep}`)) {
|
|
203
|
+
try {
|
|
204
|
+
await rmdir(directory);
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
if (isNodeError(error) && ['ENOENT', 'ENOTEMPTY'].includes(error.code ?? '')) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
directory = dirname(directory);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function assertSkillName(expectedName, files) {
|
|
216
|
+
const skillFile = files.get('SKILL.md');
|
|
217
|
+
if (skillFile === undefined) {
|
|
218
|
+
throw new Error(`Canonical skill is missing SKILL.md: ${expectedName}`);
|
|
219
|
+
}
|
|
220
|
+
const match = /^---\n[\s\S]*?^name:\s*(.+)$/mu.exec(Buffer.from(skillFile).toString('utf8'));
|
|
221
|
+
if (match?.[1]?.trim() !== expectedName) {
|
|
222
|
+
throw new Error(`Canonical skill directory and frontmatter names differ: ${expectedName}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare const MANIFEST_PATH = ".agents/.devtools-manifest.json";
|
|
2
|
+
export declare const SKILLS_ROOT = ".agents/skills";
|
|
3
|
+
interface ManagedSkillManifestEntry {
|
|
4
|
+
readonly files: Readonly<Record<string, string>>;
|
|
5
|
+
}
|
|
6
|
+
export interface ManagedSkillsManifest {
|
|
7
|
+
readonly schemaVersion: 1;
|
|
8
|
+
readonly sourceDevtoolsVersion: string;
|
|
9
|
+
readonly skills: Readonly<Record<string, ManagedSkillManifestEntry>>;
|
|
10
|
+
}
|
|
11
|
+
export declare function createManifestContents(contentsByPath: ReadonlyMap<string, Uint8Array>, devtoolsVersion: string, skillNames: readonly string[]): string;
|
|
12
|
+
export declare function readManagedSkillsManifest(targetDirectory: string): Promise<ManagedSkillsManifest | null>;
|
|
13
|
+
export declare function assertManagedSkillPath(relativePath: string): void;
|
|
14
|
+
export declare function resolveManagedPath(targetDirectory: string, relativePath: string): string;
|
|
15
|
+
export declare function isNodeError(error: unknown): error is NodeJS.ErrnoException;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { posix, relative, resolve } from 'node:path';
|
|
4
|
+
export const MANIFEST_PATH = '.agents/.devtools-manifest.json';
|
|
5
|
+
export const SKILLS_ROOT = '.agents/skills';
|
|
6
|
+
const MANIFEST_SCHEMA_VERSION = 1;
|
|
7
|
+
export function createManifestContents(contentsByPath, devtoolsVersion, skillNames) {
|
|
8
|
+
const skills = Object.fromEntries(skillNames.map((skillName) => {
|
|
9
|
+
const prefix = `${SKILLS_ROOT}/${skillName}/`;
|
|
10
|
+
const files = Object.fromEntries([...contentsByPath.entries()]
|
|
11
|
+
.filter(([relativePath]) => relativePath.startsWith(prefix))
|
|
12
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
13
|
+
.map(([relativePath, contents]) => [relativePath, sha256(contents)]));
|
|
14
|
+
return [skillName, { files }];
|
|
15
|
+
}));
|
|
16
|
+
const manifest = {
|
|
17
|
+
schemaVersion: MANIFEST_SCHEMA_VERSION,
|
|
18
|
+
sourceDevtoolsVersion: devtoolsVersion,
|
|
19
|
+
skills,
|
|
20
|
+
};
|
|
21
|
+
return `${JSON.stringify(manifest, null, 2)}\n`;
|
|
22
|
+
}
|
|
23
|
+
export async function readManagedSkillsManifest(targetDirectory) {
|
|
24
|
+
try {
|
|
25
|
+
const contents = await readFile(resolveManagedPath(targetDirectory, MANIFEST_PATH), 'utf8');
|
|
26
|
+
return parseManagedSkillsManifest(JSON.parse(contents));
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (isNodeError(error) && error.code === 'ENOENT') {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function assertManagedSkillPath(relativePath) {
|
|
36
|
+
assertSafeRelativePath(relativePath);
|
|
37
|
+
if (!relativePath.startsWith(`${SKILLS_ROOT}/`)) {
|
|
38
|
+
throw new Error(`Managed skill manifest path is outside ${SKILLS_ROOT}: ${relativePath}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function resolveManagedPath(targetDirectory, relativePath) {
|
|
42
|
+
assertSafeRelativePath(relativePath);
|
|
43
|
+
const targetPath = resolve(targetDirectory, ...relativePath.split('/'));
|
|
44
|
+
const relativeTarget = relative(resolve(targetDirectory), targetPath);
|
|
45
|
+
if (relativeTarget.startsWith('..') || relativeTarget === '') {
|
|
46
|
+
throw new Error(`Managed path escapes the target repository: ${relativePath}`);
|
|
47
|
+
}
|
|
48
|
+
return targetPath;
|
|
49
|
+
}
|
|
50
|
+
export function isNodeError(error) {
|
|
51
|
+
return error instanceof Error && 'code' in error;
|
|
52
|
+
}
|
|
53
|
+
function parseManagedSkillsManifest(value) {
|
|
54
|
+
if (!isRecord(value) || value.schemaVersion !== MANIFEST_SCHEMA_VERSION) {
|
|
55
|
+
throw new Error(`Unsupported managed skills manifest at ${MANIFEST_PATH}.`);
|
|
56
|
+
}
|
|
57
|
+
if (typeof value.sourceDevtoolsVersion !== 'string' || !isRecord(value.skills)) {
|
|
58
|
+
throw new Error(`Invalid managed skills manifest at ${MANIFEST_PATH}.`);
|
|
59
|
+
}
|
|
60
|
+
const skillEntries = Object.entries(value.skills).map(([skillName, rawEntry]) => {
|
|
61
|
+
if (!isSafeName(skillName) || !isRecord(rawEntry) || !isRecord(rawEntry.files)) {
|
|
62
|
+
throw new Error(`Invalid managed skill entry in ${MANIFEST_PATH}: ${skillName}`);
|
|
63
|
+
}
|
|
64
|
+
const files = Object.fromEntries(Object.entries(rawEntry.files).map(([relativePath, hash]) => {
|
|
65
|
+
assertManagedSkillPath(relativePath);
|
|
66
|
+
if (typeof hash !== 'string' || !/^sha256:[a-f0-9]{64}$/u.test(hash)) {
|
|
67
|
+
throw new Error(`Invalid managed skill hash in ${MANIFEST_PATH}: ${relativePath}`);
|
|
68
|
+
}
|
|
69
|
+
return [relativePath, hash];
|
|
70
|
+
}));
|
|
71
|
+
return [skillName, { files }];
|
|
72
|
+
});
|
|
73
|
+
return {
|
|
74
|
+
schemaVersion: MANIFEST_SCHEMA_VERSION,
|
|
75
|
+
sourceDevtoolsVersion: value.sourceDevtoolsVersion,
|
|
76
|
+
skills: Object.fromEntries(skillEntries),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function assertSafeRelativePath(relativePath) {
|
|
80
|
+
if (relativePath === '' ||
|
|
81
|
+
relativePath !== posix.normalize(relativePath) ||
|
|
82
|
+
relativePath.startsWith('/') ||
|
|
83
|
+
relativePath.split('/').some((segment) => segment === '..' || segment === '')) {
|
|
84
|
+
throw new Error(`Unsafe managed path: ${relativePath}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function isRecord(value) {
|
|
88
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
89
|
+
}
|
|
90
|
+
function isSafeName(value) {
|
|
91
|
+
return /^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(value);
|
|
92
|
+
}
|
|
93
|
+
function sha256(contents) {
|
|
94
|
+
return `sha256:${createHash('sha256').update(contents).digest('hex')}`;
|
|
95
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ankhorage/devtools",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "Shared development tools and repository standards for Ankhorage",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/ankhorage/devtools#readme",
|
|
@@ -25,6 +25,10 @@
|
|
|
25
25
|
"devtools.knip",
|
|
26
26
|
"devtools.sync",
|
|
27
27
|
"devtools.status",
|
|
28
|
+
"devtools.agents.sync",
|
|
29
|
+
"devtools.agents.status",
|
|
30
|
+
"devtools.skills.sync",
|
|
31
|
+
"devtools.skills.status",
|
|
28
32
|
"devtools.eslint.sync",
|
|
29
33
|
"devtools.eslint.status",
|
|
30
34
|
"devtools.prettier.sync",
|
|
@@ -85,7 +89,7 @@
|
|
|
85
89
|
"examples"
|
|
86
90
|
],
|
|
87
91
|
"scripts": {
|
|
88
|
-
"build": "rm -rf dist tsconfig.tsbuildinfo && tsc && mkdir -p dist/tools/prettier dist/tools/workflows dist/tools/vscode && cp src/tools/prettier/index.cjs dist/tools/prettier/index.cjs && cp -R src/tools/workflows/files dist/tools/workflows/files && cp -R src/tools/vscode/files dist/tools/vscode/files",
|
|
92
|
+
"build": "rm -rf dist tsconfig.tsbuildinfo && tsc && mkdir -p dist/tools/prettier dist/tools/workflows dist/tools/vscode dist/tools/skills && cp src/tools/prettier/index.cjs dist/tools/prettier/index.cjs && cp -R src/tools/workflows/files dist/tools/workflows/files && cp -R src/tools/vscode/files dist/tools/vscode/files && cp -R src/tools/skills/assets dist/tools/skills/assets",
|
|
89
93
|
"typecheck": "bun x tsc --noEmit -p tsconfig.test.json",
|
|
90
94
|
"doctor": "ankhorage-doctor validate .",
|
|
91
95
|
"knip:check": "knip",
|
|
@@ -102,7 +106,7 @@
|
|
|
102
106
|
},
|
|
103
107
|
"dependencies": {
|
|
104
108
|
"@ankhorage/utility": "^0.2.0",
|
|
105
|
-
"@changesets/cli": "^
|
|
109
|
+
"@changesets/cli": "^3.0.1",
|
|
106
110
|
"@eslint/compat": "^2.1.0",
|
|
107
111
|
"@eslint/js": "^10.0.1",
|
|
108
112
|
"eslint": "^10.9.1",
|
|
@@ -113,7 +117,7 @@
|
|
|
113
117
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
114
118
|
"eslint-plugin-react-native": "^5.0.0",
|
|
115
119
|
"eslint-plugin-security": "^4.0.1",
|
|
116
|
-
"eslint-plugin-simple-import-sort": "^
|
|
120
|
+
"eslint-plugin-simple-import-sort": "^14.0.0",
|
|
117
121
|
"eslint-plugin-unused-imports": "^4.4.1",
|
|
118
122
|
"knip": "^6.33.0",
|
|
119
123
|
"prettier": "^3.9.6",
|
|
@@ -123,8 +127,8 @@
|
|
|
123
127
|
"@ankhorage/ankh": "^0.8.4",
|
|
124
128
|
"@ankhorage/doctor": "0.10.4",
|
|
125
129
|
"@types/bun": "^1.4.0",
|
|
126
|
-
"@types/node": "^
|
|
127
|
-
"typescript": "
|
|
130
|
+
"@types/node": "^26.4.0",
|
|
131
|
+
"typescript": "~6.0.3"
|
|
128
132
|
},
|
|
129
133
|
"packageManager": "bun@1.4.0"
|
|
130
134
|
}
|