@ankhorage/contracts 1.18.3 → 1.19.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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @ankhorage/contracts
2
2
 
3
+ ## 1.19.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 37105b0: Add published ankh package metadata and document capability naming conventions.
8
+
9
+ ## 1.19.0
10
+
11
+ ### Minor Changes
12
+
13
+ - b618979: Add `@ankhorage/contracts/cli` metadata contracts for Ankh package discovery.
14
+
3
15
  ## 1.18.3
4
16
 
5
17
  ### Patch Changes
package/README.md CHANGED
@@ -22,6 +22,7 @@ Shared public contracts for Ankhorage packages and standalone provider packages.
22
22
  ```ts
23
23
  import type { AppManifest } from '@ankhorage/contracts';
24
24
  import type { AuthAdapter } from '@ankhorage/contracts/auth';
25
+ import type { AnkhCommandProviderManifest, AnkhPackageMetadata } from '@ankhorage/contracts/cli';
25
26
  import type { DbAdapter } from '@ankhorage/contracts/db';
26
27
  import type { StorageAdapter } from '@ankhorage/contracts/storage';
27
28
  ```
@@ -71,6 +72,91 @@ export function createSupabaseAuthAdapter(): AuthAdapter {
71
72
  }
72
73
  ```
73
74
 
75
+ ## CLI discovery contracts
76
+
77
+ `@ankhorage/contracts/cli` contains metadata-only discovery contracts for Ankh
78
+ packages and command providers.
79
+
80
+ ```ts
81
+ import type { AnkhCommandProviderManifest, AnkhPackageMetadata } from '@ankhorage/contracts/cli';
82
+ ```
83
+
84
+ Provider packages expose `package.json.ankh` metadata using a package-relative
85
+ provider module path:
86
+
87
+ ```json
88
+ {
89
+ "ankh": {
90
+ "category": "infra",
91
+ "provider": "./dist/ankh.provider.js",
92
+ "capabilities": ["infra.up", "infra.status"]
93
+ }
94
+ }
95
+ ```
96
+
97
+ Metadata-only packages use `null` for `provider`:
98
+
99
+ ```json
100
+ {
101
+ "ankh": {
102
+ "category": "contracts",
103
+ "provider": null,
104
+ "capabilities": ["contracts.cli"]
105
+ }
106
+ }
107
+ ```
108
+
109
+ `@ankhorage/contracts` now publishes this metadata shape directly from its own
110
+ `package.json`:
111
+
112
+ ```json
113
+ {
114
+ "ankh": {
115
+ "category": "contracts",
116
+ "provider": null,
117
+ "capabilities": ["contracts.cli"]
118
+ }
119
+ }
120
+ ```
121
+
122
+ Command descriptor paths are relative to the provider category:
123
+
124
+ - `category: "infra"` with `path: ["up"]` maps to `ankh infra up`
125
+ - `category: "dev"` with `path: ["android", "scan"]` maps to `ankh dev android scan`
126
+
127
+ This subpath contains contracts only. It must not depend on `commander`,
128
+ `@ankhorage/ankh`, provider implementations, or runtime CLI execution logic.
129
+
130
+ Capability names are dot-separated package metadata identifiers.
131
+
132
+ Preferred forms:
133
+
134
+ - `<category>.<action>` for simple capabilities
135
+ - `<category>.<resource>.<action>` for nested or domain-specific capabilities
136
+
137
+ Valid examples:
138
+
139
+ - `infra.up`
140
+ - `templates.create`
141
+ - `contracts.cli`
142
+ - `board.web.import`
143
+ - `doctor.repo.validate`
144
+ - `dev.android.rebuild`
145
+ - `expoRuntime.plan`
146
+
147
+ Invalid examples:
148
+
149
+ - `infra`
150
+ - `infra-up`
151
+ - `.status`
152
+ - `infra.`
153
+ - `infra..up`
154
+
155
+ `AnkhCapabilityId` remains intentionally broad at the TypeScript level so
156
+ packages can publish stable serializable metadata without embedding policy
157
+ validation logic here. Stricter validation belongs later in
158
+ `@ankhorage/doctor`.
159
+
74
160
  ## Profile contract
75
161
 
76
162
  Auth providers own identity records. App-facing profile data should be modeled separately, usually in an app table such as `profiles`.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ export type AnkhCommandCategory = string;
2
+ export type AnkhProviderReference = `./${string}`;
3
+ export type AnkhCapabilityId = `${string}.${string}`;
4
+ export interface AnkhCommandDescriptor {
5
+ readonly path: readonly [string, ...string[]];
6
+ readonly summary: string;
7
+ readonly capability: AnkhCapabilityId;
8
+ readonly aliases?: readonly string[];
9
+ readonly examples?: readonly string[];
10
+ }
11
+ export interface AnkhCommandProviderManifest {
12
+ readonly id: string;
13
+ readonly category: AnkhCommandCategory;
14
+ readonly version: string;
15
+ readonly capabilities: readonly AnkhCapabilityId[];
16
+ readonly commands: readonly AnkhCommandDescriptor[];
17
+ }
18
+ export interface AnkhPackageMetadata {
19
+ readonly category: AnkhCommandCategory;
20
+ readonly provider: AnkhProviderReference | null;
21
+ readonly capabilities: readonly AnkhCapabilityId[];
22
+ }
23
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,mBAAmB,GAAG,MAAM,CAAC;AAEzC,MAAM,MAAM,qBAAqB,GAAG,KAAK,MAAM,EAAE,CAAC;AAElD,MAAM,MAAM,gBAAgB,GAAG,GAAG,MAAM,IAAI,MAAM,EAAE,CAAC;AAErD,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC,CAAC;IAC9C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,gBAAgB,CAAC;IACtC,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACvC;AAED,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,QAAQ,EAAE,mBAAmB,CAAC;IACvC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,YAAY,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACnD,QAAQ,CAAC,QAAQ,EAAE,SAAS,qBAAqB,EAAE,CAAC;CACrD;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,QAAQ,EAAE,mBAAmB,CAAC;IACvC,QAAQ,CAAC,QAAQ,EAAE,qBAAqB,GAAG,IAAI,CAAC;IAChD,QAAQ,CAAC,YAAY,EAAE,SAAS,gBAAgB,EAAE,CAAC;CACpD"}
package/dist/cli.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"","sourcesContent":["export type AnkhCommandCategory = string;\n\nexport type AnkhProviderReference = `./${string}`;\n\nexport type AnkhCapabilityId = `${string}.${string}`;\n\nexport interface AnkhCommandDescriptor {\n readonly path: readonly [string, ...string[]];\n readonly summary: string;\n readonly capability: AnkhCapabilityId;\n readonly aliases?: readonly string[];\n readonly examples?: readonly string[];\n}\n\nexport interface AnkhCommandProviderManifest {\n readonly id: string;\n readonly category: AnkhCommandCategory;\n readonly version: string;\n readonly capabilities: readonly AnkhCapabilityId[];\n readonly commands: readonly AnkhCommandDescriptor[];\n}\n\nexport interface AnkhPackageMetadata {\n readonly category: AnkhCommandCategory;\n readonly provider: AnkhProviderReference | null;\n readonly capabilities: readonly AnkhCapabilityId[];\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './auth';
2
2
  export * from './bindings';
3
+ export * from './cli';
3
4
  export * from './data';
4
5
  export * from './db';
5
6
  export * from './nutrition';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC;AACvB,cAAc,YAAY,CAAC;AAC3B,cAAc,QAAQ,CAAC;AACvB,cAAc,MAAM,CAAC;AACrB,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,SAAS,CAAC;AACxB,cAAc,WAAW,CAAC;AAC1B,cAAc,SAAS,CAAC;AACxB,cAAc,MAAM,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC;AACvB,cAAc,YAAY,CAAC;AAC3B,cAAc,OAAO,CAAC;AACtB,cAAc,QAAQ,CAAC;AACvB,cAAc,MAAM,CAAC;AACrB,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,SAAS,CAAC;AACxB,cAAc,WAAW,CAAC;AAC1B,cAAc,SAAS,CAAC;AACxB,cAAc,MAAM,CAAC"}
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './auth';
2
2
  export * from './bindings';
3
+ export * from './cli';
3
4
  export * from './data';
4
5
  export * from './db';
5
6
  export * from './nutrition';
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC;AACvB,cAAc,YAAY,CAAC;AAC3B,cAAc,QAAQ,CAAC;AACvB,cAAc,MAAM,CAAC;AACrB,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,SAAS,CAAC;AACxB,cAAc,WAAW,CAAC;AAC1B,cAAc,SAAS,CAAC;AACxB,cAAc,MAAM,CAAC","sourcesContent":["export * from './auth';\nexport * from './bindings';\nexport * from './data';\nexport * from './db';\nexport * from './nutrition';\nexport * from './requirements';\nexport * from './state';\nexport * from './storage';\nexport * from './types';\nexport * from './ui';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC;AACvB,cAAc,YAAY,CAAC;AAC3B,cAAc,OAAO,CAAC;AACtB,cAAc,QAAQ,CAAC;AACvB,cAAc,MAAM,CAAC;AACrB,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,SAAS,CAAC;AACxB,cAAc,WAAW,CAAC;AAC1B,cAAc,SAAS,CAAC;AACxB,cAAc,MAAM,CAAC","sourcesContent":["export * from './auth';\nexport * from './bindings';\nexport * from './cli';\nexport * from './data';\nexport * from './db';\nexport * from './nutrition';\nexport * from './requirements';\nexport * from './state';\nexport * from './storage';\nexport * from './types';\nexport * from './ui';\n"]}
package/package.json CHANGED
@@ -1,7 +1,14 @@
1
1
  {
2
2
  "name": "@ankhorage/contracts",
3
- "version": "1.18.3",
3
+ "version": "1.19.1",
4
4
  "main": "./dist/index.js",
5
+ "ankh": {
6
+ "category": "contracts",
7
+ "provider": null,
8
+ "capabilities": [
9
+ "contracts.cli"
10
+ ]
11
+ },
5
12
  "dependencies": {
6
13
  "@ankhorage/color-theory": "^0.0.7"
7
14
  },
@@ -25,6 +32,10 @@
25
32
  "types": "./dist/bindings.d.ts",
26
33
  "default": "./dist/bindings.js"
27
34
  },
35
+ "./cli": {
36
+ "types": "./dist/cli.d.ts",
37
+ "default": "./dist/cli.js"
38
+ },
28
39
  "./data": {
29
40
  "types": "./dist/data/index.d.ts",
30
41
  "default": "./dist/data/index.js"
@@ -0,0 +1,88 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+
3
+ import type {
4
+ AnkhCapabilityId,
5
+ AnkhCommandDescriptor,
6
+ AnkhCommandProviderManifest,
7
+ AnkhPackageMetadata,
8
+ } from './index';
9
+
10
+ describe('cli contracts', () => {
11
+ it('accepts provider package metadata and provider manifests', () => {
12
+ const packageMetadata = {
13
+ category: 'infra',
14
+ provider: './dist/ankh.provider.js',
15
+ capabilities: ['infra.up', 'infra.status'],
16
+ } as const satisfies AnkhPackageMetadata;
17
+
18
+ const upCommand = {
19
+ path: ['up'],
20
+ summary: 'Bring project infrastructure up',
21
+ capability: 'infra.up',
22
+ aliases: ['start'],
23
+ examples: ['ankh infra up shop'],
24
+ } as const satisfies AnkhCommandDescriptor;
25
+
26
+ const manifest = {
27
+ id: '@ankhorage/infra',
28
+ category: 'infra',
29
+ version: '1.0.0',
30
+ capabilities: ['infra.up', 'infra.status'],
31
+ commands: [upCommand],
32
+ } as const satisfies AnkhCommandProviderManifest;
33
+
34
+ expect(JSON.parse(JSON.stringify(packageMetadata))).toEqual(packageMetadata);
35
+ expect(JSON.parse(JSON.stringify(manifest))).toEqual(manifest);
36
+ expect(manifest.id).toBe('@ankhorage/infra');
37
+ expect(manifest.category).toBe('infra');
38
+ expect(manifest.commands[0].path).toEqual(['up']);
39
+ });
40
+
41
+ it('accepts metadata-only packages without a provider module', () => {
42
+ const packageMetadata = {
43
+ category: 'contracts',
44
+ provider: null,
45
+ capabilities: ['contracts.cli'],
46
+ } as const satisfies AnkhPackageMetadata;
47
+
48
+ expect(JSON.parse(JSON.stringify(packageMetadata))).toEqual(packageMetadata);
49
+ expect(packageMetadata.provider).toBeNull();
50
+ });
51
+
52
+ it('keeps command paths relative to the provider category', () => {
53
+ const androidScanCommand = {
54
+ path: ['android', 'scan'],
55
+ summary: 'Scan an Android target',
56
+ capability: 'dev.android.scan',
57
+ examples: ['ankh dev android scan'],
58
+ } as const satisfies AnkhCommandDescriptor;
59
+
60
+ const manifest = {
61
+ id: '@ankhorage/dev',
62
+ category: 'dev',
63
+ version: '1.0.0',
64
+ capabilities: ['dev.android.scan'],
65
+ commands: [androidScanCommand],
66
+ } as const satisfies AnkhCommandProviderManifest;
67
+
68
+ expect(manifest.category).toBe('dev');
69
+ expect(manifest.commands[0].path).toEqual(['android', 'scan']);
70
+ expect(manifest.commands[0].path[0]).not.toBe(manifest.category);
71
+ });
72
+
73
+ it('accepts dot-separated capability ids for discovery metadata', () => {
74
+ const capabilities = [
75
+ 'infra.up',
76
+ 'templates.list',
77
+ 'board.web.import',
78
+ 'contracts.cli',
79
+ ] as const satisfies readonly AnkhCapabilityId[];
80
+
81
+ expect(capabilities).toEqual([
82
+ 'infra.up',
83
+ 'templates.list',
84
+ 'board.web.import',
85
+ 'contracts.cli',
86
+ ]);
87
+ });
88
+ });
package/src/cli.ts ADDED
@@ -0,0 +1,27 @@
1
+ export type AnkhCommandCategory = string;
2
+
3
+ export type AnkhProviderReference = `./${string}`;
4
+
5
+ export type AnkhCapabilityId = `${string}.${string}`;
6
+
7
+ export interface AnkhCommandDescriptor {
8
+ readonly path: readonly [string, ...string[]];
9
+ readonly summary: string;
10
+ readonly capability: AnkhCapabilityId;
11
+ readonly aliases?: readonly string[];
12
+ readonly examples?: readonly string[];
13
+ }
14
+
15
+ export interface AnkhCommandProviderManifest {
16
+ readonly id: string;
17
+ readonly category: AnkhCommandCategory;
18
+ readonly version: string;
19
+ readonly capabilities: readonly AnkhCapabilityId[];
20
+ readonly commands: readonly AnkhCommandDescriptor[];
21
+ }
22
+
23
+ export interface AnkhPackageMetadata {
24
+ readonly category: AnkhCommandCategory;
25
+ readonly provider: AnkhProviderReference | null;
26
+ readonly capabilities: readonly AnkhCapabilityId[];
27
+ }
@@ -5,6 +5,7 @@ import { COLOR_HARMONIES } from '@ankhorage/color-theory';
5
5
  import { describe, expect, it } from 'bun:test';
6
6
 
7
7
  import {
8
+ type AnkhPackageMetadata,
8
9
  APP_CATEGORIES,
9
10
  type AppCategory,
10
11
  type AppManifest,
@@ -56,6 +57,26 @@ async function collectTypeScriptFiles(directory: string): Promise<string[]> {
56
57
  }
57
58
 
58
59
  describe('contracts', () => {
60
+ it('exports the cli subpath for Ankh discovery contracts', async () => {
61
+ const expectedAnkhMetadata = {
62
+ category: 'contracts',
63
+ provider: null,
64
+ capabilities: ['contracts.cli'],
65
+ } as const satisfies AnkhPackageMetadata;
66
+
67
+ const packageJson = JSON.parse(await readFile(join(process.cwd(), 'package.json'), 'utf8')) as {
68
+ ankh?: unknown;
69
+ exports?: Record<string, { default?: string; types?: string }>;
70
+ };
71
+
72
+ expect(packageJson.exports?.['./cli']).toEqual({
73
+ types: './dist/cli.d.ts',
74
+ default: './dist/cli.js',
75
+ });
76
+ expect(packageJson.ankh).toEqual(expectedAnkhMetadata);
77
+ expect(JSON.parse(JSON.stringify(expectedAnkhMetadata))).toEqual(expectedAnkhMetadata);
78
+ });
79
+
59
80
  it('exports stable platform constants', () => {
60
81
  expect(NAVIGATOR_TYPES).toEqual(['stack', 'tabs', 'drawer']);
61
82
  expect(APP_CATEGORIES).toEqual([
@@ -213,7 +234,7 @@ describe('contracts', () => {
213
234
 
214
235
  it('ThemeModeConfig.harmony accepts all ColorHarmony values', () => {
215
236
  for (const harmony of COLOR_HARMONIES) {
216
- const config: ThemeModeConfig = { primaryColor: '#ff0000', harmony };
237
+ const config = { primaryColor: '#ff0000', harmony } satisfies ThemeModeConfig;
217
238
  expect(config.harmony).toBe(harmony);
218
239
  }
219
240
  });
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './auth';
2
2
  export * from './bindings';
3
+ export * from './cli';
3
4
  export * from './data';
4
5
  export * from './db';
5
6
  export * from './nutrition';