@ankhorage/devtools 1.14.7 → 1.15.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.
Files changed (58) hide show
  1. package/README.md +159 -2
  2. package/dist/apmRelease.d.ts +4 -0
  3. package/dist/apmRelease.js +3 -0
  4. package/dist/cli/bin/apm-release.d.ts +2 -0
  5. package/dist/cli/bin/apm-release.js +15 -0
  6. package/dist/cli/commands/apm/sync.d.ts +5 -0
  7. package/dist/cli/commands/apm/sync.js +5 -0
  8. package/dist/cli/commands/apm/validate.d.ts +5 -0
  9. package/dist/cli/commands/apm/validate.js +5 -0
  10. package/dist/cli/commands.d.ts +6 -2
  11. package/dist/cli/commands.js +15 -1
  12. package/dist/cli/index.d.ts +2 -2
  13. package/dist/cli/index.js +9 -0
  14. package/dist/cli/runProviderCommand.js +5 -0
  15. package/dist/features/apm-release-validation/adapters/inbound/runApmReleaseCommandAsync.d.ts +10 -0
  16. package/dist/features/apm-release-validation/adapters/inbound/runApmReleaseCommandAsync.js +51 -0
  17. package/dist/features/apm-release-validation/adapters/outbound/createApmReleaseDescriptorPort.d.ts +3 -0
  18. package/dist/features/apm-release-validation/adapters/outbound/createApmReleaseDescriptorPort.js +31 -0
  19. package/dist/features/apm-release-validation/adapters/outbound/inspectPackedApmReleaseCandidateAsync.d.ts +3 -0
  20. package/dist/features/apm-release-validation/adapters/outbound/inspectPackedApmReleaseCandidateAsync.js +84 -0
  21. package/dist/features/apm-release-validation/adapters/outbound/probePackedApmExtensionAsync.d.ts +11 -0
  22. package/dist/features/apm-release-validation/adapters/outbound/probePackedApmExtensionAsync.js +82 -0
  23. package/dist/features/apm-release-validation/adapters/outbound/readApmReleaseManifestAsync.d.ts +2 -0
  24. package/dist/features/apm-release-validation/adapters/outbound/readApmReleaseManifestAsync.js +9 -0
  25. package/dist/features/apm-release-validation/adapters/outbound/resolveApmReleaseCommandAsync.d.ts +2 -0
  26. package/dist/features/apm-release-validation/adapters/outbound/resolveApmReleaseCommandAsync.js +18 -0
  27. package/dist/features/apm-release-validation/application/synchronizeApmReleaseMetadataAsync.d.ts +3 -0
  28. package/dist/features/apm-release-validation/application/synchronizeApmReleaseMetadataAsync.js +29 -0
  29. package/dist/features/apm-release-validation/application/validateApmReleaseCandidate.d.ts +11 -0
  30. package/dist/features/apm-release-validation/application/validateApmReleaseCandidate.js +76 -0
  31. package/dist/features/apm-release-validation/composition/synchronizeApmReleaseDescriptorAsync.d.ts +2 -0
  32. package/dist/features/apm-release-validation/composition/synchronizeApmReleaseDescriptorAsync.js +6 -0
  33. package/dist/features/apm-release-validation/composition/validatePackedApmReleaseAsync.d.ts +12 -0
  34. package/dist/features/apm-release-validation/composition/validatePackedApmReleaseAsync.js +14 -0
  35. package/dist/features/apm-release-validation/utils/readApmReleaseMetadata.d.ts +2 -0
  36. package/dist/features/apm-release-validation/utils/readApmReleaseMetadata.js +6 -0
  37. package/dist/internal/readmeDocs.js +1 -1
  38. package/dist/owner/synchronizeRenovateOwnerAsync.js +2 -23
  39. package/dist/policy/renderBunPolicyDocumentation.d.ts +3 -0
  40. package/dist/policy/renderBunPolicyDocumentation.js +24 -0
  41. package/dist/tools/eslint/index.js +2 -2
  42. package/dist/tools/eslint/resolveEslintProfile.d.ts +3 -0
  43. package/dist/tools/eslint/{profile.js → resolveEslintProfile.js} +8 -18
  44. package/dist/tools/eslint/resolveEslintProfileFromDetectionInput.d.ts +4 -0
  45. package/dist/tools/eslint/resolveEslintProfileFromDetectionInput.js +10 -0
  46. package/dist/tools/workflows/files/ci.yml +7 -0
  47. package/dist/tools/workflows/files/release.yml +27 -1
  48. package/dist/tools/workflows/index.js +4 -1
  49. package/dist/tools/workflows/renderWorkflowAsync.d.ts +1 -0
  50. package/dist/tools/workflows/renderWorkflowAsync.js +1 -0
  51. package/dist/types/apm-release-validation.d.ts +33 -0
  52. package/dist/types/apm-release-validation.js +1 -0
  53. package/dist/types/bunPolicy.d.ts +5 -0
  54. package/dist/types/bunPolicy.js +1 -0
  55. package/dist/types/public.d.ts +1 -0
  56. package/dist/types/public.js +1 -0
  57. package/package.json +20 -5
  58. package/dist/tools/eslint/profile.d.ts +0 -4
package/README.md CHANGED
@@ -1,4 +1,13 @@
1
- # devtools
1
+ <!-- markdownlint-disable MD013 MD033 -->
2
+ <!-- This file is generated by Paradox. Do not edit manually. -->
3
+
4
+ # @ankhorage/devtools
5
+
6
+ ![license: MIT](././paradox/badges/license.svg) ![npm: v1.14.8](././paradox/badges/npm.svg) ![runtime: bun](././paradox/badges/runtime.svg) ![typescript: strict](././paradox/badges/typescript.svg) ![eslint: checked](././paradox/badges/eslint.svg) ![prettier: checked](././paradox/badges/prettier.svg) ![build: checked](././paradox/badges/build.svg) ![tests: checked](././paradox/badges/tests.svg) ![docs: paradox](././paradox/badges/docs.svg)
7
+
8
+ Shared tooling, repository automation, runtime policies, and agent standards for Ankhorage TypeScript projects
9
+
10
+ ## Usage
2
11
 
