@astrale-os/cli 1.0.0-beta.4 → 1.0.0-beta.6

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 (173) hide show
  1. package/dist/astrale.js +55649 -59388
  2. package/dist/public/connect-core.js +1972 -26831
  3. package/dist/public/keys/index.js +1754 -40686
  4. package/dist/public/paths/index.js +859 -39511
  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/idp.d.ts +1 -0
  8. package/dist/types/lib/instance-target.d.ts +1 -1
  9. package/dist/types/lib/log.d.ts +2 -2
  10. package/dist/types/lib/update.d.ts +30 -0
  11. package/package.json +4 -6
  12. package/src/__tests__/fixtures/publication.ts +20 -0
  13. package/src/admin/__tests__/binding.test.ts +14 -21
  14. package/src/admin/__tests__/fixture.ts +101 -0
  15. package/src/admin/binding.ts +86 -6
  16. package/src/admin/catalog/.spec/api.d.ts +2 -2
  17. package/src/admin/catalog/__tests__/client.test.ts +20 -49
  18. package/src/admin/catalog/client.ts +35 -26
  19. package/src/admin/catalog/model.ts +3 -4
  20. package/src/admin/graph/.spec/api.d.ts +4 -10
  21. package/src/admin/graph/nodes.ts +1 -1
  22. package/src/admin/instance/.spec/api.d.ts +2 -2
  23. package/src/admin/instance/__tests__/client.test.ts +58 -83
  24. package/src/admin/instance/client.ts +49 -39
  25. package/src/admin/instance/model.ts +9 -8
  26. package/src/commands/__tests__/call-describe.test.ts +46 -33
  27. package/src/commands/__tests__/domain-install-operation.test.ts +3 -14
  28. package/src/commands/__tests__/domain-install-owned.test.ts +1 -1
  29. package/src/commands/__tests__/domain-list.test.ts +5 -25
  30. package/src/commands/__tests__/install-identity-override.test.ts +3 -14
  31. package/src/commands/__tests__/read-commands.test.ts +35 -10
  32. package/src/commands/__tests__/update.test.ts +25 -3
  33. package/src/commands/call-describe.ts +59 -67
  34. package/src/commands/call.ts +41 -54
  35. package/src/commands/domain/install.ts +2 -2
  36. package/src/commands/domain/uninstall.ts +2 -2
  37. package/src/commands/get.ts +13 -14
  38. package/src/commands/identity/__tests__/register.test.ts +3 -3
  39. package/src/commands/identity/register.ts +5 -4
  40. package/src/commands/instance/delete.ts +5 -3
  41. package/src/commands/instance/status.ts +1 -9
  42. package/src/commands/introspect.ts +4 -11
  43. package/src/commands/logs.ts +24 -25
  44. package/src/commands/mutate.ts +6 -5
  45. package/src/commands/query.ts +27 -30
  46. package/src/commands/token.ts +8 -7
  47. package/src/commands/update.ts +31 -11
  48. package/src/commands/view.ts +4 -3
  49. package/src/connection/.spec/api.d.ts +7 -8
  50. package/src/connection/.spec/architecture.md +5 -4
  51. package/src/connection/.spec/flows/session.ts +2 -2
  52. package/src/connection/.spec/laws/connection.ts +6 -2
  53. package/src/connection/.spec/layout.ts +1 -0
  54. package/src/connection/__tests__/credential.test.ts +1 -1
  55. package/src/connection/__tests__/errors.test.ts +27 -20
  56. package/src/connection/__tests__/exchange.test.ts +30 -1
  57. package/src/connection/__tests__/failure-fixtures.ts +39 -0
  58. package/src/connection/__tests__/failure-safety.test.ts +75 -0
  59. package/src/connection/__tests__/self.test.ts +31 -11
  60. package/src/connection/__tests__/session.test.ts +3 -3
  61. package/src/connection/call.ts +1 -1
  62. package/src/connection/credential.ts +2 -2
  63. package/src/connection/errors.ts +1 -355
  64. package/src/connection/exchange.ts +47 -23
  65. package/src/connection/failure/classify.ts +110 -0
  66. package/src/connection/failure/debug.ts +26 -0
  67. package/src/connection/failure/index.ts +18 -0
  68. package/src/connection/failure/model.ts +28 -0
  69. package/src/connection/failure/render.ts +116 -0
  70. package/src/connection/reasons.ts +16 -0
  71. package/src/connection/self.ts +39 -21
  72. package/src/connection/session.ts +8 -8
  73. package/src/errors.ts +3 -2
  74. package/src/graph/.spec/api.d.ts +12 -5
  75. package/src/graph/.spec/layout.ts +9 -1
  76. package/src/graph/__tests__/mutation.test.ts +2 -2
  77. package/src/graph/__tests__/query.test.ts +25 -6
  78. package/src/graph/class.ts +30 -0
  79. package/src/graph/index.ts +1 -0
  80. package/src/graph/query.ts +21 -32
  81. package/src/identity/.spec/api.d.ts +2 -2
  82. package/src/identity/__tests__/registration.test.ts +1 -1
  83. package/src/identity/registration.ts +3 -3
  84. package/src/lib/__tests__/domain-publication.test.ts +4 -15
  85. package/src/lib/__tests__/idp.test.ts +25 -0
  86. package/src/lib/__tests__/instance-target.test.ts +6 -39
  87. package/src/lib/__tests__/log-errors.test.ts +53 -0
  88. package/src/lib/__tests__/studio-server-deps.test.ts +1 -1
  89. package/src/lib/__tests__/update.test.ts +101 -8
  90. package/src/lib/__tests__/view-open-intent.test.ts +0 -2
  91. package/src/lib/__tests__/view-server.test.ts +1 -1
  92. package/src/lib/__tests__/view-session.test.ts +1 -1
  93. package/src/lib/command-dx.ts +1 -4
  94. package/src/lib/domain-publication.ts +1 -1
  95. package/src/lib/idp.ts +29 -1
  96. package/src/lib/instance-target.ts +3 -14
  97. package/src/lib/log.ts +11 -6
  98. package/src/lib/update.ts +120 -26
  99. package/src/lib/validation.ts +2 -2
  100. package/src/lib/view/resolve.ts +1 -1
  101. package/src/program/__tests__/program.test.ts +1 -1
  102. package/studio/client/dist/assets/{elk-api-lwhFo7vB.js → elk-api-Di4OXGNH.js} +1 -1
  103. package/studio/client/dist/assets/index-BLm6J11H.js +81 -0
  104. package/studio/client/dist/assets/index-BiGR8Oo9.css +1 -0
  105. package/studio/client/dist/assets/index-DTjvkOUX.js +8 -0
  106. package/studio/client/dist/index.html +2 -2
  107. package/studio/package.json +2 -9
  108. package/studio/server/agent/ask.test.ts +1 -1
  109. package/studio/server/agent/bridge/grant.test.ts +1 -1
  110. package/studio/server/agent/bridge/routes.test.ts +1 -1
  111. package/studio/server/agent/harness/claude/adapter.test.ts +15 -5
  112. package/studio/server/agent/prompts/anchors.test.ts +32 -36
  113. package/studio/server/agent/prompts/anchors.ts +20 -49
  114. package/studio/server/agent/prompts/system.test.ts +3 -2
  115. package/studio/server/agent/prompts/system.ts +3 -3
  116. package/studio/server/agent/routes.test.ts +1 -1
  117. package/studio/server/agent/run/coordinator.test.ts +1 -1
  118. package/studio/server/api/canvas.ts +0 -1
  119. package/studio/server/api-agent-loadout.test.ts +1 -1
  120. package/studio/server/api.test.ts +1 -1
  121. package/studio/server/cache.test.ts +23 -12
  122. package/studio/server/cache.ts +12 -15
  123. package/studio/server/domain.test.ts +50 -68
  124. package/studio/server/domain.ts +82 -63
  125. package/studio/server/handoff/copy.ts +1 -1
  126. package/studio/server/index.ts +1 -1
  127. package/studio/server/instances/probe.test.ts +1 -1
  128. package/studio/server/introspect/anatomy/views/routes.ts +46 -37
  129. package/studio/server/introspect/anatomy/views.ts +9 -14
  130. package/studio/server/introspect/anatomy-extras.test.ts +26 -42
  131. package/studio/server/introspect/anatomy.test.ts +22 -14
  132. package/studio/server/introspect/anatomy.ts +6 -9
  133. package/studio/server/introspect/bundle.ts +0 -3
  134. package/studio/server/introspect/canonical-schema.test.ts +94 -389
  135. package/studio/server/introspect/canonical-schema.ts +227 -457
  136. package/studio/server/introspect/core-extractor.ts +18 -127
  137. package/studio/server/introspect/core.ts +1 -2
  138. package/studio/server/introspect/diff.test.ts +11 -7
  139. package/studio/server/introspect/diff.ts +18 -55
  140. package/studio/server/introspect/extractor.ts +22 -117
  141. package/studio/server/introspect/overlay-tsmorph.test.ts +119 -240
  142. package/studio/server/introspect/overlay.test.ts +26 -54
  143. package/studio/server/introspect/overlay.ts +10 -37
  144. package/studio/server/introspect/revision.test.ts +1 -1
  145. package/studio/server/introspect/revision.ts +1 -1
  146. package/studio/server/introspect/runtime.test.ts +65 -183
  147. package/studio/server/introspect/runtime.ts +8 -25
  148. package/studio/server/introspect/schema-ir-json.test.ts +70 -0
  149. package/studio/server/introspect/schema-ir-json.ts +38 -68
  150. package/studio/server/introspect/schema-refs.test.ts +43 -88
  151. package/studio/server/introspect/schema-refs.ts +14 -49
  152. package/studio/server/introspect/source-overlay/handlers.ts +116 -636
  153. package/studio/server/introspect/source-overlay/kernel-calls.ts +0 -3
  154. package/studio/server/introspect/source-overlay/spans.ts +15 -45
  155. package/studio/server/state/baseline.test.ts +14 -9
  156. package/studio/server/state/baseline.ts +3 -5
  157. package/studio/server/state/visibility.ts +1 -5
  158. package/studio/server/views/model.ts +3 -3
  159. package/studio/server/views/target.test.ts +17 -74
  160. package/studio/server/views/target.ts +24 -50
  161. package/studio/server/watch.test.ts +11 -11
  162. package/studio/server/watch.ts +8 -4
  163. package/studio/server/workspace-watch.ts +2 -2
  164. package/studio/shared/contracts/schema.ts +38 -107
  165. package/studio/shared/contracts/surface.test.ts +13 -15
  166. package/studio/shared/contracts/workspace.ts +2 -11
  167. package/studio/shared/schema/identity.test.ts +20 -44
  168. package/studio/shared/schema/identity.ts +9 -23
  169. package/viewer/dist/main.js +40 -61
  170. package/studio/client/dist/assets/index-B-c-smo9.js +0 -8
  171. package/studio/client/dist/assets/index-BckHuAWk.js +0 -81
  172. package/studio/client/dist/assets/index-C4aNQ6dz.css +0 -1
  173. package/studio/server/introspect/anatomy/views/legacy.ts +0 -232
