@astrale-os/cli 1.0.0-beta.5 → 1.0.0-beta.7

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 (162) hide show
  1. package/dist/astrale.js +66254 -64461
  2. package/dist/public/connect-core.js +14895 -33867
  3. package/dist/public/keys/index.js +1246 -34250
  4. package/dist/public/paths/index.js +866 -33833
  5. package/dist/types/admin/instance/model.d.ts +3 -4
  6. package/dist/types/errors.d.ts +1 -1
  7. package/dist/types/lib/instance-target.d.ts +1 -1
  8. package/dist/types/lib/log.d.ts +2 -2
  9. package/package.json +4 -9
  10. package/src/__tests__/fixtures/publication.ts +20 -0
  11. package/src/admin/__tests__/binding.test.ts +12 -19
  12. package/src/admin/__tests__/fixture.ts +101 -0
  13. package/src/admin/binding.ts +83 -3
  14. package/src/admin/catalog/.spec/api.d.ts +1 -1
  15. package/src/admin/catalog/__tests__/client.test.ts +20 -49
  16. package/src/admin/catalog/client.ts +35 -26
  17. package/src/admin/catalog/model.ts +3 -4
  18. package/src/admin/graph/.spec/api.d.ts +4 -10
  19. package/src/admin/graph/nodes.ts +1 -1
  20. package/src/admin/instance/.spec/api.d.ts +1 -1
  21. package/src/admin/instance/__tests__/client.test.ts +58 -83
  22. package/src/admin/instance/client.ts +49 -39
  23. package/src/admin/instance/model.ts +9 -8
  24. package/src/commands/__tests__/call-describe.test.ts +46 -33
  25. package/src/commands/__tests__/domain-install-operation.test.ts +3 -14
  26. package/src/commands/__tests__/domain-install-owned.test.ts +1 -1
  27. package/src/commands/__tests__/domain-list.test.ts +5 -25
  28. package/src/commands/__tests__/install-identity-override.test.ts +3 -14
  29. package/src/commands/__tests__/read-commands.test.ts +35 -10
  30. package/src/commands/call-describe.ts +59 -67
  31. package/src/commands/call.ts +41 -54
  32. package/src/commands/domain/install.ts +2 -2
  33. package/src/commands/domain/uninstall.ts +2 -2
  34. package/src/commands/get.ts +13 -14
  35. package/src/commands/identity/__tests__/register.test.ts +3 -3
  36. package/src/commands/identity/register.ts +5 -4
  37. package/src/commands/instance/delete.ts +5 -3
  38. package/src/commands/instance/status.ts +1 -9
  39. package/src/commands/introspect.ts +4 -11
  40. package/src/commands/logs.ts +24 -25
  41. package/src/commands/mutate.ts +6 -5
  42. package/src/commands/query.ts +27 -30
  43. package/src/commands/token.ts +8 -7
  44. package/src/commands/view.ts +4 -3
  45. package/src/connection/.spec/api.d.ts +7 -8
  46. package/src/connection/.spec/architecture.md +5 -4
  47. package/src/connection/.spec/flows/session.ts +2 -2
  48. package/src/connection/.spec/laws/connection.ts +6 -2
  49. package/src/connection/.spec/layout.ts +1 -0
  50. package/src/connection/__tests__/credential.test.ts +1 -1
  51. package/src/connection/__tests__/errors.test.ts +27 -20
  52. package/src/connection/__tests__/exchange.test.ts +30 -1
  53. package/src/connection/__tests__/failure-fixtures.ts +39 -0
  54. package/src/connection/__tests__/failure-safety.test.ts +75 -0
  55. package/src/connection/__tests__/self.test.ts +31 -11
  56. package/src/connection/__tests__/session.test.ts +3 -3
  57. package/src/connection/call.ts +1 -1
  58. package/src/connection/credential.ts +2 -2
  59. package/src/connection/errors.ts +1 -355
  60. package/src/connection/exchange.ts +47 -23
  61. package/src/connection/failure/classify.ts +110 -0
  62. package/src/connection/failure/debug.ts +26 -0
  63. package/src/connection/failure/index.ts +18 -0
  64. package/src/connection/failure/model.ts +28 -0
  65. package/src/connection/failure/render.ts +116 -0
  66. package/src/connection/reasons.ts +16 -0
  67. package/src/connection/self.ts +39 -21
  68. package/src/connection/session.ts +8 -8
  69. package/src/errors.ts +3 -2
  70. package/src/graph/.spec/api.d.ts +12 -5
  71. package/src/graph/.spec/layout.ts +9 -1
  72. package/src/graph/__tests__/mutation.test.ts +2 -2
  73. package/src/graph/__tests__/query.test.ts +25 -6
  74. package/src/graph/class.ts +30 -0
  75. package/src/graph/index.ts +1 -0
  76. package/src/graph/query.ts +21 -32
  77. package/src/identity/.spec/api.d.ts +2 -2
  78. package/src/identity/__tests__/registration.test.ts +1 -1
  79. package/src/identity/registration.ts +3 -3
  80. package/src/lib/__tests__/domain-publication.test.ts +4 -15
  81. package/src/lib/__tests__/instance-target.test.ts +6 -39
  82. package/src/lib/__tests__/log-errors.test.ts +53 -0
  83. package/src/lib/__tests__/studio-server-deps.test.ts +1 -1
  84. package/src/lib/command-dx.ts +1 -4
  85. package/src/lib/domain-publication.ts +1 -1
  86. package/src/lib/instance-target.ts +3 -14
  87. package/src/lib/log.ts +11 -6
  88. package/src/lib/validation.ts +2 -2
  89. package/src/lib/view/resolve.ts +1 -1
  90. package/src/program/__tests__/program.test.ts +1 -1
  91. package/studio/client/dist/assets/{elk-api-YEDvaNU-.js → elk-api-Di4OXGNH.js} +1 -1
  92. package/studio/client/dist/assets/index-BLm6J11H.js +81 -0
  93. package/studio/client/dist/assets/index-BiGR8Oo9.css +1 -0
  94. package/studio/client/dist/assets/index-DTjvkOUX.js +8 -0
  95. package/studio/client/dist/index.html +2 -2
  96. package/studio/package.json +2 -7
  97. package/studio/server/agent/ask.test.ts +1 -1
  98. package/studio/server/agent/bridge/grant.test.ts +1 -1
  99. package/studio/server/agent/bridge/routes.test.ts +1 -1
  100. package/studio/server/agent/harness/claude/adapter.test.ts +15 -5
  101. package/studio/server/agent/prompts/anchors.test.ts +32 -36
  102. package/studio/server/agent/prompts/anchors.ts +20 -49
  103. package/studio/server/agent/prompts/system.test.ts +3 -2
  104. package/studio/server/agent/prompts/system.ts +3 -3
  105. package/studio/server/agent/routes.test.ts +1 -1
  106. package/studio/server/agent/run/coordinator.test.ts +1 -1
  107. package/studio/server/api/canvas.ts +0 -1
  108. package/studio/server/api-agent-loadout.test.ts +1 -1
  109. package/studio/server/api.test.ts +1 -1
  110. package/studio/server/cache.test.ts +23 -12
  111. package/studio/server/cache.ts +12 -15
  112. package/studio/server/domain.test.ts +50 -68
  113. package/studio/server/domain.ts +82 -63
  114. package/studio/server/handoff/copy.ts +1 -1
  115. package/studio/server/index.ts +1 -1
  116. package/studio/server/instances/probe.test.ts +1 -1
  117. package/studio/server/introspect/anatomy/views/routes.ts +46 -37
  118. package/studio/server/introspect/anatomy/views.ts +9 -14
  119. package/studio/server/introspect/anatomy-extras.test.ts +26 -42
  120. package/studio/server/introspect/anatomy.test.ts +22 -14
  121. package/studio/server/introspect/anatomy.ts +6 -9
  122. package/studio/server/introspect/bundle.ts +0 -3
  123. package/studio/server/introspect/canonical-schema.test.ts +94 -389
  124. package/studio/server/introspect/canonical-schema.ts +227 -457
  125. package/studio/server/introspect/core-extractor.ts +18 -127
  126. package/studio/server/introspect/core.ts +1 -2
  127. package/studio/server/introspect/diff.test.ts +11 -7
  128. package/studio/server/introspect/diff.ts +18 -55
  129. package/studio/server/introspect/extractor.ts +22 -117
  130. package/studio/server/introspect/overlay-tsmorph.test.ts +119 -240
  131. package/studio/server/introspect/overlay.test.ts +26 -54
  132. package/studio/server/introspect/overlay.ts +10 -37
  133. package/studio/server/introspect/revision.test.ts +1 -1
  134. package/studio/server/introspect/revision.ts +1 -1
  135. package/studio/server/introspect/runtime.test.ts +65 -183
  136. package/studio/server/introspect/runtime.ts +8 -25
  137. package/studio/server/introspect/schema-ir-json.test.ts +70 -0
  138. package/studio/server/introspect/schema-ir-json.ts +38 -68
  139. package/studio/server/introspect/schema-refs.test.ts +43 -88
  140. package/studio/server/introspect/schema-refs.ts +14 -49
  141. package/studio/server/introspect/source-overlay/handlers.ts +116 -636
  142. package/studio/server/introspect/source-overlay/kernel-calls.ts +0 -3
  143. package/studio/server/introspect/source-overlay/spans.ts +15 -45
  144. package/studio/server/state/baseline.test.ts +14 -9
  145. package/studio/server/state/baseline.ts +3 -5
  146. package/studio/server/state/visibility.ts +1 -5
  147. package/studio/server/views/model.ts +3 -3
  148. package/studio/server/views/target.test.ts +17 -74
  149. package/studio/server/views/target.ts +24 -50
  150. package/studio/server/watch.test.ts +11 -11
  151. package/studio/server/watch.ts +8 -4
  152. package/studio/server/workspace-watch.ts +2 -2
  153. package/studio/shared/contracts/schema.ts +38 -107
  154. package/studio/shared/contracts/surface.test.ts +12 -15
  155. package/studio/shared/contracts/workspace.ts +2 -11
  156. package/studio/shared/schema/identity.test.ts +20 -44
  157. package/studio/shared/schema/identity.ts +9 -23
  158. package/viewer/dist/main.js +41 -38
  159. package/studio/client/dist/assets/index-C-PvAXV4.js +0 -81
  160. package/studio/client/dist/assets/index-C4aNQ6dz.css +0 -1
  161. package/studio/client/dist/assets/index-CzpmJD0j.js +0 -8
  162. package/studio/server/introspect/anatomy/views/legacy.ts +0 -232