3
12
  Shared development tools and repository standards for Ankhorage TypeScript projects.
4
13
 
@@ -59,6 +68,8 @@ Synchronization ensures `@ankhorage/devtools` is declared using the version of t
59
68
 
60
69
  The package is discovered under the `devtools` category and exposes these capabilities:
61
70
 
71
+ - `devtools.apm.sync`
72
+ - `devtools.apm.validate`
62
73
  - `devtools.lint`
63
74
  - `devtools.changeset`
64
75
  - `devtools.format`
@@ -204,7 +215,7 @@ Agent Skill scripts are always TypeScript files with a `.ts` extension and run w
204
215
 
205
216
  `createConfig()` defaults to `profile: 'auto'`.
206
217
 
207
- Automatic detection reads the consuming repository's `package.json` and delegates project trait detection to `@ankhorage/utility/project`. Dependency signals are considered across `dependencies`, `devDependencies`, and `peerDependencies`.
218
+ Automatic detection reads the consuming repository's `package.json` and delegates project trait detection to `@ankhorage/project-detector`. Dependency signals are considered across `dependencies`, `devDependencies`, and `peerDependencies`.
208
219
 
209
220
  Profile precedence is:
210
221
 
@@ -375,3 +386,149 @@ A new concern should:
375
386
  4. add provider commands under `ankh devtools`
376
387
  5. include dry-run, status, and idempotence coverage
377
388
  6. document its central ownership and repository-owned extension points
