@astrale-os/cli 1.0.0-beta.12 → 1.0.0-beta.13

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 (56) hide show
  1. package/dist/astrale.js +15432 -14745
  2. package/dist/types/admin/contract.d.ts +26 -0
  3. package/dist/types/admin/instance/client.d.ts +2 -4
  4. package/dist/types/admin/instance/model.d.ts +3 -0
  5. package/package.json +1 -1
  6. package/src/admin/.spec/architecture.md +12 -5
  7. package/src/admin/__tests__/fixture.ts +19 -95
  8. package/src/admin/catalog/.spec/api.d.ts +0 -2
  9. package/src/admin/catalog/.spec/architecture.md +5 -4
  10. package/src/admin/catalog/__tests__/client.test.ts +136 -33
  11. package/src/admin/catalog/client.ts +38 -61
  12. package/src/admin/contract.ts +46 -0
  13. package/src/admin/instance/.spec/api.d.ts +4 -8
  14. package/src/admin/instance/.spec/architecture.md +7 -7
  15. package/src/admin/instance/__tests__/client.test.ts +138 -31
  16. package/src/admin/instance/client.ts +32 -37
  17. package/src/admin/instance/model.ts +3 -0
  18. package/src/commands/__tests__/call.test.ts +34 -0
  19. package/src/commands/__tests__/read-commands.test.ts +27 -0
  20. package/src/commands/__tests__/token-ttl.test.ts +49 -4
  21. package/src/commands/call.ts +28 -3
  22. package/src/commands/query.ts +8 -2
  23. package/src/commands/token.ts +34 -20
  24. package/src/commands/ui/__tests__/commands.test.ts +129 -0
  25. package/src/commands/ui/add.ts +51 -0
  26. package/src/commands/ui/doctor.ts +13 -0
  27. package/src/commands/ui/init.ts +38 -0
  28. package/src/commands/ui/list.ts +26 -0
  29. package/src/commands/ui/preset-apply.ts +19 -0
  30. package/src/commands/ui/preset-list.ts +12 -0
  31. package/src/commands/ui/shared.ts +25 -0
  32. package/src/lib/__tests__/binary.test.ts +16 -1
  33. package/src/lib/binary.ts +22 -5
  34. package/src/lib/proc.ts +7 -2
  35. package/src/program/.spec/api.d.ts +1 -0
  36. package/src/program/__tests__/program.test.ts +32 -2
  37. package/src/program/build.ts +23 -1
  38. package/src/program/command.ts +1 -0
  39. package/src/program/registry.ts +2 -1
  40. package/src/ui/.spec/api.d.ts +14 -0
  41. package/src/ui/.spec/architecture.md +10 -0
  42. package/src/ui/.spec/laws.ts +36 -0
  43. package/src/ui/.spec/layout.ts +16 -0
  44. package/src/ui/__tests__/ui.test.ts +542 -0
  45. package/src/ui/index.ts +13 -0
  46. package/src/ui/lock.ts +87 -0
  47. package/src/ui/model.ts +83 -0
  48. package/src/ui/operations.ts +539 -0
  49. package/src/ui/project.ts +146 -0
  50. package/src/ui/release.ts +267 -0
  51. package/src/ui/runner.ts +18 -0
  52. package/studio/server/agent/harness/gateway/token.test.ts +1 -1
  53. package/studio/server/agent/harness/gateway/token.ts +3 -3
  54. package/dist/types/admin/binding.d.ts +0 -19
  55. package/src/admin/__tests__/binding.test.ts +0 -31
  56. package/src/admin/binding.ts +0 -98
