@astrale-os/cli 1.0.0-beta.2 → 1.0.0-beta.20

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 (282) hide show
  1. package/README.md +16 -1
  2. package/dist/astrale.js +51105 -58057
  3. package/dist/public/connect-core.js +2085 -30702
  4. package/dist/public/keys/index.js +884 -29476
  5. package/dist/public/paths/index.js +13101 -41452
  6. package/dist/types/admin/contract.d.ts +26 -0
  7. package/dist/types/admin/instance/client.d.ts +20 -0
  8. package/dist/types/admin/instance/index.d.ts +2 -0
  9. package/dist/types/admin/instance/model.d.ts +6 -10
  10. package/dist/types/connection/auth.d.ts +3 -0
  11. package/dist/types/connection/call.d.ts +3 -0
  12. package/dist/types/connection/command.d.ts +27 -0
  13. package/dist/types/connection/credential.d.ts +14 -0
  14. package/dist/types/connection/errors.d.ts +1 -0
  15. package/dist/types/connection/exchange.d.ts +9 -0
  16. package/dist/types/connection/failure/classify.d.ts +2 -0
  17. package/dist/types/connection/failure/index.d.ts +5 -0
  18. package/dist/types/connection/failure/model.d.ts +25 -0
  19. package/dist/types/connection/failure/render.d.ts +3 -0
  20. package/dist/types/connection/index.d.ts +8 -0
  21. package/dist/types/connection/reasons.d.ts +37 -0
  22. package/dist/types/connection/self.d.ts +37 -0
  23. package/dist/types/connection/session.d.ts +31 -0
  24. package/dist/types/connection/target.d.ts +38 -0
  25. package/dist/types/errors.d.ts +1 -1
  26. package/dist/types/lib/admin-instance.d.ts +17 -0
  27. package/dist/types/lib/admin-target.d.ts +11 -36
  28. package/dist/types/lib/config.d.ts +7 -59
  29. package/dist/types/lib/failure-debug.d.ts +1 -0
  30. package/dist/types/lib/idp.d.ts +25 -173
  31. package/dist/types/lib/instance-target.d.ts +1 -1
  32. package/dist/types/lib/instance.d.ts +30 -88
  33. package/dist/types/lib/log.d.ts +5 -3
  34. package/dist/types/lib/output.d.ts +1 -1
  35. package/dist/types/lib/proc.d.ts +43 -0
  36. package/dist/types/lib/self.d.ts +9 -0
  37. package/dist/types/lib/update.d.ts +109 -0
  38. package/dist/types/lib/validation.d.ts +4 -1
  39. package/package.json +15 -20
  40. package/src/__tests__/fixtures/publication.ts +20 -0
  41. package/src/admin/.spec/architecture.md +12 -5
  42. package/src/admin/__tests__/fixture.ts +25 -0
  43. package/src/admin/catalog/.spec/api.d.ts +1 -3
  44. package/src/admin/catalog/.spec/architecture.md +5 -4
  45. package/src/admin/catalog/__tests__/client.test.ts +143 -69
  46. package/src/admin/catalog/client.ts +40 -54
  47. package/src/admin/catalog/model.ts +3 -4
  48. package/src/admin/contract.ts +46 -0
  49. package/src/admin/graph/.spec/api.d.ts +4 -10
  50. package/src/admin/graph/nodes.ts +1 -1
  51. package/src/admin/instance/.spec/api.d.ts +5 -9
  52. package/src/admin/instance/.spec/architecture.md +7 -7
  53. package/src/admin/instance/__tests__/client.test.ts +157 -175
  54. package/src/admin/instance/client.ts +39 -128
  55. package/src/admin/instance/index.ts +1 -2
  56. package/src/admin/instance/model.ts +12 -18
  57. package/src/commands/__tests__/admin-instance.test.ts +11 -8
  58. package/src/commands/__tests__/call-describe.test.ts +46 -33
  59. package/src/commands/__tests__/call.test.ts +34 -0
  60. package/src/commands/__tests__/domain-install-operation.test.ts +7 -22
  61. package/src/commands/__tests__/domain-install-owned.test.ts +1 -1
  62. package/src/commands/__tests__/domain-list.test.ts +5 -25
  63. package/src/commands/__tests__/install-direct.test.ts +115 -8
  64. package/src/commands/__tests__/install-identity-override.test.ts +3 -14
  65. package/src/commands/__tests__/instance-list-rows.test.ts +1 -1
  66. package/src/commands/__tests__/instance-status.test.ts +139 -0
  67. package/src/commands/__tests__/introspect-parse.test.ts +80 -1
  68. package/src/commands/__tests__/logs.test.ts +250 -2
  69. package/src/commands/__tests__/read-commands.test.ts +62 -10
  70. package/src/commands/__tests__/token-ttl.test.ts +49 -4
  71. package/src/commands/__tests__/update.test.ts +25 -3
  72. package/src/commands/__tests__/view.test.ts +49 -0
  73. package/src/commands/call-describe.ts +60 -68
  74. package/src/commands/call.ts +71 -59
  75. package/src/commands/domain/install.ts +54 -35
  76. package/src/commands/domain/list.ts +1 -1
  77. package/src/commands/domain/publish.ts +14 -11
  78. package/src/commands/domain/uninstall.ts +2 -2
  79. package/src/commands/get.ts +14 -15
  80. package/src/commands/identity/__tests__/register.test.ts +3 -3
  81. package/src/commands/identity/register.ts +5 -4
  82. package/src/commands/instance/create.ts +8 -16
  83. package/src/commands/instance/delete.ts +12 -9
  84. package/src/commands/instance/list.ts +2 -2
  85. package/src/commands/instance/status.ts +66 -20
  86. package/src/commands/instance/use.ts +16 -12
  87. package/src/commands/introspect.ts +48 -35
  88. package/src/commands/logs.ts +123 -33
  89. package/src/commands/mutate.ts +6 -5
  90. package/src/commands/query.ts +35 -32
  91. package/src/commands/token.ts +42 -27
  92. package/src/commands/ui/__tests__/commands.test.ts +152 -0
  93. package/src/commands/ui/add.ts +51 -0
  94. package/src/commands/ui/doctor.ts +13 -0
  95. package/src/commands/ui/init.ts +38 -0
  96. package/src/commands/ui/list.ts +26 -0
  97. package/src/commands/ui/preset-apply.ts +19 -0
  98. package/src/commands/ui/preset-list.ts +12 -0
  99. package/src/commands/ui/shared.ts +25 -0
  100. package/src/commands/update.ts +31 -11
  101. package/src/commands/view.ts +82 -37
  102. package/src/connection/.spec/api.d.ts +7 -8
  103. package/src/connection/.spec/architecture.md +5 -4
  104. package/src/connection/.spec/flows/session.ts +2 -2
  105. package/src/connection/.spec/laws/connection.ts +6 -2
  106. package/src/connection/.spec/layout.ts +1 -0
  107. package/src/connection/__tests__/credential.test.ts +20 -1
  108. package/src/connection/__tests__/errors.test.ts +27 -20
  109. package/src/connection/__tests__/exchange.test.ts +30 -1
  110. package/src/connection/__tests__/failure-fixtures.ts +39 -0
  111. package/src/connection/__tests__/failure-safety.test.ts +75 -0
  112. package/src/connection/__tests__/self.test.ts +31 -11
  113. package/src/connection/__tests__/session.test.ts +3 -3
  114. package/src/connection/__tests__/target.test.ts +1 -0
  115. package/src/connection/auth.ts +13 -0
  116. package/src/connection/call.ts +1 -1
  117. package/src/connection/credential.ts +2 -2
  118. package/src/connection/errors.ts +1 -355
  119. package/src/connection/exchange.ts +47 -23
  120. package/src/connection/failure/classify.ts +110 -0
  121. package/src/connection/failure/index.ts +18 -0
  122. package/src/connection/failure/model.ts +28 -0
  123. package/src/connection/failure/render.ts +116 -0
  124. package/src/connection/reasons.ts +16 -0
  125. package/src/connection/self.ts +39 -21
  126. package/src/connection/session.ts +19 -8
  127. package/src/errors.ts +3 -2
  128. package/src/graph/.spec/api.d.ts +12 -5
  129. package/src/graph/.spec/layout.ts +9 -1
  130. package/src/graph/__tests__/mutation.test.ts +2 -2
  131. package/src/graph/__tests__/query.test.ts +25 -6
  132. package/src/graph/class.ts +30 -0
  133. package/src/graph/index.ts +1 -0
  134. package/src/graph/query.ts +21 -32
  135. package/src/identity/.spec/api.d.ts +2 -2
  136. package/src/identity/__tests__/registration.test.ts +1 -1
  137. package/src/identity/registration.ts +3 -3
  138. package/src/keys/__tests__/keys.test.ts +4 -0
  139. package/src/keys/credential.ts +1 -0
  140. package/src/lib/__tests__/admin-target.test.ts +12 -0
  141. package/src/lib/__tests__/binary.test.ts +16 -1
  142. package/src/lib/__tests__/command-dx.test.ts +1 -1
  143. package/src/lib/__tests__/domain-publication.test.ts +10 -16
  144. package/src/lib/__tests__/idp.test.ts +34 -4
  145. package/src/lib/__tests__/instance-candidates.test.ts +1 -3
  146. package/src/lib/__tests__/instance-target.test.ts +7 -39
  147. package/src/lib/__tests__/log-errors.test.ts +64 -0
  148. package/src/lib/__tests__/studio-server-deps.test.ts +1 -1
  149. package/src/lib/__tests__/update.test.ts +264 -11
  150. package/src/lib/__tests__/view-assets.test.ts +14 -0
  151. package/src/lib/__tests__/view-external-open-intent.test.ts +150 -0
  152. package/src/lib/__tests__/view-external-open-origins.test.ts +27 -0
  153. package/src/lib/__tests__/view-open-intent.test.ts +0 -2
  154. package/src/lib/__tests__/view-server.test.ts +26 -3
  155. package/src/lib/__tests__/view-session.test.ts +2 -1
  156. package/src/lib/admin-instance.ts +14 -8
  157. package/src/lib/admin-target.ts +21 -2
  158. package/src/lib/binary.ts +22 -5
  159. package/src/lib/command-dx.ts +9 -9
  160. package/src/lib/domain-publication.ts +9 -7
  161. package/src/lib/failure-debug.ts +26 -0
  162. package/src/lib/idp.ts +34 -3
  163. package/src/lib/instance-candidates.ts +2 -2
  164. package/src/lib/instance-target.ts +3 -14
  165. package/src/lib/instance.ts +16 -11
  166. package/src/lib/log.ts +16 -9
  167. package/src/lib/output.ts +1 -1
  168. package/src/lib/proc.ts +7 -2
  169. package/src/lib/provision-instance.ts +34 -57
  170. package/src/lib/update.ts +298 -45
  171. package/src/lib/validation.ts +2 -2
  172. package/src/lib/view/assets.ts +11 -2
  173. package/src/lib/view/external-open-intent.ts +41 -0
  174. package/src/lib/view/external-open-origins.ts +37 -0
  175. package/src/lib/view/host-capabilities.ts +17 -0
  176. package/src/lib/view/resolve.ts +1 -1
  177. package/src/lib/view/server.ts +18 -3
  178. package/src/lib/view/session.ts +2 -0
  179. package/src/program/.spec/api.d.ts +1 -0
  180. package/src/program/__tests__/program.test.ts +163 -6
  181. package/src/program/argv.ts +14 -0
  182. package/src/program/build.ts +26 -4
  183. package/src/program/command.ts +1 -0
  184. package/src/program/index.ts +1 -0
  185. package/src/program/registry.ts +2 -1
  186. package/src/setup/__tests__/instance-step.test.ts +27 -10
  187. package/src/setup/steps/instance.ts +22 -15
  188. package/src/telemetry/__tests__/recorder.test.ts +1 -0
  189. package/src/telemetry/recorder.ts +9 -1
  190. package/src/ui/.spec/api.d.ts +14 -0
  191. package/src/ui/.spec/architecture.md +10 -0
  192. package/src/ui/.spec/laws.ts +36 -0
  193. package/src/ui/.spec/layout.ts +16 -0
  194. package/src/ui/__tests__/ui.test.ts +981 -0
  195. package/src/ui/index.ts +13 -0
  196. package/src/ui/lock.ts +87 -0
  197. package/src/ui/model.ts +83 -0
  198. package/src/ui/operations.ts +710 -0
  199. package/src/ui/project.ts +219 -0
  200. package/src/ui/release.ts +321 -0
  201. package/src/ui/runner.ts +18 -0
  202. package/studio/client/dist/assets/elk-api-DjmKsHXc.js +1 -0
  203. package/studio/client/dist/assets/index-7YHdeCxp.js +81 -0
  204. package/studio/client/dist/assets/index-C0FAnwNw.css +2 -0
  205. package/studio/client/dist/assets/rolldown-runtime-Dd_uD5pT.js +1 -0
  206. package/studio/client/dist/assets/schema-studio-BilUNb6Z.css +1 -0
  207. package/studio/client/dist/assets/schema-studio-CABpTfvH.js +8 -0
  208. package/studio/client/dist/index.html +3 -2
  209. package/studio/package.json +7 -13
  210. package/studio/server/agent/ask.test.ts +1 -1
  211. package/studio/server/agent/bridge/grant.test.ts +1 -1
  212. package/studio/server/agent/bridge/routes.test.ts +1 -1
  213. package/studio/server/agent/harness/claude/adapter.test.ts +15 -5
  214. package/studio/server/agent/harness/gateway/token.test.ts +1 -1
  215. package/studio/server/agent/harness/gateway/token.ts +3 -3
  216. package/studio/server/agent/prompts/anchors.test.ts +32 -36
  217. package/studio/server/agent/prompts/anchors.ts +20 -49
  218. package/studio/server/agent/prompts/system.test.ts +3 -2
  219. package/studio/server/agent/prompts/system.ts +3 -3
  220. package/studio/server/agent/routes.test.ts +1 -1
  221. package/studio/server/agent/run/coordinator.test.ts +1 -1
  222. package/studio/server/api/canvas.ts +0 -1
  223. package/studio/server/api-agent-loadout.test.ts +1 -1
  224. package/studio/server/api.test.ts +1 -1
  225. package/studio/server/cache.test.ts +23 -12
  226. package/studio/server/cache.ts +12 -15
  227. package/studio/server/domain.test.ts +50 -68
  228. package/studio/server/domain.ts +82 -63
  229. package/studio/server/handoff/copy.ts +1 -1
  230. package/studio/server/index.ts +1 -1
  231. package/studio/server/instances/probe.test.ts +1 -1
  232. package/studio/server/introspect/anatomy/views/routes.ts +46 -37
  233. package/studio/server/introspect/anatomy/views.ts +9 -14
  234. package/studio/server/introspect/anatomy-extras.test.ts +26 -42
  235. package/studio/server/introspect/anatomy.test.ts +22 -14
  236. package/studio/server/introspect/anatomy.ts +6 -9
  237. package/studio/server/introspect/bundle.ts +0 -3
  238. package/studio/server/introspect/canonical-schema.test.ts +94 -389
  239. package/studio/server/introspect/canonical-schema.ts +227 -457
  240. package/studio/server/introspect/core-extractor.ts +18 -127
  241. package/studio/server/introspect/core.ts +1 -2
  242. package/studio/server/introspect/diff.test.ts +11 -7
  243. package/studio/server/introspect/diff.ts +18 -55
  244. package/studio/server/introspect/extractor.ts +22 -117
  245. package/studio/server/introspect/overlay-tsmorph.test.ts +119 -240
  246. package/studio/server/introspect/overlay.test.ts +26 -54
  247. package/studio/server/introspect/overlay.ts +10 -37
  248. package/studio/server/introspect/revision.test.ts +1 -1
  249. package/studio/server/introspect/revision.ts +1 -1
  250. package/studio/server/introspect/runtime.test.ts +65 -183
  251. package/studio/server/introspect/runtime.ts +8 -25
  252. package/studio/server/introspect/schema-ir-json.test.ts +70 -0
  253. package/studio/server/introspect/schema-ir-json.ts +38 -68
  254. package/studio/server/introspect/schema-refs.test.ts +43 -88
  255. package/studio/server/introspect/schema-refs.ts +14 -49
  256. package/studio/server/introspect/source-overlay/handlers.ts +116 -636
  257. package/studio/server/introspect/source-overlay/kernel-calls.ts +0 -3
  258. package/studio/server/introspect/source-overlay/spans.ts +15 -45
  259. package/studio/server/state/baseline.test.ts +14 -9
  260. package/studio/server/state/baseline.ts +3 -5
  261. package/studio/server/state/visibility.ts +1 -5
  262. package/studio/server/views/model.ts +3 -3
  263. package/studio/server/views/target.test.ts +17 -74
  264. package/studio/server/views/target.ts +24 -50
  265. package/studio/server/watch.test.ts +11 -11
  266. package/studio/server/watch.ts +8 -4
  267. package/studio/server/workspace-watch.ts +2 -2
  268. package/studio/shared/contracts/schema.ts +38 -107
  269. package/studio/shared/contracts/surface.test.ts +13 -15
  270. package/studio/shared/contracts/workspace.ts +2 -11
  271. package/studio/shared/schema/identity.test.ts +20 -44
  272. package/studio/shared/schema/identity.ts +9 -23
  273. package/viewer/dist/main.js +30 -51
  274. package/.check-workspace.cjs +0 -40
  275. package/src/admin/binding.ts +0 -168
  276. package/src/commands/__tests__/instance-create-hosts.test.ts +0 -29
  277. package/studio/client/dist/assets/elk-api-lwhFo7vB.js +0 -1
  278. package/studio/client/dist/assets/index-B-c-smo9.js +0 -8
  279. package/studio/client/dist/assets/index-BckHuAWk.js +0 -81
  280. package/studio/client/dist/assets/index-C4aNQ6dz.css +0 -1
  281. package/studio/client/dist/assets/index-ChOr3yP0.css +0 -1
  282. package/studio/server/introspect/anatomy/views/legacy.ts +0 -232