389
+
390
+ ## Package-owned APM release gates
391
+
392
+ A package opts in through `ankh.apm` in its `package.json`:
393
+
394
+ ```json
395
+ {
396
+ "ankh": {
397
+ "apm": { "protocolVersion": 1, "descriptor": "./apm/update.json" }
398
+ }
399
+ }
400
+ ```
401
+
402
+ Include the descriptor in `files` and expose any executable extension with an explicit public
403
+ `exports` subpath. Use the released APM descriptor and extension types from `@ankhorage/apm/types`;
404
+ Devtools does not own or duplicate the protocol. Add `test:apm` to run the owner's deterministic
405
+ source-to-target fixtures, including skipped versions, unsupported old states, idempotency and
406
+ interruption recovery. A no-migration release still declares supported history explicitly and
407
+ can pass without an extension. Absence of metadata is reported as inapplicable, not migration-safe.
408
+
409
+ ```sh
410
+ ankh devtools apm sync .
411
+ ankh devtools apm validate . --allow-owner-code --artifact /tmp/reviewed-owner.tgz
412
+ ```
413
+
414
+ The standalone `ankhorage-apm-release sync|validate` binary runs the same operations. The public
415
+ `@ankhorage/devtools/apm-release` entrypoint exports `synchronizeApmReleaseDescriptorAsync`,
416
+ `validatePackedApmReleaseAsync` and `validateApmReleaseCandidate`; types are available from
417
+ `@ankhorage/devtools/types`. Programmatic callers can supply `previousDescriptors` to enforce
418
+ unchanged migration checksums and `relatedDescriptors` for cross-owner prerequisites. Owners
419
+ must include historical descriptors in their `test:apm` fixtures where their migrations require them.
420
+
421
+ Validation packs with lifecycle scripts disabled, reads only the packed descriptor, checks owner
422
+ identity, protocol/schema, migration/projection IDs, prerequisite graphs and public extension
423
+ capabilities using APM's canonical validators. Invalid metadata is a failure, not an opt-out.
424
+ `--allow-owner-code` is explicit permission to import the packed extension in a bounded child
425
+ process. The child is **not a security sandbox**; run only trusted owner code. Registry metadata
426
+ alone never grants this permission. Installed dependency resolution uses the repository's
427
+ frozen dependency graph; the package's own code is loaded through native public export resolution
428
+ from the extracted archive, never from the source checkout.
429
+
430
+ `--artifact` creates a new file containing exactly the validated bytes and refuses an existing
431
+ output. The JSON result includes SHA-512 integrity; `--expected-integrity` refuses a different
432
+ archive. Keep the retained archive immutable. The managed release workflow versions with
433
+ Changesets, synchronizes descriptor identity, rebuilds, runs `test:apm`, validates a fresh archive,
434
+ and publishes that same archive using `npm publish --ignore-scripts`, without repacking.
435
+ Authentication and network errors are not treated as an unpublished version. Packages without
436
+ APM metadata retain their existing Changesets publication path.
437
+
438
+ Producer order is: released APM protocol, validated owner package release, then consumer rollout.
439
+ Never declare an unpublished owner version. Project migration recovery is owned by APM and the
440
+ package's declared handlers; passing this release gate does not execute migrations against user
441
+ projects, deploy production services, or prove compatibility with shipped native binaries.
442
+
443
+ ## CLI
444
+
445
+ Run and synchronize the shared development toolchain through the Ankh CLI.
446
+
447
+ `ankh devtools changeset`, `ankh devtools lint`, `ankh devtools format`, and
448
+ `ankh devtools knip` execute the bundled Changesets, ESLint, Prettier, and Knip versions.
449
+ Repository synchronization is available through
450
+ `ankh devtools sync` and `ankh devtools status`, with focused `agents`, `skills`, `eslint`,
451
+ `prettier`, `knip`, `package`, `workflows`, and `vscode` sync/status subcommands.
452
+
453
+ Sync commands accept an optional target directory and `--dry-run`. Aggregate sync is
454
+ deterministic and idempotent: canonical managed files and skill trees are created or updated,
455
+ unrelated repository-local skills and create-only local extension files remain repository-owned,
456
+ and package metadata is merge-updated without replacing unrelated fields.
457
+
458
+ Fresh repositories can bootstrap the standard setup with `ankh devtools sync .` after adding
459
+ `@ankhorage/devtools`. Existing ESLint configuration is preserved during first migration as a
460
+ local extension before the canonical auto-detecting wrapper is installed.
461
+
462
+ APM owner releases opt in through `ankh.apm`. `ankh devtools apm sync .` binds a valid
463
+ descriptor to the selected Changesets version. `ankh devtools apm validate .` checks a
464
+ script-free tarball; `--allow-owner-code` explicitly permits the isolated executable probe.
465
+ `--artifact <path>` retains the exact accepted tarball and `--expected-integrity <SRI>`
466
+ rejects changed bytes. Owners provide `test:apm` for source-to-target and recovery fixtures.
467
+ CI runs that owner suite and the packed check. Release rebuilds after versioning, validates,
468
+ then publishes the retained archive with scripts disabled through the existing release job.
469
+ Packages without metadata keep their existing Changesets publish path unchanged.
470
+
471
+ ```bash
472
+ bunx @ankhorage/devtools ankhorage-apm-release
473
+ bunx @ankhorage/devtools ankhorage-changeset
474
+ bunx @ankhorage/devtools ankhorage-eslint
475
+ bunx @ankhorage/devtools ankhorage-knip
476
+ bunx @ankhorage/devtools ankhorage-prettier
477
+ ```
478
+
479
+ ## Generated documentation
480
+
481
+ - [Interactive documentation app](././paradox/index.html)
482
+ - [Public API reference](././paradox/exports.md)
483
+ - [Component registry](././paradox/components.md)
484
+ - [Architecture overview](././paradox/diagrams/architecture-overview.mmd)
485
+ - [Module relationships](././paradox/diagrams/module-relationships.mmd)
486
+ - [Export graph](././paradox/diagrams/export-graph.mmd)
487
+ - [createKnipMonorepoConfig sequence](././paradox/diagrams/sequences/create-knip-monorepo-config.mmd)
488
+
489
+ ## Public API
490
+
491
+ ### Utilities
492
+
493
+ <details>
494
+ <summary>validateApmReleaseCandidate</summary>
495
+
496
+ ```ts
497
+ validateApmReleaseCandidate(candidate: ApmPackedReleaseCandidate, options?: ApmReleaseValidationOptions) => ApmReleaseValidationResult
498
+ ```
499
+
500
+ Validate packed owner evidence using the released APM protocol, never a copied schema.
501
+ Opt-in packages declare ankh.apm with protocolVersion and a package-relative descriptor.
502
+ No metadata means not applicable, not a claim that no migration is needed. The descriptor
503
+ declares supported no-migration, required migration, or unsupported/manual history explicitly.
504
+ Previous descriptors enforce immutable migration checksums and related descriptors validate
505
+ cross-owner prerequisites. Package-owned source-to-target tests must also cover skipped versions,
506
+ idempotency, interruption/recovery, and unsupported historical states.
507
+
508
+ Module: `src/features/apm-release-validation/application/validateApmReleaseCandidate.ts`
509
+ Source: `src/features/apm-release-validation/application/validateApmReleaseCandidate.ts:31:1`
510
+ Related symbols: `ApmPackedReleaseCandidate`, `ApmReleaseValidationOptions`, `ApmReleaseValidationResult`
511
+
512
+ </details>
513
+
514
+ <details>
515
+ <summary>validatePackedApmReleaseAsync</summary>
516
+
517
+ ```ts
518
+ validatePackedApmReleaseAsync(targetDirectory: string, options?: ApmReleaseValidationOptions) => Promise<ApmReleaseValidationResult>
519
+ ```
520
+
521
+ Validate script-free publish bytes and optionally retain that exact accepted archive.
522
+ Release order is APM protocol, package-owner metadata/code, then consuming applications.
523
+ Build after Changesets versioning and descriptor synchronization. Execute package-owned
524
+ acceptance tests, then validate with explicit owner-code consent. The child probe uses the
525
+ packed package's native public export resolution and installed dependencies, not omitted source.
526
+ Consent permits trusted executable code; a child process is not a security sandbox.
527
+ An artifactPath retains accepted bytes exclusively, so publication need not repack or rerun
528
+ lifecycle scripts. expectedIntegrity rejects a stale candidate before any owner code loads.
529
+
530
+ Module: `src/features/apm-release-validation/composition/validatePackedApmReleaseAsync.ts`
531
+ Source: `src/features/apm-release-validation/composition/validatePackedApmReleaseAsync.ts:17:1`
532
+ Related symbols: `ApmReleaseValidationOptions`, `ApmReleaseValidationResult`
533
+
534
+ </details>
@@ -0,0 +1,4 @@
1
+ export { validateApmReleaseCandidate } from './features/apm-release-validation/application/validateApmReleaseCandidate.js';
2
+ export { synchronizeApmReleaseDescriptorAsync } from './features/apm-release-validation/composition/synchronizeApmReleaseDescriptorAsync.js';
3
+ export { validatePackedApmReleaseAsync } from './features/apm-release-validation/composition/validatePackedApmReleaseAsync.js';
4
+ export type { ApmPackedReleaseCandidate, ApmReleaseValidationOptions, ApmReleaseValidationResult, } from './types/apm-release-validation.js';
@@ -0,0 +1,3 @@
1
+ export { validateApmReleaseCandidate } from './features/apm-release-validation/application/validateApmReleaseCandidate.js';
2
+ export { synchronizeApmReleaseDescriptorAsync } from './features/apm-release-validation/composition/synchronizeApmReleaseDescriptorAsync.js';
3
+ export { validatePackedApmReleaseAsync } from './features/apm-release-validation/composition/validatePackedApmReleaseAsync.js';
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ import { runApmReleaseCommandAsync } from '../../features/apm-release-validation/adapters/inbound/runApmReleaseCommandAsync.js';
3
+ const [operation, ...argv] = process.argv.slice(2);
4
+ if (operation !== 'sync' && operation !== 'validate') {
5
+ console.error('Usage: ankhorage-apm-release <sync|validate> [directory] [--allow-owner-code] [--artifact path] [--expected-integrity value]');
6
+ process.exitCode = 1;
7
+ }
8
+ else {
9
+ const result = await runApmReleaseCommandAsync(operation, argv, {
10
+ cwd: process.cwd(),
11
+ writeStdout: (text) => process.stdout.write(text),
12
+ writeStderr: (text) => process.stderr.write(text),
13
+ });
14
+ process.exitCode = result.exitCode;
15
+ }
@@ -0,0 +1,5 @@
1
+ import type { DevtoolsProviderCommandContext } from '../../runProviderCommand.js';
2
+ /*** Adapt the public APM release sync command to the shared release boundary. */
3
+ export declare function sync(argv: readonly string[], context: DevtoolsProviderCommandContext): Promise<{
4
+ readonly exitCode: number;
5
+ }>;
@@ -0,0 +1,5 @@
1
+ import { runApmReleaseCommandAsync } from '../../../features/apm-release-validation/adapters/inbound/runApmReleaseCommandAsync.js';
2
+ /*** Adapt the public APM release sync command to the shared release boundary. */
3
+ export async function sync(argv, context) {
4
+ return runApmReleaseCommandAsync('sync', argv, context);
5
+ }
@@ -0,0 +1,5 @@
1
+ import type { DevtoolsProviderCommandContext } from '../../runProviderCommand.js';
2
+ /*** Adapt the public APM release validate command to the shared release boundary. */
3
+ export declare function validate(argv: readonly string[], context: DevtoolsProviderCommandContext): Promise<{
4
+ readonly exitCode: number;
5
+ }>;
@@ -0,0 +1,5 @@
1
+ import { runApmReleaseCommandAsync } from '../../../features/apm-release-validation/adapters/inbound/runApmReleaseCommandAsync.js';
2
+ /*** Adapt the public APM release validate command to the shared release boundary. */
3
+ export async function validate(argv, context) {
4
+ return runApmReleaseCommandAsync('validate', argv, context);
5
+ }
@@ -1,7 +1,7 @@
1
1
  export type DevtoolsToolName = 'changeset' | 'format' | 'knip' | 'lint';