@@ -1,11 +1,9 @@
1
- import { defineDomain } from '@astrale-os/sdk'
2
- import { issuer } from '@astrale-os/sdk/auth'
3
- import { createDeployment } from '@astrale-os/sdk/deployment'
4
- import { defineSchema } from '@astrale-os/sdk/schema/v1'
1
+ import { defineSchema, schema as schemaApi } from '@astrale-os/sdk/schema'
5
2
  import { afterAll, describe, expect, test } from 'bun:test'
6
3
 
7
4
  import type { DomainInfo } from '../../lib/admin-domain'
8
5
 
6
+ import { releaseFor } from '../../__tests__/fixtures/publication'
9
7
  import { byDefaultThenName, domainProjection, probe, type DomainRow } from '../domain/list'
10
8
 
11
9
  const strip = (s: string): string => s.replace(/\[[0-9;]*m/g, '')
@@ -85,16 +83,7 @@ describe('domain list — canonical Publication check', () => {
85
83
 
86
84
  test('reports the admitted schema revision in machine data', async () => {
87
85
  const schema = defineSchema('catalog-check.example.dev', {})
88
- const definition = defineDomain({
89
- schema,
90
- handlers: { functions: {}, classes: {}, interfaces: {} },
91
- })
92
- const deployed = createDeployment({
93
- definition,
94
- issuer: issuer.accept('https://catalog-check.example.dev'),
95
- bundleHref: 'https://catalog-check.example.dev/domain.bundle.json',
96
- bindings: { callables: [] },
97
- }).publication
86
+ const deployed = releaseFor(schema, 'https://catalog-check.example.dev').publication
98
87
  const server = Bun.serve({
99
88
  port: 0,
100
89
  fetch(request) {
@@ -114,23 +103,14 @@ describe('domain list — canonical Publication check', () => {
114
103
  ),
115
104
  ).resolves.toMatchObject({
116
105
  reachable: true,
117
- schemaRevision: definition.domain.$.revision,
106
+ schemaRevision: schemaApi.revision(schema),
118
107
  checkError: null,
119
108
  })
120
109
  })
121
110
 
122
111
  test('rejects a Publication whose origin differs from the catalog entry', async () => {
123
112
  const schema = defineSchema('deployed.example.dev', {})
124
- const definition = defineDomain({
125
- schema,
126
- handlers: { functions: {}, classes: {}, interfaces: {} },
127
- })
128
- const deployed = createDeployment({
129
- definition,
130
- issuer: issuer.accept('https://deployed.example.dev'),
131
- bundleHref: 'https://deployed.example.dev/domain.bundle.json',
132
- bindings: { callables: [] },
133
- }).publication
113
+ const deployed = releaseFor(schema, 'https://deployed.example.dev').publication
134
114
  const server = Bun.serve({
135
115
  port: 0,
136
116
  fetch: () => Response.json(deployed),
@@ -1,9 +1,7 @@
1
- import { defineDomain } from '@astrale-os/sdk'
2
- import { issuer } from '@astrale-os/sdk/auth'
3
- import { createDeployment } from '@astrale-os/sdk/deployment'
4
- import { defineSchema } from '@astrale-os/sdk/schema/v1'
1
+ import { defineSchema } from '@astrale-os/sdk/schema'
5
2
  import { afterAll, describe, expect, test } from 'bun:test'
6
3
 
4
+ import { releaseFor } from '../../__tests__/fixtures/publication'
7
5
  import { domainRefFromTarget, isIdentityOverride, probeDeclaredOrigin } from '../domain/install'
8
6
 
9
7
  describe('admin-path target classification', () => {
@@ -47,16 +45,7 @@ describe('declared-origin Publication probe', () => {
47
45
 
48
46
  test('reads origin from a well-formed canonical Publication', async () => {
49
47
  const schema = defineSchema('crm.acme.dev', {})
50
- const definition = defineDomain({
51
- schema,
52
- handlers: { functions: {}, classes: {}, interfaces: {} },
53
- })
54
- const deployed = createDeployment({
55
- definition,
56
- issuer: issuer.accept('https://crm.acme.dev'),
57
- bundleHref: 'https://crm.acme.dev/domain.bundle.json',
58
- bindings: { callables: [] },
59
- }).publication
48
+ const deployed = releaseFor(schema, 'https://crm.acme.dev').publication
60
49
  const url = servePublication((req) =>
61
50
  new URL(req.url).pathname === '/.well-known/astrale/domain.json'
62
51
  ? Response.json(deployed)
@@ -36,6 +36,8 @@ let mutations: unknown[] = []
36
36
  let queryResult: unknown
37
37
  let getResult: unknown
38
38
  let mutationResult: unknown
39
+ let selfFailure: unknown
40
+ let selfContexts: unknown[] = []
39
41
 
40
42
  const queryMock = mock(async (ast: unknown, options?: unknown) => {
41
43
  queryCalls.push({ ast, options })
@@ -49,7 +51,9 @@ const mutateMock = mock(async (mutation: unknown) => {
49
51
  mutations.push(mutation)
50
52
  return mutationResult
51
53
  })
52
- const expandSelfInPathMock = mock(async (path: string) => {
54
+ const expandSelfInPathMock = mock(async (path: string, context: unknown) => {
55
+ selfContexts.push(context)
56
+ if (selfFailure !== undefined) throw selfFailure
53
57
  if (path === '@self') {
54
58
  return {
55
59
  path: '@expanded-self',
@@ -59,10 +63,11 @@ const expandSelfInPathMock = mock(async (path: string) => {
59
63
  return { path, meta: undefined }
60
64
  })
61
65
  const withSelfHintMock = mock(async (action: () => Promise<unknown>) => action())
66
+ const commandContext = {
67
+ graph: { query: queryMock, getOrThrow: getOrThrowMock, mutate: mutateMock },
68
+ }
62
69
  const runKernelCommandMock = mock(async (run: RunOpts) => {
63
- const result = await run.fn({
64
- graph: { query: queryMock, getOrThrow: getOrThrowMock, mutate: mutateMock },
65
- })
70
+ const result = await run.fn(commandContext)
66
71
  await run.format?.(result, run.opts, true)
67
72
  })
68
73
 
@@ -88,6 +93,8 @@ beforeEach(() => {
88
93
  }
89
94
  getResult = undefined
90
95
  mutationResult = { createdNodes: {} }
96
+ selfFailure = undefined
97
+ selfContexts = []
91
98
  queryMock.mockClear()
92
99
  getOrThrowMock.mockClear()
93
100
  mutateMock.mockClear()
@@ -161,7 +168,7 @@ describe('query command', () => {
161
168
  ])
162
169
  })
163
170
 
164
- test('authors a Definition source through the same canonical Query V6 call', async () => {
171
+ test('authors a Class source through the same canonical Query V6 call', async () => {
165
172
  const { queryCommand } = await import('../query')
166
173
 
167
174
  await queryCommand([], {
@@ -179,8 +186,8 @@ describe('query command', () => {
179
186
  kind: 'node',
180
187
  terms: [
181
188
  {
182
- kind: 'definition',
183
- definition: { origin: 'issues.astrale.ai', kind: 'class', name: 'Issue' },
189
+ kind: 'class',
190
+ class: { origin: 'issues.astrale.ai', kind: 'class', name: 'Issue' },
184
191
  },
185
192
  ],
186
193
  binding: 'n0',
@@ -193,6 +200,17 @@ describe('query command', () => {
193
200
  ])
194
201
  })
195
202
 
203
+ test('resolves @self inside the command session and never relabels transport failure as input', async () => {
204
+ const { queryCommand } = await import('../query')
205
+ await queryCommand(['@self'], { json: true, limit: '2' })
206
+ expect(selfContexts).toEqual([commandContext])
207
+
208
+ const aggregate = new AggregateError([], '')
209
+ selfFailure = aggregate
210
+ await expect(queryCommand(['@self'], { json: true, limit: '2' })).rejects.toBe(aggregate)
211
+ expect(errors.join('\n')).not.toContain('INVALID_INPUT')
212
+ })
213
+
196
214
  /** @evidence TEST-CLI-QUERY-REJECTS-V1-BEFORE-CONNECTION */
197
215
  test('rejects a legacy AST before opening the command connection', async () => {
198
216
  const { queryCommand } = await import('../query')
@@ -215,7 +233,7 @@ describe('get command', () => {
215
233
  const { getCommand } = await import('../get')
216
234
  getResult = {
217
235
  id: 'note-1',
218
- class: '/:notes.example.dev:class.Note',
236
+ class: 'notes.example.dev:class.Note',
219
237
  props: { 'notes.example.dev:class.Note.property.title': 'Hello' },
220
238
  }
221
239
 
@@ -226,6 +244,13 @@ describe('get command', () => {
226
244
  expect(stdout).not.toContain('__labels')
227
245
  expect(stdout).not.toContain('classId')
228
246
  })
247
+
248
+ test('resolves @self through the same point-read command context', async () => {
249
+ const { getCommand } = await import('../get')
250
+ await getCommand('@self', { json: true })
251
+ expect(selfContexts).toEqual([commandContext])
252
+ expect(getTargets).toEqual(['@expanded-self'])
253
+ })
229
254
  })
230
255
 
231
256
  describe('mutate command', () => {
@@ -242,7 +267,7 @@ describe('mutate command', () => {
242
267
  {
243
268
  op: 'node.create',
244
269
  as: 'note',
245
- class: '/:notes.example.dev:class.Note',
270
+ class: 'notes.example.dev:class.Note',
246
271
  props: {},
247
272
  },
248
273
  ],
@@ -258,7 +283,7 @@ describe('mutate command', () => {
258
283
  {
259
284
  op: 'node.create',
260
285
  as: 'note',
261
- class: '/:notes.example.dev:class.Note',
286
+ class: 'notes.example.dev:class.Note',
262
287
  props: {},
263
288
  },
264
289
  ],
@@ -1,8 +1,9 @@
1
1
  import type { Path } from '@astrale-os/sdk/graph/path'
2
+ import type { ResolvedFunction, ResolvedMethod } from '@astrale-os/sdk/schema'
2
3
 
3
- import { AstraleError } from '../errors'
4
+ import { bundle, schema } from '@astrale-os/sdk/schema'
4
5
 
5
- type JsonRecord = Record<string, unknown>
6
+ import { AstraleError } from '../errors'
6
7
 
7
8
  export interface CallableDescription {
8
9
  readonly path: string
@@ -18,40 +19,40 @@ export interface CallableDescription {
18
19
  readonly candidates?: readonly CallableDescription[]
19
20
  }
20
21
 
21
- /** Resolve one callable's input/output from an installed Domain schema document. */
22
- export function describeCallableFromSchema(
22
+ /** Resolve one callable from an admitted installed Domain bundle. */
23
+ export function describeCallableFromBundle(
23
24
  path: Path,
24
- schema: unknown,
25
+ input: unknown,
25
26
  ): CallableDescription | undefined {
26
27
  if (path.ast.anchor.kind !== 'domain') return undefined
27
28
  const origin = path.ast.anchor.origin
28
29
  const last = path.ast.steps.at(-1)
29
- const document = asRecord(schema)
30
- if (document === undefined || last === undefined) return undefined
30
+ if (last === undefined) return undefined
31
+ const domain = schema.resolve(bundle.accept(input).root)
32
+ if (domain.origin !== origin) return undefined
31
33
 
32
34
  if (last.kind === 'method') {
33
- const owner = path.ast.steps.at(-2)
34
- if (
35
- owner?.kind === 'projection' &&
36
- (owner.projection.kind === 'class' || owner.projection.kind === 'interface')
37
- ) {
38
- const bag = asRecord(
39
- owner.projection.kind === 'class' ? document.classes : document.interfaces,
35
+ const ownerStep = path.ast.steps.at(-2)
36
+ if (ownerStep?.kind === 'projection' && ownerStep.projection.kind === 'class') {
37
+ const owner = domain.classes[ownerStep.projection.name]
38
+ if (owner?.kind !== 'node') return undefined
39
+ const method = namedMethod(
40
+ last.dispatch === 'static' ? owner.static.methods : owner.methods,
41
+ last.name,
42
+ last.dispatch,
40
43
  )
41
- const definition = asRecord(bag?.[owner.projection.name])
42
- const method = asRecord(asRecord(definition?.methods)?.[last.name])
43
- if (method !== undefined) {
44
- return Object.freeze({
45
- path: path.raw,
46
- origin,
47
- class: owner.projection.name,
48
- method: last.name,
49
- dispatch: last.dispatch,
50
- ...callableFields(method),
51
- })
52
- }
44
+ return method === undefined
45
+ ? undefined
46
+ : description(path, origin, owner.ref.name, method, last.dispatch)
53
47
  }
54
- const matches = findNamedMethods(document, origin, path.raw, last.name, last.dispatch)
48
+
49
+ const matches = Object.values(domain.classes).flatMap((owner) => {
50
+ if (owner.kind !== 'node') return []
51
+ const method = namedMethod(owner.methods, last.name, last.dispatch)
52
+ return method === undefined
53
+ ? []
54
+ : [description(path, origin, owner.ref.name, method, last.dispatch)]
55
+ })
55
56
  if (matches.length === 1) return matches[0]
56
57
  if (matches.length > 1) {
57
58
  return Object.freeze({
@@ -66,13 +67,13 @@ export function describeCallableFromSchema(
66
67
  }
67
68
 
68
69
  if (last.kind === 'projection' && last.projection.kind === 'function') {
69
- const fn = asRecord(asRecord(document.functions)?.[last.projection.name])
70
- if (fn === undefined) return undefined
70
+ const callable = domain.functions[last.projection.name]
71
+ if (callable === undefined) return undefined
71
72
  return Object.freeze({
72
73
  path: path.raw,
73
74
  origin,
74
- function: last.projection.name,
75
- ...callableFields(fn),
75
+ function: callable.ref.name,
76
+ ...callableFields(callable),
76
77
  })
77
78
  }
78
79
  return undefined
@@ -86,46 +87,37 @@ export function missingCallableDescription(path: string): AstraleError {
86
87
  )
87
88
  }
88
89
 
89
- function findNamedMethods(
90
- schema: JsonRecord,
90
+ function namedMethod(
91
+ methods: Iterable<ResolvedMethod>,
92
+ name: string,
93
+ dispatch: 'static' | 'instance',
94
+ ): ResolvedMethod | undefined {
95
+ const expectedStatic = dispatch === 'static'
96
+ return [...methods].find((method) => method.name === name && method.static === expectedStatic)
97
+ }
98
+
99
+ function description(
100
+ path: Path,
91
101
  origin: string,
92
- path: string,
93
- method: string,
102
+ className: string,
103
+ callable: ResolvedMethod,
94
104
  dispatch: 'static' | 'instance',
95
- ): CallableDescription[] {
96
- const matches: CallableDescription[] = []
97
- for (const bag of [schema.classes, schema.interfaces]) {
98
- const definitions = asRecord(bag)
99
- if (definitions === undefined) continue
100
- for (const [className, definition] of Object.entries(definitions)) {
101
- const methodDef = asRecord(asRecord(asRecord(definition)?.methods)?.[method])
102
- if (methodDef === undefined) continue
103
- matches.push(
104
- Object.freeze({
105
- path,
106
- origin,
107
- class: className,
108
- method,
109
- dispatch,
110
- ...callableFields(methodDef),
111
- }),
112
- )
113
- }
114
- }
115
- return matches
105
+ ): CallableDescription {
106
+ return Object.freeze({
107
+ path: path.raw,
108
+ origin,
109
+ class: className,
110
+ method: callable.name,
111
+ dispatch,
112
+ ...callableFields(callable),
113
+ })
116
114
  }
117
115
 
118
- function callableFields(value: JsonRecord): Partial<CallableDescription> {
116
+ function callableFields(callable: ResolvedFunction | ResolvedMethod): Partial<CallableDescription> {
119
117
  return {
120
- ...(typeof value.description === 'string' ? { description: value.description } : {}),
121
- ...(value.auth === undefined ? {} : { auth: value.auth }),
122
- ...(value.input === undefined ? {} : { input: value.input }),
123
- ...(value.output === undefined ? {} : { output: value.output }),
118
+ ...(callable.description === undefined ? {} : { description: callable.description }),
119
+ auth: callable.auth,
120
+ input: callable.input,
121
+ output: callable.output,
124
122
  }
125
123
  }
126
-
127
- function asRecord(input: unknown): JsonRecord | undefined {
128
- return input !== null && typeof input === 'object' && !Array.isArray(input)
129
- ? (input as JsonRecord)
130
- : undefined
131
- }
@@ -5,7 +5,7 @@ import type { CommandDefinition } from '../program/index'
5
5
 
6
6
  import { createPathCall, expandSelfInCall, runKernelCommand, withSelfHint } from '../connection'
7
7
  import { presentBinary } from '../lib/binary'
8
- import { failClosed, log } from '../lib/log'
8
+ import { failInput, log } from '../lib/log'
9
9
  import { output, present } from '../lib/output'
10
10
 
11
11
  type CallOpts = KernelCommandOpts & {
@@ -24,60 +24,47 @@ export async function callCommand(
24
24
  rawParams: string[],
25
25
  opts: CallOpts,
26
26
  ): Promise<void> {
27
- // Expand @self before describe/execute through authenticated Identity.whoami.
28
- let expanded: Awaited<ReturnType<typeof expandSelfInCall>>
29
- try {
30
- expanded = await expandSelfInCall(path, rawParams, opts)
31
- } catch (error) {
32
- failClosed(error, opts)
33
- }
34
- const expandedPath = expanded.path
35
- try {
36
- Path.parse(expandedPath)
37
- } catch (error) {
38
- failClosed(error, opts)
39
- }
40
-
41
- // ── Parse params ────────────────────────────────────────
42
27
  let params: Record<string, unknown>
43
28
  try {
44
- params = await parseParams([...expanded.parameters], opts.data)
45
- } catch (e) {
46
- failClosed(e, opts)
47
- }
48
-
49
- // ── Dry-run: show what would be sent ─────────────────────
50
- if (opts.dryRun) {
51
- output({ method: expandedPath, params }, opts)
52
- return
29
+ Path.parse(path)
30
+ params = await parseParams(rawParams, opts.data)
31
+ } catch (error) {
32
+ failInput(error, opts)
53
33
  }
54
34
 
55
- // ── Execute ────────────────────────────────────────────
56
- await runKernelCommand<MaterializedCallResult>({
57
- opts,
58
- label: expandedPath,
59
- fn: (ctx) =>
60
- withSelfHint(
61
- async () =>
62
- materializeCallResult(await ctx.session.dispatch(createPathCall(expandedPath, params))),
63
- expanded.meta,
64
- ),
65
- format: async (result, fmtOpts) => {
66
- switch (result.kind) {
67
- case 'value':
68
- present(result.value, fmtOpts)
69
- return
70
- case 'binary':
71
- await presentBinary(result.value, fmtOpts, { outFile: opts.output })
72
- return
73
- case 'stream':
74
- output(result.values, fmtOpts)
75
- return
76
- case 'redirect':
77
- throw new Error('Client Session returned an unresolved redirect.')
78
- }
35
+ await runKernelCommand<MaterializedCallResult | { readonly kind: 'dry'; readonly call: unknown }>(
36
+ {
37
+ opts,
38
+ label: path,
39
+ fn: async (ctx) => {
40
+ const expanded = await expandSelfInCall(path, params, ctx)
41
+ const request = createPathCall(expanded.path, expanded.parameters)
42
+ if (opts.dryRun) return { kind: 'dry', call: request }
43
+ return withSelfHint(
44
+ async () => materializeCallResult(await ctx.session.dispatch(request)),
45
+ expanded.meta,
46
+ )
47
+ },
48
+ format: async (result, fmtOpts) => {
49
+ switch (result.kind) {
50
+ case 'dry':
51
+ output(result.call, fmtOpts)
52
+ return
53
+ case 'value':
54
+ present(result.value, fmtOpts)
55
+ return
56
+ case 'binary':
57
+ await presentBinary(result.value, fmtOpts, { outFile: opts.output })
58
+ return
59
+ case 'stream':
60
+ output(result.values, fmtOpts)
61
+ return
62
+ case 'redirect':
63
+ throw new Error('Client Session returned an unresolved redirect.')
64
+ }
65
+ },
79
66
  },
80
- })
67
+ )
81
68
  }
82
69
 
83
70
  /** Drain a session-backed stream before the command-scoped Client Session closes. */
@@ -101,7 +88,7 @@ export async function parseParams(
101
88
  try {
102
89
  return JSON.parse(dataFlag)
103
90
  } catch {
104
- throw new Error(`Invalid JSON in --data: ${dataFlag}`)
91
+ throw new TypeError(`Invalid JSON in --data: ${dataFlag}`)
105
92
  }
106
93
  }
107
94
 
@@ -114,7 +101,7 @@ export async function parseParams(
114
101
  try {
115
102
  return JSON.parse(stdin)
116
103
  } catch {
117
- throw new Error('Invalid JSON from stdin')
104
+ throw new TypeError('Invalid JSON from stdin')
118
105
  }
119
106
  }
120
107
 
@@ -142,7 +129,7 @@ export function parseKeyValue(pairs: string[]): Record<string, unknown> {
142
129
  for (const pair of pairs) {
143
130
  const eqIdx = pair.indexOf('=')
144
131
  if (eqIdx === -1) {
145
- throw new Error(`Invalid param "${pair}" — expected key=value format`)
132
+ throw new TypeError(`Invalid param "${pair}" — expected key=value format`)
146
133
  }
147
134
  const key = pair.slice(0, eqIdx)
148
135
  const raw = pair.slice(eqIdx + 1)
@@ -150,7 +137,7 @@ export function parseKeyValue(pairs: string[]): Record<string, unknown> {
150
137
  const hint = key.endsWith(':')
151
138
  ? ` (looks like httpie's "key:=value" syntax — Astrale CLI doesn't support it; use --data '{"${key.slice(0, -1)}":<value>}' for nested values)`
152
139
  : ` (keys must be identifier-shaped: letters, digits, underscore, hyphen)`
153
- throw new Error(`Invalid param key "${key}" in "${pair}"${hint}`)
140
+ throw new TypeError(`Invalid param key "${key}" in "${pair}"${hint}`)
154
141
  }
155
142
  result[key] = coerceValue(raw)
156
143
  }
@@ -1,5 +1,5 @@
1
1
  import { Path } from '@astrale-os/sdk/graph/path'
2
- import { syscalls } from '@astrale-os/sdk/schema/kernel'
2
+ import { K } from '@astrale-os/sdk/schema'
3
3
  import chalk from 'chalk'
4
4
 
5
5
  import type { KernelCommandOpts } from '../../connection'
@@ -414,7 +414,7 @@ export async function installDirect(
414
414
  fn: async ({ session }) =>
415
415
  (await session.call(
416
416
  createPathCall(
417
- Path.project(syscalls.install.ref).raw,
417
+ Path.project(K.functions.install.ref).raw,
418
418
  directInstallCallInput(url, operation, opts.token),
419
419
  ),
420
420
  )) as DirectInstallResult,
@@ -1,5 +1,5 @@
1
1
  import { Path } from '@astrale-os/sdk/graph/path'
2
- import { syscalls } from '@astrale-os/sdk/schema/kernel'
2
+ import { K } from '@astrale-os/sdk/schema'
3
3
  import chalk from 'chalk'
4
4
 
5
5
  import type { KernelCommandOpts } from '../../connection'
@@ -77,7 +77,7 @@ Examples:
77
77
  label: `Uninstalling domain ${origin}`,
78
78
  fn: async ({ session }) =>
79
79
  (await session.call(
80
- createPathCall(Path.project(syscalls.uninstall.ref).raw, uninstallCallInput(origin)),
80
+ createPathCall(Path.project(K.functions.uninstall.ref).raw, uninstallCallInput(origin)),
81
81
  )) as UninstallResult,
82
82
  format: (result, formatOpts, machine) => {
83
83
  if (machine) {
@@ -4,40 +4,39 @@ import type { KernelCommandOpts } from '../connection'
4
4
  import type { CommandDefinition } from '../program/index'
5
5
 
6
6
  import { expandSelfInPath, runKernelCommand, withSelfHint } from '../connection'
7
- import { formatKernelError } from '../connection/errors'
8
7
  import { AstraleError } from '../errors'
9
- import { denoise, isMachine, output } from '../lib/output'
8
+ import { failInput, fatal } from '../lib/log'
9
+ import { denoise, output } from '../lib/output'
10
10
 
11
11
  type GetOpts = KernelCommandOpts & { schema?: boolean }
12
12
 
13
13
  /** Read one exact canonical Node. The caller's Path is not fabricated into the Node value. */
14
14
  export async function getCommand(target: string, opts: GetOpts): Promise<void> {
15
- let path: string
16
- let meta
17
15
  let parsed
18
16
  try {
19
- ;({ path, meta } = await expandSelfInPath(target, opts))
20
- parsed = Path.parse(path)
17
+ parsed = Path.parse(target)
21
18
  } catch (error) {
22
- await formatKernelError(error, isMachine(opts), undefined, opts.debug)
23
- process.exit(1)
19
+ failInput(error, opts)
24
20
  }
25
21
 
26
22
  const last = parsed.ast.steps.at(-1)
27
23
  if (last?.kind === 'method') {
28
24
  const error = new AstraleError(
29
25
  'NOT_A_NODE',
30
- `${path} is a callable Path, not a Node.`,
31
- `Use \`astrale call ${path}\` or \`astrale introspect ${path}\`.`,
26
+ `${target} is a callable Path, not a Node.`,
27
+ `Use \`astrale call ${target}\` or \`astrale introspect ${target}\`.`,
32
28
  )
33
- await formatKernelError(error, isMachine(opts), undefined, opts.debug)
34
- process.exit(1)
29
+ fatal(error, opts)
35
30
  }
36
31
 
37
32
  await runKernelCommand({
38
33
  opts,
39
- label: `Node ${path}`,
40
- fn: ({ graph }) => withSelfHint(() => graph.getOrThrow(parsed), meta),
34
+ label: `Node ${target}`,
35
+ fn: async (context) => {
36
+ const { path, meta } = await expandSelfInPath(target, context)
37
+ const resolved = meta === undefined ? parsed : Path.parse(path)
38
+ return withSelfHint(() => context.graph.getOrThrow(resolved), meta)
39
+ },
41
40
  format: (node, format) => output(opts.schema === true ? node : denoise(node), format),
42
41
  })
43
42
  }
@@ -1,9 +1,9 @@
1
1
  import { issuer, jwk, provision } from '@astrale-os/sdk/auth'
2
- import { ClassPath } from '@astrale-os/sdk/graph/class'
3
2
  import { normalizeProperties } from '@astrale-os/sdk/graph/properties'
4
3
  import { expect, mock, test } from 'bun:test'
5
4
  import { exportJWK, generateKeyPair, jwtVerify } from 'jose'
6
5
 
6
+ import { classKey } from '../../../graph'
7
7
  import { formatIdentityRegistration, prepareIdentityProvision } from '../register'
8
8
 
9
9
  /** @evidence TEST-CLI-IDENTITY-REGISTER-JSON-EXACT */
@@ -46,7 +46,7 @@ test('builds one exact Mutation V3 identity birth bound to a self proof', async
46
46
  kid: 'alice-key',
47
47
  })
48
48
  const kernelIssuer = issuer.accept('https://kernel.example')
49
- const classPath = ClassPath.parse('/:accounts.example:class.User')
49
+ const classPath = classKey('/:accounts.example:class.User', '--class')
50
50
  const properties = normalizeProperties({
51
51
  'accounts.example:class.User.property.name': 'Alice',
52
52
  })
@@ -70,7 +70,7 @@ test('builds one exact Mutation V3 identity birth bound to a self proof', async
70
70
  {
71
71
  op: 'node.create',
72
72
  as: 'identity',
73
- class: '/:accounts.example:class.User',
73
+ class: 'accounts.example:class.User',
74
74
  props: { 'accounts.example:class.User.property.name': 'Alice' },
75
75
  },
76
76
  ],