@@ -1,355 +1 @@
1
- import chalk from 'chalk'
2
-
3
- import type { OperationRecovery } from './command'
4
-
5
- import { AstraleError } from '../errors'
6
- import { readLocalStatus, type LocalStatus } from '../lib/local-status'
7
- import { log } from '../lib/log'
8
- import {
9
- functionInputIssues,
10
- queryInputRepair,
11
- reasonCode,
12
- schemaUpgradeDetails,
13
- schemaUpgradeHint,
14
- type FunctionInputIssue,
15
- type QueryInputRepair,
16
- } from './reasons'
17
-
18
- export { functionInputIssues, schemaUpgradeHint } from './reasons'
19
-
20
- /**
21
- * Format and display a kernel client error.
22
- *
23
- * Handles CLI-local errors plus the current Kernel Client public error families.
24
- *
25
- * When `debug` is true, additional diagnostic information (class name, full
26
- * error chain, attached url/details) is printed after the user-facing line.
27
- */
28
- export async function formatKernelError(
29
- error: unknown,
30
- isRaw: boolean,
31
- urlArg = '',
32
- debug = false,
33
- opts: { recovery?: OperationRecovery } = {},
34
- ): Promise<void> {
35
- const url =
36
- urlArg || (error instanceof Error ? ((error as Error & { url?: string }).url ?? '') : '')
37
- const localContext = await contextForError(error)
38
- // Handle AstraleError (AuthError, etc.) with structured hints
39
- if (error instanceof AstraleError) {
40
- if (isRaw) {
41
- writeRaw({ error: error.code, message: error.message, hint: error.hint })
42
- } else {
43
- log.error(`${chalk.bold(error.code)}: ${error.message}`)
44
- if (error.hint) log.dim(` ${error.hint}`)
45
- }
46
- if (debug) printDebug(error, url)
47
- return
48
- }
49
-
50
- if (!(error instanceof Error)) {
51
- if (isRaw) writeRaw({ error: 'UNKNOWN', message: String(error) })
52
- else log.error(String(error))
53
- return
54
- }
55
-
56
- const name = error.name
57
-
58
- switch (name) {
59
- case 'TransportError':
60
- presentTransportError(error, isRaw, url, localContext, opts.recovery)
61
- break
62
-
63
- case 'ResponseError': {
64
- const code = (error as { readonly code?: unknown }).code
65
- const reason = (error as { readonly reason?: unknown }).reason
66
- const codeOfReason = reasonCode(reason)
67
- const inputIssues = functionInputIssues(reason)
68
- const queryRepair = queryInputRepair(reason)
69
- const upgrade = schemaUpgradeDetails(reason)
70
- const removalHint = schemaDataRemovalHint(reason)
71
- const domainAddressNotPublic = codeOfReason === 'SCHEMA_DOMAIN_ADDRESS_NOT_PUBLIC'
72
- const displayMessage = domainAddressNotPublic
73
- ? 'Expose the Domain through a public HTTPS URL or public tunnel, then retry.'
74
- : removalHint !== undefined && !isRaw
75
- ? 'Existing business data still uses schema definitions being removed.'
76
- : error.message
77
- const hint =
78
- codeOfReason === 'FUNCTION_INPUT_INVALID' && (isRaw || inputIssues.length === 0)
79
- ? 'Use `astrale introspect <path>` to see the callable input.'
80
- : (removalHint ?? (upgrade === undefined ? undefined : schemaUpgradeHint(upgrade)))
81
- if (isRaw) {
82
- writeRaw({
83
- error: 'RESPONSE_ERROR',
84
- ...(code === undefined ? {} : { code }),
85
- message: displayMessage,
86
- ...(reason === undefined ? {} : { reason }),
87
- ...(hint === undefined ? {} : { hint }),
88
- })
89
- } else {
90
- log.error(
91
- domainAddressNotPublic
92
- ? `${chalk.bold('SCHEMA_DOMAIN_ADDRESS_NOT_PUBLIC')}: ${displayMessage}`
93
- : removalHint !== undefined
94
- ? `${chalk.bold('DATA_MIGRATION_REQUIRED')}: ${displayMessage}`
95
- : `${chalk.bold(code === undefined ? 'RESPONSE_ERROR' : `RESPONSE_ERROR(${String(code)})`)}: ${displayMessage}`,
96
- )
97
- if (codeOfReason !== undefined && !domainAddressNotPublic && removalHint === undefined) {
98
- log.dim(` reason: ${codeOfReason}`)
99
- }
100
- presentFunctionInputIssues(inputIssues)
101
- if (queryRepair !== undefined) presentQueryInputRepair(queryRepair)
102
- if (upgrade?.issue === 'issuer-changed') {
103
- log.dim(` installed issuer: ${upgrade.installedIssuer}`)
104
- log.dim(` replacement issuer: ${upgrade.replacementIssuer}`)
105
- }
106
- if (hint !== undefined) log.dim(` ${hint}`)
107
- }
108
- break
109
- }
110
-
111
- default: {
112
- const mapped = mapPublicError(error)
113
- if (isRaw) {
114
- writeRaw({
115
- error: mapped.code,
116
- message: mapped.message,
117
- ...(mapped.hint === undefined ? {} : { hint: mapped.hint }),
118
- ...(mapped.timeoutMs === undefined ? {} : { timeoutMs: mapped.timeoutMs }),
119
- })
120
- } else {
121
- log.error(`${chalk.bold(mapped.code)}: ${mapped.message}`)
122
- if (mapped.hint) log.dim(` ${mapped.hint}`)
123
- }
124
- }
125
- }
126
-
127
- if (debug) printDebug(error, url)
128
- }
129
-
130
- function schemaDataRemovalHint(reason: unknown): string | undefined {
131
- if (reason === null || typeof reason !== 'object') return undefined
132
- const value = reason as { readonly code?: unknown; readonly details?: unknown }
133
- if (value.code !== 'DATA_MIGRATION_REQUIRED') return undefined
134
- if (value.details === null || typeof value.details !== 'object') return undefined
135
-
136
- const requirements = (value.details as { readonly requirements?: unknown }).requirements
137
- if (
138
- !Array.isArray(requirements) ||
139
- requirements.length === 0 ||
140
- !requirements.every(
141
- (requirement) =>
142
- requirement !== null &&
143
- typeof requirement === 'object' &&
144
- (requirement as { readonly operation?: unknown }).operation === 'remove-facts' &&
145
- (requirement as { readonly reason?: unknown }).reason === 'destructive-change',
146
- )
147
- ) {
148
- return undefined
149
- }
150
-
151
- return 'Delete this data explicitly, then retry. No data was deleted.'
152
- }
153
-
154
- function mapPublicError(error: Error): {
155
- code: string
156
- message: string
157
- hint?: string
158
- timeoutMs?: number
159
- } {
160
- const name = error.name
161
- if (name === 'PathError') {
162
- return { code: 'PATH_INVALID', message: error.message }
163
- }
164
- if (name === 'NodeUnavailableError') {
165
- return {
166
- code: 'NODE_UNAVAILABLE',
167
- message: error.message,
168
- hint: 'If this is a callable Path, use `astrale call` or `astrale introspect`.',
169
- }
170
- }
171
- if (name === 'AuthValueError') {
172
- return { code: 'AUTH_VALUE_INVALID', message: error.message }
173
- }
174
- if (name === 'ClientError' && /timed out/i.test(error.message)) {
175
- const timeoutMs = (error as { timeoutMs?: number }).timeoutMs
176
- return {
177
- code: 'TIMEOUT',
178
- message: error.message,
179
- hint: 'Try increasing with --timeout',
180
- ...(timeoutMs === undefined ? {} : { timeoutMs }),
181
- }
182
- }
183
- if (name === 'ClientError' && /Publication discovery returned HTTP/i.test(error.message)) {
184
- return {
185
- code: 'KERNEL_DISCOVERY_FAILED',
186
- message: error.message,
187
- hint: 'Pass the Kernel issuer URL (no /invoke suffix), e.g. https://host/kernel/host',
188
- }
189
- }
190
- return { code: name && name !== 'Error' ? name : 'UNKNOWN', message: error.message }
191
- }
192
-
193
- function presentTransportError(
194
- error: Error,
195
- isRaw: boolean,
196
- url: string,
197
- context: LocalStatus | undefined,
198
- recovery: OperationRecovery | undefined,
199
- ): void {
200
- const phase = transportPhase(error)
201
- const delivery = transportDelivery(error)
202
- const code =
203
- phase === 'connect'
204
- ? 'CONNECTION_ERROR'
205
- : phase === 'timeout'
206
- ? 'TIMEOUT'
207
- : phase === 'closed'
208
- ? 'DISCONNECTED'
209
- : 'TRANSPORT_ERROR'
210
- if (isRaw) {
211
- writeRaw({
212
- error: code,
213
- message: error.message,
214
- ...(url === '' ? {} : { url }),
215
- ...(phase === undefined ? {} : { phase }),
216
- ...(delivery === undefined ? {} : { delivery }),
217
- ...(context === undefined ? {} : { context }),
218
- ...(delivery === 'unknown' && recovery !== undefined ? recovery : {}),
219
- })
220
- return
221
- }
222
- log.error(`${chalk.bold(code)}: ${error.message}`)
223
- if (url !== '') log.dim(` target: ${url}`)
224
- if (phase !== undefined) log.dim(` phase: ${phase}`)
225
- if (phase === 'connect') log.dim(' Check the target and run `astrale status`.')
226
- else if (phase === 'timeout') log.dim(' Try increasing `--timeout`.')
227
- else if (delivery === 'unknown') printOperationRecovery(recovery)
228
- printLocalContext(context)
229
- }
230
-
231
- function printOperationRecovery(recovery: OperationRecovery | undefined): void {
232
- if (recovery === undefined) {
233
- log.dim(' Delivery is unknown; do not automatically retry a mutating call.')
234
- return
235
- }
236
- log.dim(' Delivery is unknown; retry with the same operation id:')
237
- log.dim(` operation: ${recovery.operation}`)
238
- log.dim(` ${recovery.retry}`)
239
- }
240
-
241
- function presentFunctionInputIssues(issues: readonly FunctionInputIssue[]): void {
242
- for (const issue of issues) {
243
- const location = issue.path === undefined || issue.path === '' ? '<input>' : issue.path
244
- console.log(chalk.red(` ${location}: ${issue.message} (${chalk.dim(issue.code)})`))
245
- }
246
- }
247
-
248
- function presentQueryInputRepair(repair: QueryInputRepair): void {
249
- if (repair.phase === 'plan') {
250
- log.dim(` ${repair.path ?? '/'} ${repair.issue}`)
251
- return
252
- }
253
- if (repair.phase === 'limit') {
254
- log.dim(` ${repair.path ?? '/'} ${repair.limit} limit ${repair.actual}/${repair.maximum}`)
255
- return
256
- }
257
- log.dim(` ${repair.path} ${repair.phase} input`)
258
- }
259
-
260
- function transportPhase(error: Error): string | undefined {
261
- const phase = (error as Error & { readonly phase?: unknown }).phase
262
- return phase === 'connect' ||
263
- phase === 'send' ||
264
- phase === 'receive' ||
265
- phase === 'timeout' ||
266
- phase === 'closed'
267
- ? phase
268
- : undefined
269
- }
270
-
271
- function transportDelivery(error: Error): string | undefined {
272
- const delivery = (error as Error & { readonly delivery?: unknown }).delivery
273
- return delivery === 'not-sent' || delivery === 'unknown' ? delivery : undefined
274
- }
275
-
276
- function writeRaw(payload: Record<string, unknown>): void {
277
- process.stderr.write(JSON.stringify(payload) + '\n')
278
- }
279
-
280
- async function contextForError(error: unknown): Promise<LocalStatus | undefined> {
281
- if (!(error instanceof Error)) return undefined
282
- if (error.name !== 'TransportError') return undefined
283
- return readLocalStatus().catch(() => undefined)
284
- }
285
-
286
- function printLocalContext(context: LocalStatus | undefined): void {
287
- if (!context) return
288
- process.stderr.write(chalk.dim('\nContext:\n'))
289
- if ('error' in context.admin) {
290
- process.stderr.write(chalk.dim(` admin: invalid (${context.admin.error})\n`))
291
- } else {
292
- process.stderr.write(chalk.dim(` admin: ${context.admin.name} -> ${context.admin.url}\n`))
293
- }
294
- if (context.instance) {
295
- process.stderr.write(
296
- chalk.dim(` instance: ${context.instance.active} -> ${context.instance.url}\n`),
297
- )
298
- } else {
299
- process.stderr.write(chalk.dim(' instance: none\n'))
300
- }
301
- if (context.identity) {
302
- const source =
303
- context.identity.source === 'idp'
304
- ? `idp:${context.identity.idp ?? 'unknown'}`
305
- : context.identity.source
306
- process.stderr.write(chalk.dim(` identity: ${context.identity.name} [${source}]\n`))
307
- if (context.identity.session?.cached) {
308
- const state = context.identity.session.requiresLogin ? 'login required' : 'ready'
309
- process.stderr.write(chalk.dim(` session: ${state}\n`))
310
- } else if (context.identity.source === 'idp') {
311
- process.stderr.write(chalk.dim(' session: not cached\n'))
312
- }
313
- } else {
314
- process.stderr.write(chalk.dim(' identity: none\n'))
315
- }
316
- }
317
-
318
- function printDebug(error: unknown, url: string): void {
319
- process.stderr.write('\n' + chalk.dim('── debug ─────────────────') + '\n')
320
- if (url) process.stderr.write(chalk.dim(`url: ${url}`) + '\n')
321
- if (error instanceof Error) {
322
- process.stderr.write(chalk.dim(`class: ${error.constructor.name}`) + '\n')
323
- if (error.stack) process.stderr.write(chalk.dim(error.stack) + '\n')
324
- // Walk cause chain
325
- let cause = (error as Error & { cause?: unknown }).cause
326
- while (cause instanceof Error) {
327
- process.stderr.write(
328
- chalk.dim(`caused by: ${cause.constructor.name}: ${cause.message}`) + '\n',
329
- )
330
- if (cause.stack) process.stderr.write(chalk.dim(cause.stack) + '\n')
331
- cause = (cause as Error & { cause?: unknown }).cause
332
- }
333
- // Any attached fields (code, details, etc.)
334
- const extras: Record<string, unknown> = {}
335
- const bag = error as unknown as Record<string, unknown>
336
- for (const key of [
337
- 'code',
338
- 'type',
339
- 'reason',
340
- 'details',
341
- 'errors',
342
- 'data',
343
- 'timeoutMs',
344
- 'requestId',
345
- ]) {
346
- const v = bag[key]
347
- if (v !== undefined) extras[key] = v
348
- }
349
- if (Object.keys(extras).length > 0) {
350
- process.stderr.write(chalk.dim(`extras: ${JSON.stringify(extras, null, 2)}`) + '\n')
351
- }
352
- } else {
353
- process.stderr.write(chalk.dim(String(error)) + '\n')
354
- }
355
- }
1
+ export { formatKernelError, functionInputIssues, schemaUpgradeHint } from './failure'
@@ -1,9 +1,9 @@
1
- import type { Fetch } from '@astrale-os/kernel-client'
2
1
  import type { IssuerId } from '@astrale-os/sdk/auth'