2
2
  type DevtoolsManagedScope = 'agents' | 'all' | 'eslint' | 'knip' | 'package' | 'prettier' | 'skills' | 'vscode' | 'workflows';
3
3
  type DevtoolsManagedOperation = 'status' | 'sync';
4
- type DevtoolsCapability = 'devtools.changeset' | 'devtools.format' | 'devtools.knip' | 'devtools.lint' | 'devtools.status' | 'devtools.sync' | 'devtools.agents.status' | 'devtools.agents.sync' | 'devtools.eslint.status' | 'devtools.eslint.sync' | 'devtools.knip.status' | 'devtools.knip.sync' | 'devtools.package.status' | 'devtools.package.sync' | 'devtools.prettier.status' | 'devtools.prettier.sync' | 'devtools.skills.status' | 'devtools.skills.sync' | 'devtools.vscode.status' | 'devtools.vscode.sync' | 'devtools.workflows.status' | 'devtools.workflows.sync';
4
+ type DevtoolsCapability = 'devtools.apm.sync' | 'devtools.apm.validate' | 'devtools.changeset' | 'devtools.format' | 'devtools.knip' | 'devtools.lint' | 'devtools.status' | 'devtools.sync' | 'devtools.agents.status' | 'devtools.agents.sync' | 'devtools.eslint.status' | 'devtools.eslint.sync' | 'devtools.knip.status' | 'devtools.knip.sync' | 'devtools.package.status' | 'devtools.package.sync' | 'devtools.prettier.status' | 'devtools.prettier.sync' | 'devtools.skills.status' | 'devtools.skills.sync' | 'devtools.vscode.status' | 'devtools.vscode.sync' | 'devtools.workflows.status' | 'devtools.workflows.sync';
5
5
  interface DevtoolsCommandBase {
6
6
  readonly path: readonly [string, ...string[]];
7
7
  readonly capability: DevtoolsCapability;
@@ -18,7 +18,11 @@ export interface DevtoolsRepositoryCommandDefinition extends DevtoolsCommandBase
18
18
  readonly scope: DevtoolsManagedScope;
19
19
  readonly operation: DevtoolsManagedOperation;
20
20
  }
21
- export type DevtoolsCommandDefinition = DevtoolsExternalCommandDefinition | DevtoolsRepositoryCommandDefinition;
21
+ interface DevtoolsApmReleaseCommandDefinition extends DevtoolsCommandBase {
22
+ readonly kind: 'apm-release';
23
+ readonly operation: 'sync' | 'validate';
24
+ }
25
+ export type DevtoolsCommandDefinition = DevtoolsExternalCommandDefinition | DevtoolsRepositoryCommandDefinition | DevtoolsApmReleaseCommandDefinition;
22
26
  export declare function getDevtoolsCommands(): readonly DevtoolsCommandDefinition[];
23
27
  export declare function findDevtoolsCommandByPath(path: readonly string[]): DevtoolsCommandDefinition | null;
24
28
  export declare function getDevtoolsToolCommand(toolName: DevtoolsToolName): DevtoolsExternalCommandDefinition;
