@ankhorage/devtools 1.11.11 → 1.12.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/README.md +12 -2
- package/dist/tools/agents/index.js +13 -0
- package/dist/tools/eslint/managed.d.ts +6 -0
- package/dist/tools/eslint/managed.js +58 -1
- package/dist/tools/shared/managedFiles.d.ts +1 -0
- package/dist/tools/shared/managedFiles.js +19 -3
- package/dist/tools/skills/assets/ankhorage-project-structure/SKILL.md +33 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -188,7 +188,7 @@ The canonical workflow, VS Code, and skill files are packaged with `@ankhorage/d
|
|
|
188
188
|
|
|
189
189
|
## Managed agent instructions
|
|
190
190
|
|
|
191
|
-
`ankh devtools agents sync` owns the repository-root `AGENTS.md`. The shared instructions are intentionally small and stable. The target repository's package name and description are rendered from `package.json`; the remaining content defines the unconditional current-architecture policy
|
|
191
|
+
`ankh devtools agents sync` owns the repository-root `AGENTS.md`. The shared instructions are intentionally small and stable. The target repository's package name and description are rendered from `package.json`; the remaining content defines the unconditional current-architecture policy, directs structural work to the managed project-structure skill, and requires the canonical build, type, lint, Knip, Changeset, and formatting commands before pull request creation.
|
|
192
192
|
|
|
193
193
|
Only the current Ankhorage architecture is supported. Managed instructions reject deprecated APIs, compatibility aliases, shims, dual old/new paths, historical-state fallbacks, and migrations whose sole purpose is obsolete state. A canonical cross-package change requires affected repositories to update to the latest released public API.
|
|
194
194
|
|
|
@@ -245,7 +245,7 @@ The React profile adds React and React Hooks correctness rules. The React Native
|
|
|
245
245
|
|
|
246
246
|
### Managed ESLint setup and local overrides
|
|
247
247
|
|
|
248
|
-
`ankh devtools eslint sync` centrally owns `eslint.config.mjs` and creates `eslint.local.config.mjs` once.
|
|
248
|
+
`ankh devtools eslint sync` centrally owns `eslint.config.mjs` and creates `eslint.local.config.mjs` once. When the repository has a root `examples/` directory, synchronization also owns `eslint.examples.config.mjs`; repositories without public examples do not receive that file, and synchronization removes the managed wrapper when the directory is removed.
|
|
249
249
|
|
|
250
250
|
The canonical wrapper uses automatic profile detection and appends repository-owned flat-config entries:
|
|
251
251
|
|
|
@@ -267,6 +267,16 @@ export default [
|
|
|
267
267
|
|
|
268
268
|
Use `eslint.local.config.mjs` for narrow repository-specific flat-config overrides, including temporary file-specific migration overrides. On first synchronization, an existing non-canonical `eslint.config.mjs` is preserved as the initial local config before the canonical wrapper is installed. Synchronization never overwrites that local file afterward.
|
|
269
269
|
|
|
270
|
+
Each public example lives in a named directory, such as `examples/basic-usage/*.ts`; example source files do not live directly under `examples/`. The examples wrapper uses root `tsconfig.eslint.json` and `tsconfig.json` when present and discovers TypeScript projects below `examples/`. This covers example directories included only by the root ESLint project as well as standalone applications with their own tsconfig. It applies the same shared policy and appends the same repository-owned local entries.
|
|
271
|
+
|
|
272
|
+
An existing consumer-owned `eslint.examples.config.mjs` requires explicit adoption before synchronization can replace it. Status, dry-run, and sync report an actionable error instead of overwriting or deleting it. Move its repository-specific overrides into `eslint.local.config.mjs`, preserving existing entries and the examples-only file scope; retain custom parser options there when needed. Remove the old examples config only after that transfer, then rerun sync and the examples lint. The generated wrapper carries a Devtools ownership marker and subsequent synchronization updates it normally. Do not mark an old consumer config as managed to bypass this transfer.
|
|
273
|
+
|
|
274
|
+
Repositories can lint their independently runnable examples explicitly:
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
ankhorage-eslint examples --config eslint.examples.config.mjs --max-warnings=0
|
|
278
|
+
```
|
|
279
|
+
|
|
270
280
|
## Prettier
|
|
271
281
|
|
|
272
282
|
`ankh devtools prettier sync` owns `.prettierrc.js`, emits the correct ESM or CommonJS wrapper based on the repository's `package.json` module type, and creates `prettier.local.config.js` once for narrow repository-specific options.
|
|
@@ -39,6 +39,19 @@ For directory ownership, package boundaries, architectural profiles, ports and a
|
|
|
39
39
|
entrypoints, or cross-repository structural work, load and follow
|
|
40
40
|
\`.agents/skills/ankhorage-project-structure/SKILL.md\`.
|
|
41
41
|
|
|
42
|
+
## Pull requests
|
|
43
|
+
|
|
44
|
+
Before creating a pull request, run all of these commands in this order and resolve every failure:
|
|
45
|
+
|
|
46
|
+
\`\`\`sh
|
|
47
|
+
bun run build
|
|
48
|
+
bun run check-types
|
|
49
|
+
bun run lint
|
|
50
|
+
bun run knip:test
|
|
51
|
+
bun run changeset
|
|
52
|
+
bun run format
|
|
53
|
+
\`\`\`
|
|
54
|
+
|
|
42
55
|
## Skill scripts
|
|
43
56
|
|
|
44
57
|
Scripts inside an Agent Skill must always be TypeScript files with the \`.ts\` extension.
|
|
@@ -5,6 +5,12 @@ export declare const eslintManagedFiles: readonly [{
|
|
|
5
5
|
}, {
|
|
6
6
|
readonly relativePath: "eslint.config.mjs";
|
|
7
7
|
readonly contents: "import { createConfig } from '@ankhorage/devtools/eslint';\nimport localConfig from './eslint.local.config.mjs';\n\nconst localEntries = Array.isArray(localConfig) ? localConfig : [localConfig];\n\nexport default [\n ...createConfig({\n files: ['src/**/*.{ts,tsx}'],\n project: ['./tsconfig.json'],\n tsconfigRootDir: import.meta.dirname,\n }),\n ...localEntries,\n];\n";
|
|
8
|
+
}, {
|
|
9
|
+
readonly relativePath: "eslint.examples.config.mjs";
|
|
10
|
+
readonly contents: "// This file is managed by @ankhorage/devtools.\nimport { existsSync } from 'node:fs';\n\nimport { createConfig } from '@ankhorage/devtools/eslint';\nimport localConfig from './eslint.local.config.mjs';\n\nconst exampleFiles = ['examples/**/*.{ts,tsx}'];\nconst localEntries = Array.isArray(localConfig) ? localConfig : [localConfig];\nconst rootProjects = ['./tsconfig.eslint.json', './tsconfig.json'].filter((project) =>\n existsSync(new URL(project, import.meta.url)),\n);\n\nexport default [\n ...createConfig({\n files: exampleFiles,\n project: [...rootProjects, './examples/**/tsconfig.json'],\n tsconfigRootDir: import.meta.dirname,\n }),\n ...localEntries,\n];\n";
|
|
11
|
+
readonly isApplicable: typeof hasExamplesDirectory;
|
|
8
12
|
}];
|
|
9
13
|
declare function renderInitialLocalConfig(targetDirectory: string): Promise<string>;
|
|
14
|
+
/*** Report whether the target repository owns public examples at its root. */
|
|
15
|
+
declare function hasExamplesDirectory(targetDirectory: string): Promise<boolean>;
|
|
10
16
|
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises';
|
|
1
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
3
|
const ESLINT_CONFIG = `import { createConfig } from '@ankhorage/devtools/eslint';
|
|
4
4
|
import localConfig from './eslint.local.config.mjs';
|
|
@@ -16,6 +16,27 @@ export default [
|
|
|
16
16
|
`;
|
|
17
17
|
const EMPTY_LOCAL_CONFIG = `export default [];
|
|
18
18
|
`;
|
|
19
|
+
const EXAMPLES_OWNERSHIP_MARKER = '// This file is managed by @ankhorage/devtools.\n';
|
|
20
|
+
const ESLINT_EXAMPLES_CONFIG = `${EXAMPLES_OWNERSHIP_MARKER}import { existsSync } from 'node:fs';
|
|
21
|
+
|
|
22
|
+
import { createConfig } from '@ankhorage/devtools/eslint';
|
|
23
|
+
import localConfig from './eslint.local.config.mjs';
|
|
24
|
+
|
|
25
|
+
const exampleFiles = ['examples/**/*.{ts,tsx}'];
|
|
26
|
+
const localEntries = Array.isArray(localConfig) ? localConfig : [localConfig];
|
|
27
|
+
const rootProjects = ['./tsconfig.eslint.json', './tsconfig.json'].filter((project) =>
|
|
28
|
+
existsSync(new URL(project, import.meta.url)),
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
export default [
|
|
32
|
+
...createConfig({
|
|
33
|
+
files: exampleFiles,
|
|
34
|
+
project: [...rootProjects, './examples/**/tsconfig.json'],
|
|
35
|
+
tsconfigRootDir: import.meta.dirname,
|
|
36
|
+
}),
|
|
37
|
+
...localEntries,
|
|
38
|
+
];
|
|
39
|
+
`;
|
|
19
40
|
export const eslintManagedFiles = [
|
|
20
41
|
{
|
|
21
42
|
relativePath: 'eslint.local.config.mjs',
|
|
@@ -26,6 +47,11 @@ export const eslintManagedFiles = [
|
|
|
26
47
|
relativePath: 'eslint.config.mjs',
|
|
27
48
|
contents: ESLINT_CONFIG,
|
|
28
49
|
},
|
|
50
|
+
{
|
|
51
|
+
relativePath: 'eslint.examples.config.mjs',
|
|
52
|
+
contents: ESLINT_EXAMPLES_CONFIG,
|
|
53
|
+
isApplicable: hasExamplesDirectory,
|
|
54
|
+
},
|
|
29
55
|
];
|
|
30
56
|
async function renderInitialLocalConfig(targetDirectory) {
|
|
31
57
|
try {
|
|
@@ -42,3 +68,34 @@ async function renderInitialLocalConfig(targetDirectory) {
|
|
|
42
68
|
function isNodeError(error) {
|
|
43
69
|
return error instanceof Error && 'code' in error;
|
|
44
70
|
}
|
|
71
|
+
/*** Report whether the target repository owns public examples at its root. */
|
|
72
|
+
async function hasExamplesDirectory(targetDirectory) {
|
|
73
|
+
await assertExamplesConfigOwnershipAsync(targetDirectory);
|
|
74
|
+
try {
|
|
75
|
+
return (await stat(resolve(targetDirectory, 'examples'))).isDirectory();
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (isNodeError(error) && error.code === 'ENOENT') {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/*** Require explicit adoption of consumer overrides before managing an existing examples config. */
|
|
85
|
+
async function assertExamplesConfigOwnershipAsync(targetDirectory) {
|
|
86
|
+
let contents;
|
|
87
|
+
try {
|
|
88
|
+
contents = await readFile(resolve(targetDirectory, 'eslint.examples.config.mjs'), 'utf8');
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
if (isNodeError(error) && error.code === 'ENOENT')
|
|
92
|
+
return;
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
if (!contents.startsWith(EXAMPLES_OWNERSHIP_MARKER)) {
|
|
96
|
+
throw new Error('Cannot replace consumer-owned eslint.examples.config.mjs. ' +
|
|
97
|
+
'Move its repository-specific overrides into eslint.local.config.mjs, keeping their ' +
|
|
98
|
+
'examples file scope and preserving existing local entries. Then remove the old examples ' +
|
|
99
|
+
'config and rerun sync to create the Devtools-managed wrapper.');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -6,6 +6,7 @@ export interface ManagedFileDefinition {
|
|
|
6
6
|
readonly contents?: string;
|
|
7
7
|
readonly render?: ManagedFileRenderer;
|
|
8
8
|
readonly mode?: ManagedFileMode;
|
|
9
|
+
readonly isApplicable?: (targetDirectory: string) => Promise<boolean> | boolean;
|
|
9
10
|
}
|
|
10
11
|
type ManagedFileState = 'current' | 'missing' | 'obsolete' | 'outdated';
|
|
11
12
|
export type ManagedFileSyncAction = 'unchanged' | 'created' | 'removed' | 'updated' | 'would-create' | 'would-remove' | 'would-update';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
1
|
+
import { mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { dirname, resolve } from 'node:path';
|
|
3
3
|
export async function resolveManagedTargetDirectory(cwd, requestedPath) {
|
|
4
4
|
const targetDirectory = resolve(cwd, requestedPath ?? '.');
|
|
@@ -15,10 +15,14 @@ export async function resolveManagedTargetDirectory(cwd, requestedPath) {
|
|
|
15
15
|
return targetDirectory;
|
|
16
16
|
}
|
|
17
17
|
export async function inspectManagedFiles(targetDirectory, definitions) {
|
|
18
|
-
|
|
18
|
+
const statuses = await Promise.all(definitions.map(async (definition) => {
|
|
19
19
|
const targetPath = resolve(targetDirectory, definition.relativePath);
|
|
20
|
+
const isApplicable = await (definition.isApplicable?.(targetDirectory) ?? true);
|
|
20
21
|
try {
|
|
21
22
|
const targetContents = await readFile(targetPath, 'utf8');
|
|
23
|
+
if (!isApplicable) {
|
|
24
|
+
return { relativePath: definition.relativePath, state: 'obsolete' };
|
|
25
|
+
}
|
|
22
26
|
if ((definition.mode ?? 'replace') === 'create-only') {
|
|
23
27
|
return { relativePath: definition.relativePath, state: 'current' };
|
|
24
28
|
}
|
|
@@ -30,11 +34,14 @@ export async function inspectManagedFiles(targetDirectory, definitions) {
|
|
|
30
34
|
}
|
|
31
35
|
catch (error) {
|
|
32
36
|
if (isMissingFileError(error)) {
|
|
33
|
-
return
|
|
37
|
+
return isApplicable
|
|
38
|
+
? { relativePath: definition.relativePath, state: 'missing' }
|
|
39
|
+
: undefined;
|
|
34
40
|
}
|
|
35
41
|
throw new Error(`Failed to inspect managed file: ${targetPath}`, { cause: error });
|
|
36
42
|
}
|
|
37
43
|
}));
|
|
44
|
+
return statuses.filter((status) => status !== undefined);
|
|
38
45
|
}
|
|
39
46
|
export async function syncManagedFiles(targetDirectory, definitions, options) {
|
|
40
47
|
const statuses = await inspectManagedFiles(targetDirectory, definitions);
|
|
@@ -54,6 +61,15 @@ async function syncManagedFile(targetDirectory, status, definitionsByPath, optio
|
|
|
54
61
|
if (definition === undefined) {
|
|
55
62
|
throw new Error(`Missing managed file definition for ${status.relativePath}.`);
|
|
56
63
|
}
|
|
64
|
+
if (status.state === 'obsolete') {
|
|
65
|
+
if (!options.dryRun) {
|
|
66
|
+
await rm(resolve(targetDirectory, definition.relativePath));
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
relativePath: status.relativePath,
|
|
70
|
+
action: options.dryRun ? 'would-remove' : 'removed',
|
|
71
|
+
};
|
|
72
|
+
}
|
|
57
73
|
if (options.dryRun) {
|
|
58
74
|
return {
|
|
59
75
|
relativePath: status.relativePath,
|
|
@@ -31,6 +31,18 @@ If `hexagonal-architecture` is missing or unreadable, stop immediately and repor
|
|
|
31
31
|
Cannot continue: the required repository skill `hexagonal-architecture` is missing or unreadable at `.agents/skills/hexagonal-architecture/SKILL.md`. Synchronize the repository skills from `@ankhorage/devtools` and retry.
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
+
## Repository-root examples
|
|
35
|
+
|
|
36
|
+
`examples/` is a generally valid repository-root folder in every repository covered by this skill.
|
|
37
|
+
Use it for complete, intentional, user-facing examples that people can inspect, copy, install, and
|
|
38
|
+
run independently of a monorepo or internal fixture layout.
|
|
39
|
+
|
|
40
|
+
Each example lives in a named subdirectory, such as `examples/basic-usage/*.ts`. Do not put example
|
|
41
|
+
source files directly under `examples/`.
|
|
42
|
+
|
|
43
|
+
Test-only fixtures remain owned by the applicable test structure. Do not relabel fixtures as public
|
|
44
|
+
examples merely to bypass repository structure rules.
|
|
45
|
+
|
|
34
46
|
## Required source layout
|
|
35
47
|
|
|
36
48
|
Every repository provides `src/features/`. It lists the repository's actual product capabilities;
|
|
@@ -64,6 +76,7 @@ src/
|
|
|
64
76
|
utils/
|
|
65
77
|
types/
|
|
66
78
|
<topic>.ts
|
|
79
|
+
constants.ts
|
|
67
80
|
utils/
|
|
68
81
|
```
|
|
69
82
|
|
|
@@ -142,6 +155,26 @@ For example, `selectRoute.ts` can own a non-exported `SelectRouteInput` directly
|
|
|
142
155
|
Types used by several local navigation operations belong together in `src/types/navigation.ts`.
|
|
143
156
|
A navigation binding exchanged by Studio and Navigator belongs in `@ankhorage/contracts/navigator`.
|
|
144
157
|
|
|
158
|
+
## Constant ownership
|
|
159
|
+
|
|
160
|
+
Constants are static declarations, not utility implementations. Do not create one exported
|
|
161
|
+
constant per constant-named file under `utils/`.
|
|
162
|
+
|
|
163
|
+
1. **Used by one implementation module:** keep the constant private in the module that owns it.
|
|
164
|
+
2. **Reused only inside a feature:** group related constants in that feature's `constants.ts`.
|
|
165
|
+
3. **Shared across features in one package:** group related package metadata, static policy values,
|
|
166
|
+
and other constants in `src/constants.ts`.
|
|
167
|
+
|
|
168
|
+
A `constants.ts` module may export multiple related constants. Keep it cohesive by ownership and
|
|
169
|
+
purpose; it is not a package-wide catch-all. Split constants when they have different owners, not
|
|
170
|
+
merely to create one file per export.
|
|
171
|
+
|
|
172
|
+
For example, Navigator's
|
|
173
|
+
[`src/utils/NAVIGATOR_PACKAGE_METADATA.ts`](https://github.com/ankhorage/navigator/blob/main/src/utils/NAVIGATOR_PACKAGE_METADATA.ts)
|
|
174
|
+
and
|
|
175
|
+
[`src/utils/NAVIGATOR_ROUTER_POLICY.ts`](https://github.com/ankhorage/navigator/blob/main/src/utils/NAVIGATOR_ROUTER_POLICY.ts)
|
|
176
|
+
belong together in `ankhorage/navigator/src/constants.ts`.
|
|
177
|
+
|
|
145
178
|
## Utilities
|
|
146
179
|
|
|
147
180
|
`utils/` is the only utility directory name. Do not create `shared/`, `helper/`, `helpers/`,
|
package/package.json
CHANGED