2
+ import type { Fetch } from '@astrale-os/sdk/client'
3
3
 
4
- import { call, Client } from '@astrale-os/kernel-client'
5
- import { createAuth } from '@astrale-os/kernel-client/auth'
4
+ import { createAuth } from '@astrale-os/sdk/auth'
6
5
  import { credential, exchange as exchangeProtocol } from '@astrale-os/sdk/auth'
6
+ import { call, Client } from '@astrale-os/sdk/client'
7
7
 
8
8
  import type { SourceCredentialResolver } from './credential'
9
9
  import type { ConnectionTarget } from './target'
@@ -111,15 +111,20 @@ async function exchange(
111
111
  exchangeProtocol.paths(domainIssuer).configuration,
112
112
  domainIssuer,
113
113
  ).toString()
114
- const configurationResponse = await fetch(configurationUrl, {
115
- method: 'GET',
116
- redirect: 'error',
117
- credentials: 'omit',
118
- cache: 'no-store',
119
- referrerPolicy: 'no-referrer',
120
- headers: { accept: 'application/json' },
121
- signal,
122
- })
114
+ const configurationResponse = await fetchExchange(
115
+ fetch,
116
+ configurationUrl,
117
+ {
118
+ method: 'GET',
119
+ redirect: 'error',
120
+ credentials: 'omit',
121
+ cache: 'no-store',
122
+ referrerPolicy: 'no-referrer',
123
+ headers: { accept: 'application/json' },
124
+ signal,
125
+ },
126
+ 'Domain issuer discovery could not be reached.',
127
+ )
123
128
  if (!configurationResponse.ok) {
124
129
  throw new AstraleError(
125
130
  'TOKEN_EXCHANGE_DISCOVERY_FAILED',
@@ -139,18 +144,23 @@ async function exchange(
139
144
  )
140
145
  }
141
146
 
142
- const response = await fetch(endpoint, {
143
- method: 'POST',
144
- redirect: 'error',
145
- credentials: 'omit',
146
- cache: 'no-store',
147
- referrerPolicy: 'no-referrer',
148
- headers: {
149
- authorization: `Bearer ${envelope}`,
150
- accept: exchangeProtocol.MEDIA_TYPE,
147
+ const response = await fetchExchange(
148
+ fetch,
149
+ endpoint,
150
+ {
151
+ method: 'POST',
152
+ redirect: 'error',
153
+ credentials: 'omit',
154
+ cache: 'no-store',
155
+ referrerPolicy: 'no-referrer',
156
+ headers: {
157
+ authorization: `Bearer ${envelope}`,
158
+ accept: exchangeProtocol.MEDIA_TYPE,
159
+ },
160
+ signal,
151
161
  },
152
- signal,
153
- })
162
+ 'Domain token exchange could not be reached.',
163
+ )
154
164
  const body = await boundedJson(response, signal)
155
165
  if (!response.ok) {
156
166
  let admitted: exchangeProtocol.ErrorResponse
@@ -193,6 +203,20 @@ async function exchange(
193
203
  return Object.freeze({ credential: exchanged.token, expiresAt: exchanged.expiresAt })
194
204
  }
195
205
 
206
+ async function fetchExchange(
207
+ fetch: Fetch,
208
+ input: string,
209
+ init: RequestInit,
210
+ message: string,
211
+ ): Promise<Response> {
212
+ try {
213
+ return await fetch(input, init)
214
+ } catch (cause) {
215
+ if (init.signal?.aborted) throw cause
216
+ throw new AstraleError('TOKEN_EXCHANGE_UNAVAILABLE', message, undefined, { cause })
217
+ }
218
+ }
219
+
196
220
  function requireExchangeTransport(
197
221
  target: ConnectionTarget & { readonly domainIssuer: IssuerId },
198
222
  ): void {
@@ -0,0 +1,110 @@
1
+ import { AuthValueError } from '@astrale-os/sdk/auth'
2
+ import { ClientError, ProtocolError, ResponseError, TransportError } from '@astrale-os/sdk/client'
3
+ import { NodeUnavailableError } from '@astrale-os/sdk/client'
4
+ import { PathError } from '@astrale-os/sdk/graph/path'
5
+
6
+ import type { FailureDiagnostic, TransportDiagnosticContext } from './model'
7
+
8
+ import { AstraleError } from '../../errors'
9
+
10
+ export function classifyFailure(error: unknown): FailureDiagnostic {
11
+ if (error instanceof AstraleError) return simple(error.code, error.message, error.hint)
12
+ if (error instanceof TransportError) {
13
+ const context = transportContext(error)
14
+ if (context === undefined) return simple('TRANSPORT_ERROR', error.message)
15
+ return {
16
+ kind: 'transport',
17
+ code: lifecycleCode(error.phase),
18
+ message: error.message,
19
+ phase: error.phase,
20
+ context,
21
+ }
22
+ }
23
+ if (error instanceof ResponseError) {
24
+ return {
25
+ kind: 'response',
26
+ code: error.code,
27
+ message: error.message,
28
+ ...(error.reason === undefined ? {} : { reason: error.reason }),
29
+ }
30
+ }
31
+ if (error instanceof ClientError) {
32
+ const failure = sessionFailure(error)
33
+ if (failure !== undefined) return simple(lifecycleCode(failure), error.message)
34
+ }
35
+ if (error instanceof ProtocolError) return simple('PROTOCOL_ERROR', error.message)
36
+ if (error instanceof NodeUnavailableError)
37
+ return simple(
38
+ 'NODE_UNAVAILABLE',
39
+ error.message,
40
+ 'If this is a callable Path, use `astrale call` or `astrale introspect`.',
41
+ )
42
+ if (error instanceof PathError) return simple('PATH_INVALID', error.message)
43
+ if (error instanceof AuthValueError) return simple('AUTH_VALUE_INVALID', error.message)
44
+ if (error instanceof ClientError) return simple('CLIENT_ERROR', error.message)
45
+ return simple('UNEXPECTED_ERROR', 'The CLI encountered an unexpected internal failure.')
46
+ }
47
+
48
+ type TransportCode = Extract<FailureDiagnostic, { kind: 'transport' }>['code']
49
+ type SessionFailure = 'cancelled' | 'closed' | 'timeout'
50
+
51
+ function lifecycleCode(value: TransportError['phase']): TransportCode
52
+ function lifecycleCode(value: SessionFailure): string
53
+ function lifecycleCode(value: TransportError['phase'] | SessionFailure): string {
54
+ if (value === 'connect') return 'CONNECTION_ERROR'
55
+ if (value === 'timeout') return 'TIMEOUT'
56
+ if (value === 'closed') return 'DISCONNECTED'
57
+ return value === 'cancelled' ? 'CANCELLED' : 'TRANSPORT_ERROR'
58
+ }
59
+
60
+ function transportContext(error: TransportError): TransportDiagnosticContext | undefined {
61
+ const evidence = error as TransportError & {
62
+ readonly context?: unknown
63
+ readonly delivery?: unknown
64
+ readonly invocation?: unknown
65
+ }
66
+ if (record(evidence.context)) {
67
+ if (
68
+ evidence.context.kind === 'acquisition' &&
69
+ (evidence.context.resource === 'publication' || evidence.context.resource === 'bundle')
70
+ ) {
71
+ return { kind: 'acquisition', resource: evidence.context.resource }
72
+ }
73
+ if (
74
+ evidence.context.kind === 'invocation' &&
75
+ (evidence.context.delivery === 'not-sent' || evidence.context.delivery === 'unknown')
76
+ ) {
77
+ return {
78
+ kind: 'invocation',
79
+ delivery: evidence.context.delivery,
80
+ ...(evidence.context.invocation === undefined
81
+ ? {}
82
+ : { invocation: evidence.context.invocation }),
83
+ }
84
+ }
85
+ }
86
+ if (evidence.delivery !== 'not-sent' && evidence.delivery !== 'unknown') return undefined
87
+ return {
88
+ kind: 'invocation',
89
+ delivery: evidence.delivery,
90
+ ...(evidence.invocation === undefined ? {} : { invocation: evidence.invocation }),
91
+ }
92
+ }
93
+
94
+ function sessionFailure(error: ClientError): SessionFailure | undefined {
95
+ const failure = (error as ClientError & { readonly failure?: unknown }).failure
96
+ return failure === 'cancelled' || failure === 'closed' || failure === 'timeout'
97
+ ? failure
98
+ : undefined
99
+ }
100
+
101
+ function record(input: unknown): input is Readonly<Record<string, unknown>> {
102
+ return input !== null && typeof input === 'object' && !Array.isArray(input)
103
+ }
104
+
105
+ const simple = (code: string, message: string, hint?: string): FailureDiagnostic => ({
106
+ kind: 'simple',
107
+ code,
108
+ message,
109
+ ...(hint === undefined ? {} : { hint }),
110
+ })
@@ -0,0 +1,26 @@
1
+ import chalk from 'chalk'
2
+
3
+ const MAXIMUM_DEBUG_FAILURES = 32
4
+
5
+ export function printFailureDebug(error: unknown, url: string): void {
6
+ process.stderr.write(`\n${chalk.dim('── debug ─────────────────')}\n`)
7
+ if (url) process.stderr.write(`${chalk.dim(`url: ${url}`)}\n`)
8
+ const seen = new Set<unknown>()
9
+ const pending: Array<{ value: unknown; relation: string }> = [{ value: error, relation: 'error' }]
10
+ while (pending.length > 0 && seen.size < MAXIMUM_DEBUG_FAILURES) {
11
+ const { value, relation } = pending.shift()!
12
+ if (seen.has(value)) continue
13
+ seen.add(value)
14
+ if (!(value instanceof Error)) {
15
+ process.stderr.write(`${chalk.dim(`${relation}: ${String(value)}`)}\n`)
16
+ continue
17
+ }
18
+ process.stderr.write(
19
+ `${chalk.dim(`${relation}: ${value.constructor.name}: ${value.message}`)}\n`,
20
+ )
21
+ if (value.stack) process.stderr.write(`${chalk.dim(value.stack)}\n`)
22
+ if (value.cause !== undefined) pending.push({ value: value.cause, relation: 'caused by' })
23
+ if (value instanceof AggregateError)
24
+ for (const child of value.errors) pending.push({ value: child, relation: 'aggregate' })
25
+ }
26
+ }
@@ -0,0 +1,18 @@
1
+ import type { OperationRecovery } from '../command'
2
+
3
+ import { classifyFailure } from './classify'
4
+ import { printFailureDebug } from './debug'
5
+ import { renderFailure } from './render'
6
+
7
+ export { functionInputIssues, schemaUpgradeHint } from '../reasons'
8
+
9
+ export async function formatKernelError(
10
+ error: unknown,
11
+ machine: boolean,
12
+ urlArg = '',
13
+ debug = false,
14
+ options: { recovery?: OperationRecovery } = {},
15
+ ): Promise<void> {
16
+ renderFailure(classifyFailure(error), machine, urlArg, options.recovery)
17
+ if (debug) printFailureDebug(error, urlArg)
18
+ }
@@ -0,0 +1,28 @@
1
+ export type TransportDiagnosticContext =
2
+ | { readonly kind: 'acquisition'; readonly resource: 'publication' | 'bundle' }
3
+ | {
4
+ readonly kind: 'invocation'
5
+ readonly delivery: 'not-sent' | 'unknown'
6
+ readonly invocation?: unknown
7
+ }
8
+
9
+ export type FailureDiagnostic =
10
+ | {
11
+ readonly kind: 'simple'
12
+ readonly code: string
13
+ readonly message: string
14
+ readonly hint?: string
15
+ }
16
+ | {
17
+ readonly kind: 'response'
18
+ readonly code: number
19
+ readonly message: string
20
+ readonly reason?: unknown
21
+ }
22
+ | {
23
+ readonly kind: 'transport'
24
+ readonly code: 'CONNECTION_ERROR' | 'DISCONNECTED' | 'TIMEOUT' | 'TRANSPORT_ERROR'
25
+ readonly message: string
26
+ readonly phase: string
27
+ readonly context: TransportDiagnosticContext
28
+ }