@@ -1,4 +1,18 @@
1
1
  const DEVTOOLS_COMMANDS = [
2
+ {
3
+ kind: 'apm-release',
4
+ operation: 'sync',
5
+ path: ['apm', 'sync'],
6
+ capability: 'devtools.apm.sync',
7
+ summary: 'Bind owner metadata to the Changesets release version.',
8
+ },
9
+ {
10
+ kind: 'apm-release',
11
+ operation: 'validate',
12
+ path: ['apm', 'validate'],
13
+ capability: 'devtools.apm.validate',
14
+ summary: 'Validate the exact packed APM release artifact.',
15
+ },
2
16
  externalCommand('changeset', 'devtools.changeset', 'Run the shared Changesets toolchain.', '@changesets/cli', 'changeset'),
3
17
  externalCommand('lint', 'devtools.lint', 'Run the shared ESLint toolchain.', 'eslint', 'eslint'),
4
18
  externalCommand('format', 'devtools.format', 'Run the shared Prettier toolchain.', 'prettier', 'prettier'),
@@ -27,7 +41,7 @@ export function getDevtoolsCommands() {
27
41
  }
28
42
  export function findDevtoolsCommandByPath(path) {
29
43
  return (DEVTOOLS_COMMANDS.find((command) => command.path.length === path.length &&
30
- command.path.every((segment, index) => segment === path[index])) ?? null);
44
+ command.path.every((segment, index) => segment === path.at(index))) ?? null);
31
45
  }