@@ -6,8 +6,9 @@ import type { KernelCommandOpts, SelfExpansionMeta } from '../connection'
6
6
  import type { CommandDefinition } from '../program/index'
7
7
 
8
8
  import { expandSelfInPath, runKernelCommand, withSelfHint } from '../connection'
9
+ import { AstraleError } from '../errors'
9
10
  import { prepareQuery, type QueryCommandInput } from '../graph/index'
10
- import { failClosed } from '../lib/log'
11
+ import { failInput } from '../lib/log'
11
12
  import { isMachine, output } from '../lib/output'
12
13
 
13
14
  type QueryOpts = KernelCommandOpts & {
@@ -22,48 +23,49 @@ type QueryOpts = KernelCommandOpts & {
22
23
 
23
24
  export async function queryCommand(sources: string[], opts: QueryOpts): Promise<void> {
24
25
  let input: QueryCommandInput
25
- let meta: SelfExpansionMeta | undefined
26
26
  try {
27
27
  const ast = await readAst(opts)
28
- if (ast === undefined) {
29
- const expanded = await Promise.all(sources.map((source) => expandSelfInPath(source, opts)))
30
- input = {
31
- sources: expanded.map(({ path }) => path),
32
- definition: opts.definition,
33
- edge: opts.edge,
34
- direction: opts.direction,
35
- limit: opts.limit,
36
- cursor: opts.cursor,
37
- }
38
- meta = expanded.find((entry) => entry.meta !== undefined)?.meta
39
- } else {
40
- input = {
41
- sources,
42
- ast,
43
- definition: opts.definition,
44
- edge: opts.edge,
45
- direction: opts.direction,
46
- limit: opts.limit,
47
- cursor: opts.cursor,
48
- }
28
+ input = {
29
+ sources,
30
+ ...(ast === undefined ? {} : { ast }),
31
+ definition: opts.definition,
32
+ edge: opts.edge,
33
+ direction: opts.direction,
34
+ limit: opts.limit,
35
+ cursor: opts.cursor,
49
36
  }
50
37
  } catch (error) {
51
- failClosed(error, opts)
38
+ failInput(error, opts)
52
39
  }
53
40
 
54
41
  let prepared
55
42
  try {
56
43
  prepared = prepareQuery(input)
57
44
  } catch (error) {
58
- failClosed(error, opts)
45
+ failInput(error, opts)
59
46
  }
60
47
 
61
48
  await runKernelCommand({
62
49
  opts,
63
50
  label: 'Query',
64
- fn: ({ graph }) => withSelfHint(() => graph.query(prepared.ast, { page: prepared.page }), meta),
51
+ fn: async (context) => {
52
+ const expanded = await Promise.all(sources.map((source) => expandSelfInPath(source, context)))
53
+ const meta: SelfExpansionMeta | undefined = expanded.find(
54
+ (entry) => entry.meta !== undefined,
55
+ )?.meta
56
+ const resolved =
57
+ meta === undefined
58
+ ? prepared
59
+ : prepareQuery({ ...input, sources: expanded.map(({ path }) => path) })
60
+ return withSelfHint(() => context.graph.query(resolved.ast, { page: resolved.page }), meta)
61
+ },
65
62
  format: (response, format) => {
66
- output(response.result, format)
63
+ output(
64
+ response.page.next === undefined
65
+ ? response.result
66
+ : { ...response.result, page: { next: response.page.next } },
67
+ format,
68
+ )
67
69
  if (response.page.next && !isMachine(format)) {
68
70
  process.stderr.write(` cursor: ${response.page.next}\n`)
69
71
  }
@@ -81,9 +83,9 @@ async function readAst(opts: QueryOpts): Promise<unknown | undefined> {
81
83
  try {
82
84
  raw = await readFile(opts.file, 'utf8')
83
85
  } catch (error) {
84
- throw new Error(
85
- `Cannot read --file ${opts.file}: ${error instanceof Error ? error.message : error}`,
86
- )
86
+ throw new AstraleError('FILE_READ_FAILED', `Cannot read --file ${opts.file}.`, undefined, {
87
+ cause: error,
88
+ })
87
89
  }
88
90
  return parseJson(raw, opts.file)
89
91
  }
@@ -105,7 +107,8 @@ Behavior:
105
107
  one exact Edge-Class expansion; --direction defaults to outgoing. --ast and
106
108
  --file accept a complete canonical astrale.graph.query/v6 document, including
107
109
  Property ordering and Node or Edge reference/value projections. --cursor resumes
108
- one caller-bound query scope.
110
+ one caller-bound query scope. Machine output adds page.next only when another
111
+ page exists; pass that opaque value back through --cursor.
109
112
 
110
113
  Legacy depth/children selector JSON and raw Cypher are not portable Kernel
111
114
  V2 query contracts and are not accepted.
@@ -122,7 +125,7 @@ Examples:
122
125
  { flags: '-f, --file <path>', description: 'Read a canonical Query V6 document from a file' },
123
126
  {
124
127
  flags: '--definition <path>',
125
- description: 'Select Nodes implementing one exact Class or Interface',
128
+ description: 'Select Nodes satisfying one exact Class',
126
129
  },
127
130
  { flags: '--edge <class>', description: 'Expand one exact Edge Class' },
128
131
  {
@@ -1,4 +1,4 @@
1
- import { issuer } from '@astrale-os/sdk/auth'
1
+ import { issuer, type AuthApi, type IssuerId, type MintedCredential } from '@astrale-os/sdk/auth'
2
2
 
3
3
  import type { KernelCommandOpts } from '../connection'
4
4
  import type { CommandDefinition } from '../program/index'
@@ -6,12 +6,12 @@ import type { CommandDefinition } from '../program/index'
6
6
  import { runKernelCommand } from '../connection'
7
7
  import { AstraleError } from '../errors'
8
8
  import { decodeJwtExpiration } from '../lib/local-status'
9
- import { failClosed, log } from '../lib/log'
9
+ import { failInput, log } from '../lib/log'
10
10
  import { output } from '../lib/output'
11
11
 
12
12
  /**
13
- * `astrale token` — mint a fresh delegation token for the active instance
14
- * + active identity through the bound AuthApi.
13
+ * `astrale token` — mint a fresh audience-bound token for the active instance
14
+ * and selected identity through the bound AuthApi.
15
15
  */
16
16
  export type TokenOpts = KernelCommandOpts & {
17
17
  audience?: string
@@ -21,23 +21,25 @@ export type TokenOpts = KernelCommandOpts & {
21
21
  for?: string
22
22
  }
23
23
 
24
+ const DEFAULT_TOKEN_TTL_SECONDS = 4 * 60
25
+
24
26
  export async function tokenCommand(opts: TokenOpts): Promise<void> {
25
27
  const commandOpts: TokenOpts = opts.for && !opts.as ? { ...opts, as: opts.for } : opts
26
- const ttl = parseTtl(commandOpts.ttl)
28
+ let ttl: number
29
+ try {
30
+ ttl = parseTtl(commandOpts.ttl)
31
+ } catch (error) {
32
+ failInput(error, opts)
33
+ }
27
34
  await runKernelCommand<string>({
28
35
  opts: commandOpts,
29
- label: 'Minting delegation token',
36
+ label: 'Minting token',
30
37
  fn: async (ctx) => {
31
38
  const audience =
32
39
  commandOpts.audience === undefined
33
40
  ? ctx.target.kernelIssuer
34
41
  : issuer.accept(commandOpts.audience)
35
- const self = await ctx.auth.whoami()
36
- return ctx.auth.delegate(self.id, {
37
- audience,
38
- ttlSeconds: ttl,
39
- attenuation: { kind: 'identity', self: true },
40
- })
42
+ return issueToken(ctx.auth, ctx.target.kernelIssuer, audience, ttl)
41
43
  },
42
44
  format: (token, fmtOpts) => {
43
45
  if (fmtOpts.json || fmtOpts.format !== undefined) {
@@ -45,15 +47,31 @@ export async function tokenCommand(opts: TokenOpts): Promise<void> {
45
47
  return
46
48
  }
47
49
  if (!fmtOpts.raw && (process.stdout.isTTY ?? false)) {
48
- log.dim(' (delegation token — ES256, self-identity)')
50
+ log.dim(' (audience-bound token — ES256, self-identity)')
49
51
  }
50
52
  process.stdout.write(`${token}\n`)
51
53
  },
52
54
  })
53
55
  }
54
56
 
57
+ /** Issue either a top-level Kernel credential or an external-audience delegation. */
58
+ export async function issueToken(
59
+ auth: Pick<AuthApi, 'delegate' | 'mint' | 'whoami'>,
60
+ kernel: IssuerId,
61
+ audience: IssuerId,
62
+ ttlSeconds: number,
63
+ ): Promise<MintedCredential> {
64
+ if (audience === kernel) return auth.mint({ ttlSeconds })
65
+ const self = await auth.whoami()
66
+ return auth.delegate(self.id, {
67
+ audience,
68
+ ttlSeconds,
69
+ attenuation: { kind: 'identity', self: true },
70
+ })
71
+ }
72
+
55
73
  export function parseTtl(raw: string | undefined): number {
56
- if (raw === undefined) return 3600
74
+ if (raw === undefined) return DEFAULT_TOKEN_TTL_SECONDS
57
75
  if (!/^\d+$/.test(raw)) {
58
76
  throw new AstraleError(
59
77
  'INVALID_FLAG',
@@ -72,16 +90,17 @@ export function parseTtl(raw: string | undefined): number {
72
90
 
73
91
  export default {
74
92
  name: 'token',
75
- description: 'Mint a fresh delegation token for the active instance + identity',
93
+ description: 'Mint a fresh audience-bound credential for the active instance + identity',
76
94
  afterHelpText: `
77
95
  Behavior:
78
- Delegates the selected authenticated identity for 3600 seconds by default.
79
- The default audience is the target Kernel issuer; choose --audience when the
80
- credential is intended for another service. --for is an alias of --as.
96
+ Mints a token for the selected authenticated identity for 240 seconds by default.
97
+ The default Kernel audience produces a top-level Grant credential reusable with
98
+ --creds. A different --audience produces a delegated service envelope. --for is
99
+ an alias of --as.
81
100
 
82
- The receiver must admit the exact token audience. A Kernel-audience token can
83
- be reused with --creds; a service-audience token can be sent as a Bearer token
84
- to that service's authenticated endpoint.
101
+ Every receiver must admit the exact token audience. A service-audience token can
102
+ be sent as a Bearer token only to that service's authenticated endpoint. The
103
+ requested lifetime cannot exceed the selected source credential's remaining life.
85
104
 
86
105
  Examples:
87
106
  $ export TOKEN=$(astrale token --audience shell.astrale.ai --raw)
@@ -93,14 +112,10 @@ Examples:
93
112
  flags: '--audience <aud>',
94
113
  description: 'Token audience (default: target Kernel issuer)',
95
114
  },
96
- { flags: '--ttl <sec>', description: 'TTL in seconds (default: 3600)' },
115
+ { flags: '--ttl <sec>', description: 'TTL in seconds (default: 240)' },
97
116
  { flags: '--for <identity>', description: 'Mint the token for this identity (alias of --as)' },
98
117
  ],
99
118
  action: async (opts) => {
100
- try {
101
- await tokenCommand(opts as Parameters<typeof tokenCommand>[0])
102
- } catch (error) {
103
- failClosed(error, opts as TokenOpts)
104
- }
119
+ await tokenCommand(opts as Parameters<typeof tokenCommand>[0])
105
120
  },
106
121
  } satisfies CommandDefinition
@@ -0,0 +1,152 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
2
+
3
+ import addCommand from '../add'
4
+ import listCommand from '../list'
5
+
6
+ class ExitError extends Error {}
7
+
8
+ const commit = 'a'.repeat(40)
9
+ const item = {
10
+ name: 'pattern-chart-line-basic',
11
+ type: 'registry:block',
12
+ title: 'Line chart',
13
+ description: 'A controlled chart.',
14
+ dependencies: ['@astrale-os/ui@^0.3.0-beta.0'],
15
+ files: [
16
+ {
17
+ path: 'registry/patterns/chart/line-basic.tsx',
18
+ type: 'registry:component',
19
+ target: 'components/astrale/pattern/chart/line-basic.tsx',
20
+ },
21
+ ],
22
+ meta: { canonicalAddress: 'pattern/chart/line/basic' },
23
+ }
24
+
25
+ let stdout = ''
26
+ let stderr = ''
27
+ let originalArgv: string[]
28
+ let originalExit: typeof process.exit
29
+ let originalFetch: typeof globalThis.fetch
30
+ let originalStdout: typeof process.stdout.write
31
+ let originalStderr: typeof process.stderr.write
32
+
33
+ beforeEach(() => {
34
+ stdout = ''
35
+ stderr = ''
36
+ originalArgv = process.argv
37
+ originalExit = process.exit
38
+ originalFetch = globalThis.fetch
39
+ originalStdout = process.stdout.write
40
+ originalStderr = process.stderr.write
41
+ process.stdout.write = ((chunk: string | Uint8Array) => {
42
+ stdout += String(chunk)
43
+ return true
44
+ }) as typeof process.stdout.write
45
+ process.stderr.write = ((chunk: string | Uint8Array) => {
46
+ stderr += String(chunk)
47
+ return true
48
+ }) as typeof process.stderr.write
49
+ process.exit = (() => {
50
+ throw new ExitError()
51
+ }) as typeof process.exit
52
+ globalThis.fetch = mockFetch()
53
+ })
54
+
55
+ afterEach(() => {
56
+ process.argv = originalArgv
57
+ process.exit = originalExit
58
+ globalThis.fetch = originalFetch
59
+ process.stdout.write = originalStdout
60
+ process.stderr.write = originalStderr
61
+ })
62
+
63
+ describe('UI command machine contracts', () => {
64
+ test('list emits exactly one parseable JSON value', async () => {
65
+ const action = listCommand.action as (
66
+ query: string | undefined,
67
+ options: { json?: boolean; limit?: string },
68
+ ) => Promise<void>
69
+ await action('line-basic', { json: true, limit: '100' })
70
+
71
+ expect(stderr).toBe('')
72
+ expect(JSON.parse(stdout)).toEqual([item])
73
+ })
74
+
75
+ test('list forwards an explicit release without consulting the beta channel', async () => {
76
+ const seen: string[] = []
77
+ globalThis.fetch = mockFetch(seen)
78
+ const action = listCommand.action as (
79
+ query: string | undefined,
80
+ options: { json?: boolean; limit?: string; version?: string },
81
+ ) => Promise<void>
82
+
83
+ await action('line-basic', { json: true, limit: '100', version: '0.3.0-beta.1' })
84
+
85
+ expect(JSON.parse(stdout)).toEqual([item])
86
+ expect(seen.some((url) => url.endsWith('/@astrale-os/ui/beta'))).toBe(false)
87
+ expect(seen.some((url) => url.includes('/git/ref/tags/v0.3.0-beta.1'))).toBe(true)
88
+ })
89
+
90
+ test('add rejects missing items without prompting in machine mode', async () => {
91
+ process.argv = ['node', 'astrale', '--no-prompt', 'ui', 'add']
92
+ const add = addCommand.action as (items: string[], options: { json?: boolean }) => Promise<void>
93
+ await expect(add([], { json: true })).rejects.toBeInstanceOf(ExitError)
94
+ expect(JSON.parse(stderr)).toMatchObject({ error: 'UI_ITEM_NOT_FOUND' })
95
+ expect(stdout).toBe('')
96
+ })
97
+
98
+ test('registry failures retain a stable code without leaking transport secrets', async () => {
99
+ globalThis.fetch = (async () => {
100
+ throw new Error('Authorization: Bearer npm_super_secret_value_that_must_not_escape')
101
+ }) as unknown as typeof fetch
102
+ const action = listCommand.action as (
103
+ query: string | undefined,
104
+ options: { json?: boolean; limit?: string },
105
+ ) => Promise<void>
106
+ await expect(action('line-basic', { json: true, limit: '100' })).rejects.toBeInstanceOf(
107
+ ExitError,
108
+ )
109
+ expect(JSON.parse(stderr)).toEqual({
110
+ error: 'UI_REGISTRY_UNAVAILABLE',
111
+ message: 'Unable to reach npm UI release.',
112
+ })
113
+ expect(stderr).not.toContain('super_secret')
114
+ })
115
+ })
116
+
117
+ function mockFetch(seen: string[] = []): typeof fetch {
118
+ return (async (input: string | URL | Request) => {
119
+ const url = String(input)
120
+ seen.push(url)
121
+ if (url.endsWith('/@astrale-os/ui/beta')) return Response.json({ version: '0.3.0-beta.1' })
122
+ if (url.includes('/git/ref/tags/')) {
123
+ return Response.json({ object: { type: 'commit', sha: commit, url: '' } })
124
+ }
125
+ if (url.endsWith('/tooling/compatibility.json')) {
126
+ return Response.json({
127
+ version: 1,
128
+ shadcn: '4.18.0',
129
+ base: 'base',
130
+ style: 'nova',
131
+ baseUi: '1.7.0',
132
+ react: '^18.3.1 || ^19.0.0',
133
+ tailwind: '^4.3.3',
134
+ presets: ['astrale', 'compact', 'expressive'],
135
+ })
136
+ }
137
+ if (url.endsWith('/registry/patterns/chart/registry.json')) {
138
+ return Response.json({
139
+ items: [
140
+ {
141
+ ...item,
142
+ files: item.files.map((file) => ({ ...file, path: 'line-basic.tsx' })),
143
+ },
144
+ ],
145
+ })
146
+ }
147
+ if (url.endsWith('/registry.json')) {
148
+ return Response.json({ include: ['registry/patterns/chart/registry.json'] })
149
+ }
150
+ return new Response('not found', { status: 404 })
151
+ }) as typeof fetch
152
+ }
@@ -0,0 +1,51 @@
1
+ import type { CommandDefinition } from '../../program'
2
+
3
+ import { promptMultiSelect } from '../../lib/prompt'
4
+ import { addUi, listLockedUi, UiError } from '../../ui'
5
+ import { UI_JSON_OPTION, UI_PROJECT_OPTION, runUiCommand, type UiCommandOptions } from './shared'
6
+
7
+ type Options = UiCommandOptions & { dryRun?: boolean; overwrite?: boolean; yes?: boolean }
8
+
9
+ export default {
10
+ name: 'add',
11
+ description: 'Install consumer-owned Astrale pattern or block source',
12
+ arguments: [
13
+ {
14
+ name: 'items',
15
+ description: 'Canonical pattern or block addresses',
16
+ required: false,
17
+ variadic: true,
18
+ },
19
+ ],
20
+ options: [
21
+ UI_PROJECT_OPTION,
22
+ { flags: '--dry-run', description: 'Show exact files and dependencies without writing' },
23
+ { flags: '--overwrite', description: 'Allow replacing locally changed installed source' },
24
+ { flags: '--yes', description: 'Confirm the planned operation non-interactively' },
25
+ UI_JSON_OPTION,
26
+ ],
27
+ afterHelpText:
28
+ '\nInstalled source belongs to the application. Ordinary add never overwrites local edits.\nRun astrale ui doctor before intentionally replacing local source with --overwrite --yes.\n',
29
+ action: async (items: string[], options: Options) =>
30
+ runUiCommand(options, async () => {
31
+ let selected = items
32
+ if (selected.length === 0) {
33
+ if (process.argv.includes('--ci') || process.argv.includes('--no-prompt')) {
34
+ throw new UiError('UI_ITEM_NOT_FOUND', 'No UI item was provided in non-interactive mode.')
35
+ }
36
+ const available = await listLockedUi(options.project)
37
+ selected =
38
+ (await promptMultiSelect(
39
+ 'Choose Astrale UI source to install',
40
+ available.map((item) => ({
41
+ name: item.title ?? item.meta.canonicalAddress,
42
+ value: item.meta.canonicalAddress,
43
+ description: item.description,
44
+ })),
45
+ )) ?? []
46
+ if (selected.length === 0)
47
+ throw new UiError('UI_ITEM_NOT_FOUND', 'No UI item was selected.')
48
+ }
49
+ return addUi(selected, options)
50
+ }),
51
+ } satisfies CommandDefinition
@@ -0,0 +1,13 @@
1
+ import type { CommandDefinition } from '../../program'
2
+
3
+ import { doctorUi } from '../../ui'
4
+ import { UI_JSON_OPTION, runUiCommand, type UiCommandOptions } from './shared'
5
+
6
+ export default {
7
+ name: 'doctor',
8
+ description: 'Verify Astrale UI package, CSS, config, lock, and installed source integrity',
9
+ arguments: [{ name: 'path', description: 'Application directory', required: false }],
10
+ options: [UI_JSON_OPTION],
11
+ action: async (path: string | undefined, options: UiCommandOptions) =>
12
+ runUiCommand(options, () => doctorUi(path)),
13
+ } satisfies CommandDefinition
@@ -0,0 +1,38 @@
1
+ import type { CommandDefinition } from '../../program'
2
+
3
+ import { initUi, type UiPreset } from '../../ui'
4
+ import { UI_JSON_OPTION, runUiCommand, type UiCommandOptions } from './shared'
5
+
6
+ type Options = UiCommandOptions & {
7
+ preset?: UiPreset
8
+ version?: string
9
+ dryRun?: boolean
10
+ force?: boolean
11
+ install?: boolean
12
+ }
13
+
14
+ export default {
15
+ name: 'init',
16
+ description: 'Initialize Astrale UI in an existing React and Tailwind v4 application',
17
+ arguments: [{ name: 'path', description: 'Application directory', required: false }],
18
+ options: [
19
+ {
20
+ flags: '--preset <name>',
21
+ description: 'Visual preset',
22
+ choices: ['astrale', 'compact', 'expressive'],
23
+ default: 'astrale',
24
+ },
25
+ { flags: '--version <version>', description: 'Exact @astrale-os/ui version or release tag' },
26
+ { flags: '--dry-run', description: 'Print the complete plan without writing or installing' },
27
+ { flags: '--force', description: 'Replace an existing Astrale UI lock intentionally' },
28
+ {
29
+ flags: '--no-install',
30
+ description: 'Write configuration without running the package manager',
31
+ },
32
+ UI_JSON_OPTION,
33
+ ],
34
+ afterHelpText:
35
+ '\nExamples:\n $ astrale ui init --preset astrale\n $ astrale ui init ./app --dry-run --json\n',
36
+ action: async (path: string | undefined, options: Options) =>
37
+ runUiCommand(options, () => initUi({ path, ...options })),
38
+ } satisfies CommandDefinition
@@ -0,0 +1,26 @@
1
+ import type { CommandDefinition } from '../../program'
2
+
3
+ import { listUi } from '../../ui'
4
+ import { UI_JSON_OPTION, runUiCommand, type UiCommandOptions } from './shared'
5
+
6
+ type Options = UiCommandOptions & { type?: string; limit?: string; version?: string }
7
+
8
+ export default {
9
+ name: 'list',
10
+ description: 'List or search Astrale patterns and blocks',
11
+ arguments: [{ name: 'query', description: 'Optional search text', required: false }],
12
+ options: [
13
+ {
14
+ flags: '--type <type>',
15
+ description: 'Restrict to pattern or block',
16
+ choices: ['pattern', 'block'],
17
+ },
18
+ { flags: '--limit <n>', description: 'Maximum results', default: '100' },
19
+ { flags: '--version <version>', description: 'Exact release to inspect' },
20
+ UI_JSON_OPTION,
21
+ ],
22
+ action: async (query: string | undefined, options: Options) =>
23
+ runUiCommand(options, () =>
24
+ listUi(query, { type: options.type, limit: Number(options.limit), version: options.version }),
25
+ ),
26
+ } satisfies CommandDefinition
@@ -0,0 +1,19 @@
1
+ import type { CommandDefinition } from '../../program'
2
+
3
+ import { applyPreset, type UiPreset } from '../../ui'
4
+ import { UI_JSON_OPTION, UI_PROJECT_OPTION, runUiCommand, type UiCommandOptions } from './shared'
5
+
6
+ type Options = UiCommandOptions & { dryRun?: boolean }
7
+
8
+ export default {
9
+ name: 'apply',
10
+ description: 'Change the preset CSS import without rewriting component source',
11
+ arguments: [{ name: 'name', description: 'Preset name' }],
12
+ options: [
13
+ UI_PROJECT_OPTION,
14
+ { flags: '--dry-run', description: 'Show the CSS and lock change without writing' },
15
+ UI_JSON_OPTION,
16
+ ],
17
+ action: async (name: UiPreset, options: Options) =>
18
+ runUiCommand(options, () => applyPreset(name, options)),
19
+ } satisfies CommandDefinition
@@ -0,0 +1,12 @@
1
+ import type { CommandDefinition } from '../../program'
2
+
3
+ import { UI_PRESETS } from '../../ui'
4
+ import { UI_JSON_OPTION, runUiCommand, type UiCommandOptions } from './shared'
5
+
6
+ export default {
7
+ name: 'list',
8
+ description: 'List qualified Astrale UI visual presets',
9
+ options: [UI_JSON_OPTION],
10
+ action: async (options: UiCommandOptions) =>
11
+ runUiCommand(options, async () => UI_PRESETS.map((name) => ({ name }))),
12
+ } satisfies CommandDefinition
@@ -0,0 +1,25 @@
1
+ import { fatal } from '../../lib/log'
2
+ import { output } from '../../lib/output'
3
+
4
+ export type UiCommandOptions = { json?: boolean; project?: string }
5
+
6
+ export async function runUiCommand(
7
+ options: UiCommandOptions,
8
+ operation: () => Promise<unknown>,
9
+ ): Promise<void> {
10
+ try {
11
+ output(await operation(), { json: options.json })
12
+ } catch (error) {
13
+ fatal(error, { json: options.json })
14
+ }
15
+ }
16
+
17
+ export const UI_PROJECT_OPTION = {
18
+ flags: '--project <path>',
19
+ description: 'Existing application root (defaults to the current project)',
20
+ } as const
21
+
22
+ export const UI_JSON_OPTION = {
23
+ flags: '--json',
24
+ description: 'Emit one machine-readable JSON value',
25
+ } as const
@@ -12,7 +12,7 @@ import {
12
12
  type SdkOutdated,
13
13
  } from '../lib/sdk-deps'
14
14
  import { ASTRALE_CLI_SKILL, detectSkill, installSkills, SKILL_INSTALL_HINT } from '../lib/skills'
15
- import { DEFAULT_UPDATE_CHANNEL, updateAstrale } from '../lib/update'
15
+ import { DEFAULT_UPDATE_CHANNEL, packageManagedUpdateError, updateAstrale } from '../lib/update'
16
16
 
17
17
  type UpdateOpts = RawOutputOpts & {
18
18
  check?: boolean
@@ -104,14 +104,22 @@ async function refreshSdkDeps(check: boolean, assumeYes = false): Promise<boolea
104
104
  /**
105
105
  * Read-only staleness report for tooling — `astrale update --check --json`.
106
106
  * domain-studio polls this on load to drive its "update available" badge. It is
107
- * unified and NON-THROWING: the CLI axis catches the package-managed / no-metadata
108
- * case (reported as `managed`, never stale), and the SDK axis is already
109
- * best-effort. Skills are intentionally absent they ride along with `astrale
110
- * update`, so a stale CLI or SDK is the only signal worth surfacing.
107
+ * unified and NON-THROWING: an explicit package-managed result uses npm release
108
+ * identity, while script-install failures remain script failures in `error`
109
+ * instead of being recategorized. The SDK axis is already best-effort. Skills are
110
+ * intentionally absent — they ride along with `astrale update`, so a stale CLI or
111
+ * SDK is the only signal worth surfacing.
111
112
  */
112
113
  export type StaleReport = {
113
114
  stale: boolean
114
- cli: { stale: boolean; managed: boolean; current?: string; latest?: string; channel?: string }
115
+ cli: {
116
+ stale: boolean
117
+ managed: boolean
118
+ current?: string
119
+ latest?: string
120
+ channel?: string
121
+ error?: string
122
+ }
115
123
  sdk: { stale: boolean; inProject: boolean; outdated: SdkOutdated[] }
116
124
  }
117
125
 
@@ -138,6 +146,15 @@ export async function cliStale(
138
146
  version: target.version,
139
147
  currentVersion: running,
140
148
  })
149
+ if (r.status === 'managed') {
150
+ const latest = await dependencies.fetchPackageVersion(target).catch(() => undefined)
151
+ return {
152
+ stale: latest !== undefined && latest !== running,
153
+ managed: true,
154
+ current: running,
155
+ ...(latest === undefined ? {} : { latest, channel: 'npm' }),
156
+ }
157
+ }
141
158
  if (r.status === 'updated') {
142
159
  return {
143
160
  stale: false,
@@ -154,13 +171,12 @@ export async function cliStale(
154
171
  latest: r.latestVersion,
155
172
  channel: r.channel,
156
173
  }
157
- } catch {
158
- const latest = await dependencies.fetchPackageVersion(target).catch(() => undefined)
174
+ } catch (error) {
159
175
  return {
160
- stale: latest !== undefined && latest !== running,
161
- managed: true,
176
+ stale: false,
177
+ managed: false,
162
178
  current: running,
163
- ...(latest === undefined ? {} : { latest, channel: 'npm' }),
179
+ error: error instanceof Error ? error.message : String(error),
164
180
  }
165
181
  }
166
182
  }
@@ -294,6 +310,10 @@ Examples:
294
310
  log.success(`Updated astrale ${result.previousVersion} -> ${result.currentVersion}`)
295
311
  log.dim(` channel: ${result.channel}`)
296
312
  log.dim(` binary: ${result.bin}`)
313
+ } else if (result?.status === 'managed') {
314
+ const error = packageManagedUpdateError(result.executable)
315
+ if (!opts.yes) throw error
316
+ log.warn(`CLI self-update skipped: ${error.message}`)
297
317
  }
298
318
 
299
319
  // Axis B — agent skills. Keep an existing install current with the CLI.