@@ -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
@@ -39,11 +39,26 @@ describe('presentBinary', () => {
39
39
 
40
40
  test('--json inlines text for text-like content types', async () => {
41
41
  await presentBinary(
42
- { mediaType: 'text/plain', body: new TextEncoder().encode('hi') },
42
+ { mediaType: 'text/plain', status: 202, body: new TextEncoder().encode('hi') },
43
43
  { json: true },
44
44
  )
45
45
  const obj = JSON.parse(Buffer.concat(chunks).toString())
46
46
  expect(obj.body).toBe('hi')
47
+ expect(obj.status).toBe(202)
48
+ })
49
+
50
+ test('--raw drains streaming binary chunks in exact order', async () => {
51
+ await presentBinary(
52
+ {
53
+ mediaType: 'application/octet-stream',
54
+ body: (async function* () {
55
+ yield new Uint8Array([1, 2])
56
+ yield new Uint8Array([3, 4])
57
+ })(),
58
+ },
59
+ { raw: true },
60
+ )
61
+ expect(Array.from(Buffer.concat(chunks))).toEqual([1, 2, 3, 4])
47
62
  })
48
63
 
49
64
  test('-o writes raw bytes to a file', async () => {
package/src/lib/binary.ts CHANGED
@@ -7,14 +7,31 @@ import { output } from './output'
7
7
 
8
8
  /** A binary kernel result: bytes (or a stream of them) plus its content-type. */
9
9
  export type BinaryLike = {
10
- readonly body: Uint8Array
10
+ readonly body: Uint8Array | AsyncIterable<Uint8Array>
11
11
  readonly mediaType: string
12
+ readonly status?: number
12
13
  readonly headers?: Readonly<Record<string, string>>
13
14
  }
14
15
 
15
16
  /** Collapse a (possibly streamed) binary body into a single byte array. */
16
- export async function readBinaryBody(body: Uint8Array): Promise<Uint8Array> {
17
- return body
17
+ export async function readBinaryBody(
18
+ body: Uint8Array | AsyncIterable<Uint8Array>,
19
+ ): Promise<Uint8Array> {
20
+ if (body instanceof Uint8Array) return new Uint8Array(body)
21
+ const chunks: Uint8Array[] = []
22
+ let size = 0
23
+ for await (const chunk of body) {
24
+ const detached = new Uint8Array(chunk)
25
+ chunks.push(detached)
26
+ size += detached.byteLength
27
+ }
28
+ const bytes = new Uint8Array(size)
29
+ let offset = 0
30
+ for (const chunk of chunks) {
31
+ bytes.set(chunk, offset)
32
+ offset += chunk.byteLength
33
+ }
34
+ return bytes
18
35
  }
19
36
 
20
37
  function isTextLike(contentType: string): boolean {
@@ -43,13 +60,13 @@ function humanSize(bytes: number): string {
43
60
  function jsonEnvelope(resp: BinaryLike, bytes: Uint8Array): Record<string, unknown> {
44
61
  if (isTextLike(resp.mediaType)) {
45
62
  return {
46
- status: 200,
63
+ status: resp.status ?? 200,
47
64
  contentType: resp.mediaType,
48
65
  body: new TextDecoder().decode(bytes),
49
66
  }
50
67
  }
51
68
  return {
52
- status: 200,
69
+ status: resp.status ?? 200,
53
70
  contentType: resp.mediaType,
54
71
  bodyBase64: Buffer.from(bytes).toString('base64'),
55
72
  }
package/src/lib/proc.ts CHANGED
@@ -22,7 +22,11 @@ export function run(
22
22
  opts: { cwd?: string } = {},
23
23
  ): Promise<RunResult> {
24
24
  return new Promise((resolve, reject) => {
25
- const child = spawn(file, args, { cwd: opts.cwd, stdio: ['ignore', 'pipe', 'pipe'] })
25
+ const child = spawn(file, args, {
26
+ cwd: opts.cwd,
27
+ shell: false,
28
+ stdio: ['ignore', 'pipe', 'pipe'],
29
+ })
26
30
  let stdout = ''
27
31
  let stderr = ''
28
32
  child.stdout?.setEncoding('utf8')
@@ -50,7 +54,7 @@ export function runInherit(
50
54
  opts: { cwd?: string } = {},
51
55
  ): Promise<number> {
52
56
  return new Promise((resolve, reject) => {
53
- const child = spawn(file, args, { cwd: opts.cwd, stdio: 'inherit' })
57
+ const child = spawn(file, args, { cwd: opts.cwd, shell: false, stdio: 'inherit' })
54
58
  child.on('error', reject)
55
59
  child.on('close', (code) => resolve(code ?? -1))
56
60
  })
@@ -73,6 +77,7 @@ export function spawnHandle(
73
77
  return spawn(file, args, {
74
78
  cwd: opts.cwd,
75
79
  env: opts.env,
80
+ shell: false,
76
81
  // `detached` makes the child its own process-group leader so the caller can
77
82
  // tear down the whole tree (the child AND its grandchildren) with a single
78
83
  // group signal — `process.kill(-child.pid, …)`.
@@ -5,6 +5,7 @@ export interface CommandArgument {
5
5
  readonly name: string
6
6
  readonly description: string
7
7
  readonly required?: boolean
8
+ readonly variadic?: boolean
8
9
  }
9
10
 
10
11
  /** One Commander-compatible option in a CLI command definition. */
@@ -181,13 +181,21 @@ describe('program composition', () => {
181
181
  'status',
182
182
  'studio',
183
183
  'token',
184
+ 'ui',
185
+ 'ui add',
186
+ 'ui doctor',
187
+ 'ui init',
188
+ 'ui list',
189
+ 'ui preset',
190
+ 'ui preset apply',
191
+ 'ui preset list',
184
192
  'update',
185
193
  'use',
186
194
  'view',
187
195
  'whoami',
188
196
  ])
189
197
  expect(createHash('sha256').update(JSON.stringify(surface)).digest('hex')).toBe(
190
- '56d2049ff4f7ddd24abdd9df39d4a827757f1b39a92dc7fb55933105d722f97f',
198
+ '873d096c4e99f29ab1db17bec0abc4ad2de613f5e44f9cba97c95b052c8b5a98',
191
199
  )
192
200
  })
193
201
 
@@ -281,7 +289,7 @@ describe('help contract — admin target surface is registered', () => {
281
289
  describe('help contract — connect-only command surface', () => {
282
290
  test('runtime management commands are not registered', async () => {
283
291
  const program = await buildProgram()
284
- const names = allCommands(program).map((command) => command.name())
292
+ const names = program.commands.map((command) => command.name())
285
293
 
286
294
  // `logs` and `domain` have managed/admin meanings, so only retired verbs are absent.
287
295
  for (const removed of [
@@ -303,6 +311,28 @@ describe('help contract — connect-only command surface', () => {
303
311
  })
304
312
  })
305
313
 
314
+ describe('help contract — UI is project tooling', () => {
315
+ test('UI commands are local-only and add accepts zero or more canonical addresses', async () => {
316
+ const program = await buildProgram()
317
+ const ui = program.commands.find((command) => command.name() === 'ui')
318
+ const add = ui?.commands.find((command) => command.name() === 'add')
319
+
320
+ expect(ui?.commands.map((command) => command.name())).toEqual([
321
+ 'init',
322
+ 'list',
323
+ 'add',
324
+ 'doctor',
325
+ 'preset',
326
+ ])
327
+ expect(add?.helpInformation()).toContain('[items...]')
328
+ for (const command of ui?.commands ?? []) {
329
+ const help = command.helpInformation()
330
+ expect(help).not.toContain('--url <url>')
331
+ expect(help).not.toContain('--anonymous')
332
+ }
333
+ })
334
+ })
335
+
306
336
  describe('help contract — read command split', () => {
307
337
  test('get is point-read only and query owns structured read flags', async () => {
308
338
  const program = await buildProgram()
@@ -63,6 +63,27 @@ export async function buildProgram(): Promise<Command> {
63
63
  registerCommand(program, (await import('../commands/view-serve')).default)
64
64
  registerCommand(program, (await import('../commands/studio')).default)
65
65
 
66
+ registerGroup(program, {
67
+ name: 'ui',
68
+ description: 'Initialize and install Astrale UI in local applications',
69
+ commands: [
70
+ (await import('../commands/ui/init')).default,
71
+ (await import('../commands/ui/list')).default,
72
+ (await import('../commands/ui/add')).default,
73
+ (await import('../commands/ui/doctor')).default,
74
+ ],
75
+ subgroups: [
76
+ {
77
+ name: 'preset',
78
+ description: 'List and apply qualified Astrale UI presets',
79
+ commands: [
80
+ (await import('../commands/ui/preset-list')).default,
81
+ (await import('../commands/ui/preset-apply')).default,
82
+ ],
83
+ },
84
+ ],
85
+ })
86
+
66
87
  registerGroup(program, {
67
88
  name: 'instance',
68
89
  description: 'Manage admin-provisioned instances and local bookmarks',
@@ -154,6 +175,7 @@ Command groups:
154
175
  Getting started setup (sign in, pick an instance, equip your workspace)
155
176
  Kernel get, mutate, call, query, introspect, logs, view, token
156
177
  Management admin, instance, domain, identity, auth, idp, update
178
+ Application ui (initialize, inspect, and install Astrale UI source)
157
179
  Agent browser (drive the GUI via agent-browser)
158
180
  Studio studio (launch the local Domain Studio GUI for a workspace)
159
181
 
@@ -172,7 +194,7 @@ Examples:
172
194
  $ astrale instance bookmark staging --url https://kernel.example.com
173
195
  $ astrale instance create my-app
174
196
  $ astrale instance status staging
175
- $ astrale token --audience shell.astrale.ai --ttl 3600
197
+ $ astrale token --audience shell.astrale.ai
176
198
  $ astrale query /:notes.example.dev:class.Note --limit 50
177
199
  $ astrale introspect /:kernel.astrale.ai:class.Identity:whois
178
200
  $ astrale query --file query.v6.json --cursor "$CURSOR"
@@ -2,6 +2,7 @@ export interface CommandArgument {
2
2
  readonly name: string
3
3
  readonly description: string
4
4
  readonly required?: boolean
5
+ readonly variadic?: boolean
5
6
  }
6
7
 
7
8
  export interface CommandOption {
@@ -18,7 +18,8 @@ export function registerCommand(parent: Command, def: CommandDefinition): void {
18
18
 
19
19
  if (def.arguments) {
20
20
  for (const arg of def.arguments) {
21
- const bracket = arg.required !== false ? `<${arg.name}>` : `[${arg.name}]`
21
+ const name = arg.variadic ? `${arg.name}...` : arg.name
22
+ const bracket = arg.required !== false ? `<${name}>` : `[${name}]`
22
23
  cmd.argument(bracket, arg.description)
23
24
  }
24
25
  }
@@ -0,0 +1,14 @@
1
+ export {
2
+ addUi,
3
+ applyPreset,
4
+ diffUi,
5
+ doctorUi,
6
+ initUi,
7
+ listUi,
8
+ viewUi,
9
+ type InitUiOptions,
10
+ } from '../operations.js'
11
+ export { UI_PRESETS, UiError, type UiLock, type UiPreset } from '../model.js'
12
+ export { discoverUiProject, type UiProject } from '../project.js'
13
+ export { resolveUiRelease } from '../release.js'
14
+ export { shadcnInvocation, type UiRunner } from '../runner.js'
@@ -0,0 +1,10 @@
1
+ # UI local tooling
2
+
3
+ This private CLI owner manages an existing local React project. It does not connect to a Kernel and
4
+ does not add React, Base UI, Tailwind, shadcn, or the UI runtime to the installed CLI dependency
5
+ graph.
6
+
7
+ One operation resolves one immutable UI commit. Package compatibility and registry reads use that
8
+ commit, and the detected package manager invokes the release-qualified shadcn CLI on demand.
9
+ Application writes complete before the lock advances. Dry-run, list, view, diff, and doctor do not
10
+ claim a file mutation.
@@ -0,0 +1,36 @@
1
+ import { defineLaw } from '@astrale-os/spec/authoring'
2
+
3
+ export const CLI_UI_ONE_SNAPSHOT = defineLaw({
4
+ id: 'CLI-UI-ONE-SNAPSHOT',
5
+ statement:
6
+ 'Each UI operation resolves one immutable repository commit and reads compatibility, registry, and item source only from that snapshot.',
7
+ tests: [{ file: '../__tests__/ui.test.ts', id: 'TEST-CLI-UI-ONE-SNAPSHOT' }],
8
+ })
9
+
10
+ export const CLI_UI_LOCK_AFTER_SUCCESS = defineLaw({
11
+ id: 'CLI-UI-LOCK-AFTER-SUCCESS',
12
+ statement:
13
+ 'Dry runs and failed package or source operations do not advance astrale-ui.lock.json.',
14
+ tests: [{ file: '../__tests__/ui.test.ts', id: 'TEST-CLI-UI-LOCK-AFTER-SUCCESS' }],
15
+ })
16
+
17
+ export const CLI_UI_BOUNDED_REMOTE_DOCUMENTS = defineLaw({
18
+ id: 'CLI-UI-BOUNDED-REMOTE-DOCUMENTS',
19
+ statement:
20
+ 'Release metadata and registry documents are size-bounded, include-bounded, and normalized to stable UI registry errors before any project mutation.',
21
+ tests: [{ file: '../__tests__/ui.test.ts', id: 'TEST-CLI-UI-BOUNDED-REMOTE-DOCUMENTS' }],
22
+ })
23
+
24
+ export const CLI_UI_EXACT_ITEM_SOURCE = defineLaw({
25
+ id: 'CLI-UI-EXACT-ITEM-SOURCE',
26
+ statement:
27
+ 'An add operation admits the exact built item document against the release index and records a content-bearing source digest before invoking project tooling.',
28
+ tests: [{ file: '../__tests__/ui.test.ts', id: 'TEST-CLI-UI-EXACT-ITEM-SOURCE' }],
29
+ })
30
+
31
+ export const CLI_UI_SEMANTIC_DIFF = defineLaw({
32
+ id: 'CLI-UI-SEMANTIC-DIFF',
33
+ statement:
34
+ 'Diff is read-only, path-contained, and classifies locked source and files as upstream changed, unchanged, modified, or deleted without delegating truth to tool output.',
35
+ tests: [{ file: '../__tests__/ui.test.ts', id: 'TEST-CLI-UI-SEMANTIC-DIFF' }],
36
+ })
@@ -0,0 +1,16 @@
1
+ import { defineLayout } from '@astrale-os/spec/authoring'
2
+
3
+ export default defineLayout({
4
+ entries: [
5
+ '.spec/',
6
+ '__tests__/',
7
+ 'index.ts',
8
+ 'lock.ts',
9
+ 'model.ts',
10
+ 'operations.ts',
11
+ 'project.ts',
12
+ 'release.ts',
13
+ 'runner.ts',
14
+ ],
15
+ exact: true,
16
+ })