32
46
  export function getDevtoolsToolCommand(toolName) {
33
47
  for (const command of DEVTOOLS_COMMANDS) {
@@ -2,10 +2,10 @@ declare const provider: {
2
2
  id: string;
3
3
  category: string;
4
4
  version: string;
5
- capabilities: ("devtools.changeset" | "devtools.format" | "devtools.knip" | "devtools.lint" | "devtools.status" | "devtools.sync" | "devtools.agents.status" | "devtools.agents.sync" | "devtools.eslint.status" | "devtools.eslint.sync" | "devtools.knip.status" | "devtools.knip.sync" | "devtools.package.status" | "devtools.package.sync" | "devtools.prettier.status" | "devtools.prettier.sync" | "devtools.skills.status" | "devtools.skills.sync" | "devtools.vscode.status" | "devtools.vscode.sync" | "devtools.workflows.status" | "devtools.workflows.sync")[];
5
+ capabilities: ("devtools.apm.sync" | "devtools.apm.validate" | "devtools.changeset" | "devtools.format" | "devtools.knip" | "devtools.lint" | "devtools.status" | "devtools.sync" | "devtools.agents.status" | "devtools.agents.sync" | "devtools.eslint.status" | "devtools.eslint.sync" | "devtools.knip.status" | "devtools.knip.sync" | "devtools.package.status" | "devtools.package.sync" | "devtools.prettier.status" | "devtools.prettier.sync" | "devtools.skills.status" | "devtools.skills.sync" | "devtools.vscode.status" | "devtools.vscode.sync" | "devtools.workflows.status" | "devtools.workflows.sync")[];
6
6
  commands: {
7
7
  path: string[];
8
- capability: "devtools.changeset" | "devtools.format" | "devtools.knip" | "devtools.lint" | "devtools.status" | "devtools.sync" | "devtools.agents.status" | "devtools.agents.sync" | "devtools.eslint.status" | "devtools.eslint.sync" | "devtools.knip.status" | "devtools.knip.sync" | "devtools.package.status" | "devtools.package.sync" | "devtools.prettier.status" | "devtools.prettier.sync" | "devtools.skills.status" | "devtools.skills.sync" | "devtools.vscode.status" | "devtools.vscode.sync" | "devtools.workflows.status" | "devtools.workflows.sync";
8
+ capability: "devtools.apm.sync" | "devtools.apm.validate" | "devtools.changeset" | "devtools.format" | "devtools.knip" | "devtools.lint" | "devtools.status" | "devtools.sync" | "devtools.agents.status" | "devtools.agents.sync" | "devtools.eslint.status" | "devtools.eslint.sync" | "devtools.knip.status" | "devtools.knip.sync" | "devtools.package.status" | "devtools.package.sync" | "devtools.prettier.status" | "devtools.prettier.sync" | "devtools.skills.status" | "devtools.skills.sync" | "devtools.vscode.status" | "devtools.vscode.sync" | "devtools.workflows.status" | "devtools.workflows.sync";
9
9
  summary: string;
10
10
  }[];
11
11
  handlers: {
package/dist/cli/index.js CHANGED
@@ -16,6 +16,15 @@
16
16
  * `@ankhorage/devtools`. Existing ESLint configuration is preserved during first migration as a
17
17
  * local extension before the canonical auto-detecting wrapper is installed.
18
18
  *
19
+ * APM owner releases opt in through `ankh.apm`. `ankh devtools apm sync .` binds a valid
20
+ * descriptor to the selected Changesets version. `ankh devtools apm validate .` checks a
21
+ * script-free tarball; `--allow-owner-code` explicitly permits the isolated executable probe.
22
+ * `--artifact <path>` retains the exact accepted tarball and `--expected-integrity <SRI>`
23
+ * rejects changed bytes. Owners provide `test:apm` for source-to-target and recovery fixtures.
24
+ * CI runs that owner suite and the packed check. Release rebuilds after versioning, validates,
25
+ * then publishes the retained archive with scripts disabled through the existing release job.
26
+ * Packages without metadata keep their existing Changesets publish path unchanged.
27
+ *
19
28
  * @readme
20
29
  */
21
30
  import { readFileSync } from 'node:fs';
@@ -1,6 +1,11 @@
1
+ import { sync } from './commands/apm/sync.js';
2
+ import { validate } from './commands/apm/validate.js';
1
3
  import { runExternalTool } from './runExternalTool.js';
2
4
  import { runRepositoryCommand, } from './runRepositoryCommand.js';
3
5
  export async function runProviderCommand(command, argv, context) {
6
+ if (command.kind === 'apm-release') {
7
+ return command.operation === 'sync' ? sync(argv, context) : validate(argv, context);
8
+ }
4
9
  if (command.kind === 'external') {
5
10
  return await runExternalTool(command, argv, {
6
11
  cwd: context.cwd,
@@ -0,0 +1,10 @@
1
+ /*** Parse release CLI input and report the same structured validation result on every entrypoint. */
2
+ export declare function runApmReleaseCommandAsync(operation: 'sync' | 'validate', argv: readonly string[], context: CommandContext): Promise<{
3
+ readonly exitCode: number;
4
+ }>;
5
+ interface CommandContext {
6
+ readonly cwd: string;
7
+ readonly writeStdout: (text: string) => void;
8
+ readonly writeStderr: (text: string) => void;
9
+ }
10
+ export {};
@@ -0,0 +1,51 @@
1
+ import { resolve } from 'node:path';
2
+ import { synchronizeApmReleaseDescriptorAsync } from '../../composition/synchronizeApmReleaseDescriptorAsync.js';
3
+ import { validatePackedApmReleaseAsync } from '../../composition/validatePackedApmReleaseAsync.js';
4
+ /*** Parse release CLI input and report the same structured validation result on every entrypoint. */
5
+ export async function runApmReleaseCommandAsync(operation, argv, context) {
6
+ try {
7
+ const { target, options } = parseArguments(operation, argv, context.cwd);
8
+ if (operation === 'sync') {
9
+ context.writeStdout(`${JSON.stringify({ changed: await synchronizeApmReleaseDescriptorAsync(target) })}\n`);
10
+ return { exitCode: 0 };
11
+ }
12
+ const result = await validatePackedApmReleaseAsync(target, options);
13
+ context.writeStdout(`${JSON.stringify(result)}\n`);
14
+ return { exitCode: result.valid ? 0 : 1 };
15
+ }
16
+ catch (error) {
17
+ context.writeStderr(`${error instanceof Error ? error.message : 'APM release validation failed.'}\n`);
18
+ return { exitCode: 1 };
19
+ }
20
+ }
21
+ /*** Separate release argument validation from execution; reject duplicates and incomplete flags. */
22
+ function parseArguments(operation, argv, cwd) {
23
+ if (operation === 'sync' && argv.some((value) => value.startsWith('--')))
24
+ throw new Error('APM sync does not accept validation options.');
25
+ const artifactIndex = optionIndex(argv, '--artifact');
26
+ const integrityIndex = optionIndex(argv, '--expected-integrity');
27
+ const ignored = [artifactIndex, integrityIndex].flatMap((index) => index < 0 ? [] : [index, index + 1]);
28
+ const remaining = argv.filter((value, index) => !ignored.includes(index) && value !== '--allow-owner-code');
29
+ if (remaining.length > 1 || remaining.some((value) => value.startsWith('-')))
30
+ throw new Error('Expected one target directory and supported release options.');
31
+ const artifact = artifactIndex < 0 ? undefined : argv.at(artifactIndex + 1);
32
+ const integrity = integrityIndex < 0 ? undefined : argv.at(integrityIndex + 1);
33
+ return {
34
+ target: resolve(cwd, remaining.at(0) ?? '.'),
35
+ options: {
36
+ allowOwnerCode: argv.includes('--allow-owner-code'),
37
+ ...(artifact === undefined ? {} : { artifactPath: resolve(cwd, artifact) }),
38
+ ...(integrity === undefined ? {} : { expectedIntegrity: integrity }),
39
+ },
40
+ };
41
+ }
42
+ /*** Require a single option occurrence followed by a value. */
43
+ function optionIndex(argv, option) {
44
+ const index = argv.indexOf(option);
45
+ if (index < 0)
46
+ return index;
47
+ const value = argv.at(index + 1);
48
+ if (argv.lastIndexOf(option) !== index || value === undefined || value.startsWith('--'))
49
+ throw new Error(`Invalid value for ${option}.`);
50
+ return index;
51
+ }
@@ -0,0 +1,3 @@
1
+ import type { ApmReleaseDescriptorPort } from '../../../../types/apm-release-validation.js';
2
+ /*** Supply bounded owner-document reads and idempotent writes for release synchronization. */
3
+ export declare function createApmReleaseDescriptorPort(targetDirectory: string): ApmReleaseDescriptorPort;
@@ -0,0 +1,31 @@
1
+ import { readFile, realpath, writeFile } from 'node:fs/promises';
2
+ import { relative, resolve } from 'node:path';
3
+ import { resolvePathWithinRoot } from '@ankhorage/utility/node/path';
4
+ import { format } from 'prettier';
5
+ import { readApmReleaseManifestAsync } from './readApmReleaseManifestAsync.js';
6
+ /*** Supply bounded owner-document reads and idempotent writes for release synchronization. */
7
+ export function createApmReleaseDescriptorPort(targetDirectory) {
8
+ return {
9
+ readPackageAsync: () => readApmReleaseManifestAsync(resolve(targetDirectory, 'package.json')),
10
+ readDescriptorAsync: async (subpath) => readApmReleaseManifestAsync(await descriptorPathAsync(targetDirectory, subpath)),
11
+ writeDescriptorAsync: async (subpath, descriptor) => {
12
+ const path = await descriptorPathAsync(targetDirectory, subpath);
13
+ const before = await readFile(path, 'utf8');
14
+ const after = await format(JSON.stringify(descriptor), {
15
+ parser: 'json',
16
+ printWidth: 100,
17
+ tabWidth: 2,
18
+ });
19
+ if (before === after)
20
+ return false;
21
+ await writeFile(path, after, 'utf8');
22
+ return true;
23
+ },
24
+ };
25
+ }
26
+ /*** Prevent descriptor reads and writes from following paths or symlinks outside the owner root. */
27
+ async function descriptorPathAsync(target, subpath) {
28
+ const root = await realpath(target);
29
+ const resolved = await realpath(resolvePathWithinRoot(root, subpath));
30
+ return resolvePathWithinRoot(root, relative(root, resolved));
31
+ }
@@ -0,0 +1,3 @@
1
+ import type { ApmReleaseValidationOptions, ApmReleaseValidationResult } from '../../../../types/apm-release-validation.js';
2
+ /*** Validate the actual script-free packed candidate outside the checkout, retaining only accepted bytes. */
3
+ export declare function inspectPackedApmReleaseCandidateAsync(targetDirectory: string, options?: ApmReleaseValidationOptions): Promise<ApmReleaseValidationResult>;
@@ -0,0 +1,84 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import { constants } from 'node:fs';
4
+ import { copyFile, mkdir, mkdtemp, readFile, realpath, rm } from 'node:fs/promises';
5
+ import { tmpdir } from 'node:os';
6
+ import { relative, resolve } from 'node:path';
7
+ import { promisify } from 'node:util';
8
+ import { validatePackageUpdateMetadata, validateUpdateDescriptor } from '@ankhorage/apm';
9
+ import { resolvePathWithinRoot } from '@ankhorage/utility/node/path';
10
+ import { validateApmReleaseCandidate } from '../../application/validateApmReleaseCandidate.js';
11
+ import { readApmReleaseMetadata } from '../../utils/readApmReleaseMetadata.js';
12
+ import { probePackedApmExtensionAsync } from './probePackedApmExtensionAsync.js';
13
+ import { readApmReleaseManifestAsync } from './readApmReleaseManifestAsync.js';
14
+ /*** Validate the actual script-free packed candidate outside the checkout, retaining only accepted bytes. */
15
+ export async function inspectPackedApmReleaseCandidateAsync(targetDirectory, options = {}) {
16
+ const root = await realpath(targetDirectory);
17
+ const temporaryRoot = await mkdtemp(resolve(tmpdir(), 'ankh-apm-release-'));
18
+ try {
19
+ const archive = resolve(temporaryRoot, 'candidate.tgz');
20
+ const packageRoot = resolve(temporaryRoot, 'package');
21
+ await packCandidateAsync(root, temporaryRoot, archive);
22
+ const candidate = await readPackedCandidateAsync(packageRoot, archive);
23
+ const result = await validatePackedCandidateAsync(candidate, packageRoot, root, options);
24
+ if (!result.valid || options.artifactPath === undefined)
25
+ return result;
26
+ const artifactPath = resolve(options.artifactPath);
27
+ if (artifactPath === archive)
28
+ throw new Error('Artifact output must survive temporary validation cleanup.');
29
+ await mkdir(resolve(artifactPath, '..'), { recursive: true });
30
+ await copyFile(archive, artifactPath, constants.COPYFILE_EXCL);
31
+ return { ...result, artifactPath };
32
+ }
33
+ finally {
34
+ await rm(temporaryRoot, { recursive: true, force: true });
35
+ }
36
+ }
37
+ /*** Build and extract only a script-free local package archive under a separate temporary root. */
38
+ async function packCandidateAsync(root, temporaryRoot, archive) {
39
+ await execFileAsync('bun', ['pm', 'pack', '--filename', archive, '--ignore-scripts', '--quiet'], {
40
+ cwd: root,
41
+ ...PROCESS_LIMITS,
42
+ });
43
+ const { stdout } = await execFileAsync('tar', ['-tzf', archive], PROCESS_LIMITS);
44
+ for (const entry of stdout.trim().split('\n')) {
45
+ if (!entry.startsWith('package/'))
46
+ throw new Error('Packed file is outside the package prefix.');
47
+ resolvePathWithinRoot(temporaryRoot, entry);
48
+ }
49
+ await execFileAsync('tar', ['-xzf', archive, '-C', temporaryRoot, '--no-same-owner', '--no-same-permissions'], PROCESS_LIMITS);
50
+ }
51
+ /*** Read owner metadata strictly from the archive, not the source-tree descriptor. */
52
+ async function readPackedCandidateAsync(packageRoot, archive) {
53
+ const integrity = `sha512-${createHash('sha512')
54
+ .update(await readFile(archive))
55
+ .digest('base64')}`;
56
+ const packageJson = await readApmReleaseManifestAsync(resolve(packageRoot, 'package.json'));
57
+ const discovery = validatePackageUpdateMetadata(readApmReleaseMetadata(packageJson));
58
+ if (!discovery.valid || discovery.metadata === undefined) {
59
+ return { packageJson, integrity, descriptor: undefined, descriptorSource: '' };
60
+ }
61
+ const descriptorPath = await realpath(resolvePathWithinRoot(packageRoot, discovery.metadata.descriptor));
62
+ resolvePathWithinRoot(packageRoot, relative(packageRoot, descriptorPath));
63
+ const descriptorSource = await readFile(descriptorPath, 'utf8');
64
+ const descriptor = JSON.parse(descriptorSource);
65
+ return { packageJson, descriptor, descriptorSource, integrity };
66
+ }
67
+ /*** Require valid static evidence before optionally loading the exact packed owner extension. */
68
+ async function validatePackedCandidateAsync(candidate, packageRoot, root, options) {
69
+ const validation = validateUpdateDescriptor({ descriptor: candidate.descriptor });
70
+ const staticResult = validateApmReleaseCandidate(candidate, options);
71
+ const onlyExtensionMissing = staticResult.blockers.every((blocker) => blocker.startsWith('protocol.extension-binding-mismatch:'));
72
+ const extension = validation.descriptor?.extension;
73
+ return extension !== undefined && validation.valid && onlyExtensionMissing
74
+ ? probePackedApmExtensionAsync({
75
+ candidate,
76
+ packageRoot,
77
+ dependencyRoot: resolve(root, 'node_modules'),
78
+ subpath: extension.export,
79
+ options,
80
+ })
81
+ : staticResult;
82
+ }
83
+ const execFileAsync = promisify(execFile);
84
+ const PROCESS_LIMITS = { timeout: 60_000, maxBuffer: 4 * 1024 * 1024 };
@@ -0,0 +1,11 @@
1
+ import type { ApmPackedReleaseCandidate, ApmReleaseValidationOptions, ApmReleaseValidationResult } from '../../../../types/apm-release-validation.js';
2
+ /*** Load the native public ESM export in a bounded child process only after explicit owner-code consent. */
3
+ export declare function probePackedApmExtensionAsync(input: ProbeInput): Promise<ApmReleaseValidationResult>;
4
+ interface ProbeInput {
5
+ readonly candidate: ApmPackedReleaseCandidate;
6
+ readonly packageRoot: string;
7
+ readonly dependencyRoot: string;
8
+ readonly subpath: string;
9
+ readonly options: ApmReleaseValidationOptions;
10
+ }
11
+ export {};
@@ -0,0 +1,82 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { readFile, symlink, writeFile } from 'node:fs/promises';
3
+ import { resolve } from 'node:path';
4
+ import { promisify } from 'node:util';
5
+ import { isRecord } from '@ankhorage/utility/object';
6
+ /*** Load the native public ESM export in a bounded child process only after explicit owner-code consent. */
7
+ export async function probePackedApmExtensionAsync(input) {
8
+ if (input.options.allowOwnerCode !== true) {
9
+ return {
10
+ applicable: true,
11
+ valid: false,
12
+ integrity: input.candidate.integrity,
13
+ blockers: [
14
+ 'release.owner-code-consent: Executable owner validation requires --allow-owner-code.',
15
+ ],
16
+ };
17
+ }
18
+ const { probePath, resultPath } = await prepareProbeAsync(input);
19
+ try {
20
+ await execFileAsync(process.execPath, [probePath], {
21
+ cwd: input.packageRoot,
22
+ timeout: 30_000,
23
+ maxBuffer: 1024 * 1024,
24
+ env: {
25
+ PATH: process.env.PATH,
26
+ SYSTEMROOT: process.env.SYSTEMROOT,
27
+ HOME: input.packageRoot,
28
+ TMPDIR: input.packageRoot,
29
+ },
30
+ });
31
+ return await readProbeResultAsync(input.candidate, resultPath);
32
+ }
33
+ catch {
34
+ return {
35
+ applicable: true,
36
+ valid: false,
37
+ integrity: input.candidate.integrity,
38
+ blockers: [
39
+ 'release.extension-load: Packed public export failed to load or validate in the owner probe.',
40
+ ],
41
+ };
42
+ }
43
+ }
44
+ /*** Materialize a probe that uses native self-reference export resolution in the extracted package. */
45
+ async function prepareProbeAsync(input) {
46
+ const requestPath = resolve(input.packageRoot, '.apm-release-request.json');
47
+ const resultPath = resolve(input.packageRoot, '.apm-release-result.json');
48
+ const probePath = resolve(input.packageRoot, '.apm-release-probe.mjs');
49
+ const specifier = `${String(input.candidate.packageJson.name)}${input.subpath.slice(1)}`;
50
+ await symlink(input.dependencyRoot, resolve(input.packageRoot, 'node_modules'), 'dir');
51
+ await writeFile(requestPath, JSON.stringify({ candidate: input.candidate, options: input.options }));
52
+ const validatorUrl = new URL('../../application/validateApmReleaseCandidate.js', import.meta.url)
53
+ .href;
54
+ await writeFile(probePath, [
55
+ "import { readFile, writeFile } from 'node:fs/promises';",
56
+ `import { validateApmReleaseCandidate } from ${JSON.stringify(validatorUrl)};`,
57
+ `const request = JSON.parse(await readFile(${JSON.stringify(requestPath)}, 'utf8'));`,
58
+ `const loaded = await import(${JSON.stringify(specifier)});`,
59
+ 'const result = validateApmReleaseCandidate({ ...request.candidate, extension: loaded.default }, request.options);',
60
+ `await writeFile(${JSON.stringify(resultPath)}, JSON.stringify(result));`,
61
+ ].join('\n'));
62
+ return { probePath, resultPath };
63
+ }
64
+ /*** Validate child-process response data before exposing a structured report. */
65
+ async function readProbeResultAsync(candidate, resultPath) {
66
+ const result = JSON.parse(await readFile(resultPath, 'utf8'));
67
+ if (!isRecord(result) ||
68
+ typeof result.valid !== 'boolean' ||
69
+ !Array.isArray(result.blockers) ||
70
+ !result.blockers.every((value) => typeof value === 'string')) {
71
+ throw new Error('Invalid owner probe response.');
72
+ }
73
+ return {
74
+ applicable: true,
75
+ valid: result.valid,
76
+ integrity: candidate.integrity,
77
+ packageName: String(candidate.packageJson.name),
78
+ packageVersion: String(candidate.packageJson.version),
79
+ blockers: result.blockers,
80
+ };
81
+ }
82
+ const execFileAsync = promisify(execFile);
@@ -0,0 +1,2 @@
1
+ /*** Read a release-owned JSON object while rejecting malformed documents. */
2
+ export declare function readApmReleaseManifestAsync(filePath: string): Promise<Readonly<Record<string, unknown>>>;