@astrale-os/cli 0.4.0-alpha.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 (219) hide show
  1. package/.check-workspace.cjs +40 -0
  2. package/README.md +151 -0
  3. package/dist/astrale.js +59749 -0
  4. package/package.json +90 -0
  5. package/src/command.ts +40 -0
  6. package/src/commands/__tests__/admin-instance.test.ts +73 -0
  7. package/src/commands/__tests__/auth-login.test.ts +178 -0
  8. package/src/commands/__tests__/auth-token.test.ts +223 -0
  9. package/src/commands/__tests__/call.test.ts +72 -0
  10. package/src/commands/__tests__/domain-list.test.ts +74 -0
  11. package/src/commands/__tests__/help-contract.test.ts +136 -0
  12. package/src/commands/__tests__/install-identity-override.test.ts +65 -0
  13. package/src/commands/__tests__/instance-bookmark.test.ts +101 -0
  14. package/src/commands/__tests__/instance-create-hosts.test.ts +29 -0
  15. package/src/commands/__tests__/instance-list-rows.test.ts +63 -0
  16. package/src/commands/__tests__/logs.test.ts +117 -0
  17. package/src/commands/__tests__/ls.test.ts +25 -0
  18. package/src/commands/__tests__/setup-plan.test.ts +61 -0
  19. package/src/commands/admin/status.ts +61 -0
  20. package/src/commands/admin/use.ts +77 -0
  21. package/src/commands/auth/login.ts +82 -0
  22. package/src/commands/auth/logout.ts +50 -0
  23. package/src/commands/auth/status.ts +86 -0
  24. package/src/commands/auth/token.ts +162 -0
  25. package/src/commands/browser.ts +207 -0
  26. package/src/commands/call.ts +300 -0
  27. package/src/commands/describe.ts +182 -0
  28. package/src/commands/domain/install.ts +420 -0
  29. package/src/commands/domain/list.ts +154 -0
  30. package/src/commands/domain/publish.ts +155 -0
  31. package/src/commands/get.ts +60 -0
  32. package/src/commands/identity/create.ts +26 -0
  33. package/src/commands/identity/delete.ts +18 -0
  34. package/src/commands/identity/export.ts +66 -0
  35. package/src/commands/identity/import.ts +101 -0
  36. package/src/commands/identity/list.ts +56 -0
  37. package/src/commands/identity/register.ts +170 -0
  38. package/src/commands/identity/sync.ts +32 -0
  39. package/src/commands/identity/unsync.ts +24 -0
  40. package/src/commands/identity/use.ts +18 -0
  41. package/src/commands/identity/whoami.ts +34 -0
  42. package/src/commands/idp/add.ts +150 -0
  43. package/src/commands/idp/list.ts +57 -0
  44. package/src/commands/idp/refresh.ts +38 -0
  45. package/src/commands/idp/remove.ts +36 -0
  46. package/src/commands/idp/show.ts +29 -0
  47. package/src/commands/instance/active.ts +64 -0
  48. package/src/commands/instance/bookmark.ts +72 -0
  49. package/src/commands/instance/create.ts +69 -0
  50. package/src/commands/instance/delete.ts +72 -0
  51. package/src/commands/instance/forget.ts +26 -0
  52. package/src/commands/instance/list.ts +149 -0
  53. package/src/commands/instance/status.ts +42 -0
  54. package/src/commands/instance/use.ts +210 -0
  55. package/src/commands/logs.ts +347 -0
  56. package/src/commands/ls.ts +229 -0
  57. package/src/commands/query.ts +32 -0
  58. package/src/commands/setup.ts +54 -0
  59. package/src/commands/status.ts +60 -0
  60. package/src/commands/studio.ts +401 -0
  61. package/src/commands/token.ts +77 -0
  62. package/src/commands/update.ts +267 -0
  63. package/src/commands/use.ts +87 -0
  64. package/src/errors.ts +65 -0
  65. package/src/kernel/__tests__/auth.test.ts +77 -0
  66. package/src/kernel/__tests__/errors.test.ts +43 -0
  67. package/src/kernel/__tests__/remote-routing.test.ts +70 -0
  68. package/src/kernel/auth.ts +234 -0
  69. package/src/kernel/ca-fetch.ts +119 -0
  70. package/src/kernel/client.ts +191 -0
  71. package/src/kernel/errors.ts +280 -0
  72. package/src/kernel/expand.ts +217 -0
  73. package/src/kernel/index.ts +14 -0
  74. package/src/kernel/options.ts +22 -0
  75. package/src/kernel/remote-routing.ts +88 -0
  76. package/src/kernel/run.ts +63 -0
  77. package/src/kernel/types.ts +14 -0
  78. package/src/lib/__tests__/admin-target.test.ts +112 -0
  79. package/src/lib/__tests__/binary.test.ts +56 -0
  80. package/src/lib/__tests__/command-dx.test.ts +58 -0
  81. package/src/lib/__tests__/concurrency.test.ts +62 -0
  82. package/src/lib/__tests__/config.test.ts +53 -0
  83. package/src/lib/__tests__/design.test.ts +99 -0
  84. package/src/lib/__tests__/domain-identity.test.ts +60 -0
  85. package/src/lib/__tests__/format.test.ts +22 -0
  86. package/src/lib/__tests__/fs-atomic.test.ts +104 -0
  87. package/src/lib/__tests__/identity.test.ts +79 -0
  88. package/src/lib/__tests__/idp-session.driver.ts +53 -0
  89. package/src/lib/__tests__/idp-session.test.ts +357 -0
  90. package/src/lib/__tests__/idp.test.ts +385 -0
  91. package/src/lib/__tests__/instance-candidates.test.ts +73 -0
  92. package/src/lib/__tests__/instance-target.test.ts +183 -0
  93. package/src/lib/__tests__/instance.test.ts +136 -0
  94. package/src/lib/__tests__/keys.test.ts +129 -0
  95. package/src/lib/__tests__/local-status.test.ts +202 -0
  96. package/src/lib/__tests__/output.test.ts +150 -0
  97. package/src/lib/__tests__/panel.test.ts +40 -0
  98. package/src/lib/__tests__/port.test.ts +44 -0
  99. package/src/lib/__tests__/prompt.test.ts +25 -0
  100. package/src/lib/__tests__/sdk-deps.test.ts +68 -0
  101. package/src/lib/__tests__/self.test.ts +272 -0
  102. package/src/lib/__tests__/studio-server-deps.test.ts +74 -0
  103. package/src/lib/__tests__/table.test.ts +53 -0
  104. package/src/lib/__tests__/update.test.ts +246 -0
  105. package/src/lib/__tests__/use-target.test.ts +56 -0
  106. package/src/lib/__tests__/validation.test.ts +34 -0
  107. package/src/lib/admin-domain.ts +25 -0
  108. package/src/lib/admin-instance.ts +26 -0
  109. package/src/lib/admin-target.ts +217 -0
  110. package/src/lib/binary.ts +131 -0
  111. package/src/lib/browser.ts +150 -0
  112. package/src/lib/command-dx.ts +161 -0
  113. package/src/lib/concurrency.ts +31 -0
  114. package/src/lib/config.ts +45 -0
  115. package/src/lib/domain-identity.ts +49 -0
  116. package/src/lib/env.ts +49 -0
  117. package/src/lib/format.ts +4 -0
  118. package/src/lib/fs-atomic.ts +126 -0
  119. package/src/lib/identity.ts +256 -0
  120. package/src/lib/idp-session.ts +134 -0
  121. package/src/lib/idp.ts +876 -0
  122. package/src/lib/instance-candidates.ts +49 -0
  123. package/src/lib/instance-target.ts +182 -0
  124. package/src/lib/instance.ts +395 -0
  125. package/src/lib/keys.ts +294 -0
  126. package/src/lib/local-status.ts +152 -0
  127. package/src/lib/log.ts +116 -0
  128. package/src/lib/login-flow.ts +164 -0
  129. package/src/lib/meta.ts +86 -0
  130. package/src/lib/output.ts +222 -0
  131. package/src/lib/panel.ts +61 -0
  132. package/src/lib/paths.ts +11 -0
  133. package/src/lib/port.ts +41 -0
  134. package/src/lib/proc.ts +82 -0
  135. package/src/lib/prompt.ts +136 -0
  136. package/src/lib/provision-instance.ts +170 -0
  137. package/src/lib/sdk-deps.ts +104 -0
  138. package/src/lib/self.ts +166 -0
  139. package/src/lib/skills.ts +171 -0
  140. package/src/lib/table.ts +62 -0
  141. package/src/lib/update.ts +315 -0
  142. package/src/lib/use-target.ts +24 -0
  143. package/src/lib/validation.ts +59 -0
  144. package/src/program.ts +200 -0
  145. package/src/registry.ts +59 -0
  146. package/src/setup/__tests__/util.test.ts +29 -0
  147. package/src/setup/engine.ts +83 -0
  148. package/src/setup/render.ts +109 -0
  149. package/src/setup/steps/admin.ts +78 -0
  150. package/src/setup/steps/agent-browser.ts +81 -0
  151. package/src/setup/steps/auth.ts +54 -0
  152. package/src/setup/steps/domain.ts +68 -0
  153. package/src/setup/steps/index.ts +18 -0
  154. package/src/setup/steps/instance.ts +119 -0
  155. package/src/setup/steps/skills-bridge.ts +70 -0
  156. package/src/setup/steps/skills.ts +59 -0
  157. package/src/setup/types.ts +61 -0
  158. package/src/setup/util.ts +34 -0
  159. package/src/test-utils.ts +18 -0
  160. package/studio/client/dist/assets/index-DOwzZAEK.css +1 -0
  161. package/studio/client/dist/assets/index-wtU0Zxhy.js +183 -0
  162. package/studio/client/dist/index.html +13 -0
  163. package/studio/package.json +62 -0
  164. package/studio/server/agent/ask.ts +68 -0
  165. package/studio/server/agent/bridge-mcp.ts +182 -0
  166. package/studio/server/agent/bridge.ts +188 -0
  167. package/studio/server/agent/claude.ts +666 -0
  168. package/studio/server/agent/mock.ts +186 -0
  169. package/studio/server/agent/prompt.ts +202 -0
  170. package/studio/server/agent/registry.ts +29 -0
  171. package/studio/server/agent/runner.ts +484 -0
  172. package/studio/server/agent/schema-map.ts +112 -0
  173. package/studio/server/agent/types.ts +120 -0
  174. package/studio/server/api.ts +574 -0
  175. package/studio/server/cache.ts +138 -0
  176. package/studio/server/detect.ts +81 -0
  177. package/studio/server/domain.ts +70 -0
  178. package/studio/server/index.ts +136 -0
  179. package/studio/server/introspect/anatomy-extras.ts +398 -0
  180. package/studio/server/introspect/anatomy.ts +108 -0
  181. package/studio/server/introspect/bundle.ts +57 -0
  182. package/studio/server/introspect/core-extractor.ts +119 -0
  183. package/studio/server/introspect/core.ts +44 -0
  184. package/studio/server/introspect/diff.ts +133 -0
  185. package/studio/server/introspect/extractor.ts +102 -0
  186. package/studio/server/introspect/hash.ts +21 -0
  187. package/studio/server/introspect/overlay-tsmorph.ts +874 -0
  188. package/studio/server/introspect/overlay.ts +57 -0
  189. package/studio/server/introspect/runtime.ts +99 -0
  190. package/studio/server/introspect/schema-refs.ts +46 -0
  191. package/studio/server/lifecycle.ts +38 -0
  192. package/studio/server/sse.ts +57 -0
  193. package/studio/server/state/baseline.ts +211 -0
  194. package/studio/server/state/catalog.ts +117 -0
  195. package/studio/server/state/comments.ts +321 -0
  196. package/studio/server/state/context.ts +167 -0
  197. package/studio/server/state/copy.ts +156 -0
  198. package/studio/server/state/create.ts +156 -0
  199. package/studio/server/state/documents.ts +70 -0
  200. package/studio/server/state/env.ts +161 -0
  201. package/studio/server/state/git.ts +75 -0
  202. package/studio/server/state/handoff.ts +55 -0
  203. package/studio/server/state/harness-gateway.ts +181 -0
  204. package/studio/server/state/harness-token.ts +0 -0
  205. package/studio/server/state/instance.ts +244 -0
  206. package/studio/server/state/integrations.ts +55 -0
  207. package/studio/server/state/layout.ts +55 -0
  208. package/studio/server/state/settings.ts +39 -0
  209. package/studio/server/state/store.ts +97 -0
  210. package/studio/server/state/updates.ts +63 -0
  211. package/studio/server/state/usage.ts +37 -0
  212. package/studio/server/state/views.ts +138 -0
  213. package/studio/server/state/visibility.ts +33 -0
  214. package/studio/server/watch.ts +81 -0
  215. package/studio/server/workspace-state.ts +26 -0
  216. package/studio/server/workspace-watch.ts +101 -0
  217. package/studio/shared/types.ts +873 -0
  218. package/studio/tsconfig.json +23 -0
  219. package/tsconfig.json +14 -0
@@ -0,0 +1,347 @@
1
+ import type { JournalEntry, JournalFilter } from '@astrale-os/kernel-core'
2
+
3
+ /**
4
+ * `astrale logs` — tail the kernel event journal (Root.journal) for the target
5
+ * instance. Defaults to the kernel journal syscall
6
+ * `/kernel.astrale.ai/class.Root/journal`; `--service <name>` switches to the
7
+ * per-instance `services` domain log buffer (the historical behavior).
8
+ *
9
+ * Target the instance with `-i <instance>` (inherited from withKernelOptions).
10
+ */
11
+ import chalk from 'chalk'
12
+
13
+ import type { CommandDefinition } from '../command'
14
+ import type { ClientContext, KernelCommandOpts } from '../kernel'
15
+ import type { Column, ListProjection } from '../lib/output'
16
+
17
+ import { runKernelCommand, withKernelClient } from '../kernel'
18
+ import { fatal, withSpinner } from '../lib/log'
19
+ import { isMachine, output, presentList } from '../lib/output'
20
+
21
+ const ROOT_JOURNAL_PATH = '/kernel.astrale.ai/class.Root/journal'
22
+ const DEFAULT_SERVICES_ORIGIN = 'services.astrale.ai'
23
+ const DEFAULT_LIMIT = 200
24
+ const FOLLOW_INTERVAL_MS = 2000
25
+ // The journal-read syscall journals its own ops; hide them by default so polling
26
+ // doesn't self-pollute the view. `--all` shows them.
27
+ const SELF_READ_PREFIX = 'op:class.Root.method.journal:'
28
+
29
+ // The published @astrale-os/kernel-core@0.5.0 JournalFilter has no `cursor` yet
30
+ // (added on the kernel branch). Type against the augmented shape so the
31
+ // incremental cursor compiles against the consumer dependency.
32
+ type EventsParams = JournalFilter & { cursor?: number }
33
+
34
+ type LogsOpts = KernelCommandOpts & {
35
+ since?: string
36
+ until?: string
37
+ topic?: string
38
+ principal?: string
39
+ limit?: string
40
+ cursor?: string
41
+ follow?: boolean
42
+ all?: boolean
43
+ timing?: boolean
44
+ // service mode
45
+ service?: string
46
+ tail?: string
47
+ servicesOrigin?: string
48
+ }
49
+
50
+ /**
51
+ * Kernel journal page. The Root.journal syscall returns a BARE `JournalEntry[]`;
52
+ * the client derives the next cursor from the max `seq`. We still model a
53
+ * `nextCursor` field so the TTY footer / incremental tail share one shape, and
54
+ * so a future paged kernel response degrades gracefully.
55
+ */
56
+ type EventsPage = {
57
+ entries: JournalEntry[]
58
+ nextCursor?: number | null
59
+ }
60
+
61
+ // ── Root.journal (default) ───────────────────────────────────
62
+
63
+ /** The highest `seq` across entries, or null when empty. */
64
+ function maxSeq(entries: JournalEntry[]): number | null {
65
+ let max: number | null = null
66
+ for (const e of entries) {
67
+ if (typeof e.seq === 'number' && (max === null || e.seq > max)) max = e.seq
68
+ }
69
+ return max
70
+ }
71
+
72
+ /**
73
+ * Accept the bare `JournalEntry[]` the syscall returns (and tolerate a paged
74
+ * `{ entries, nextCursor }` shape if the kernel ever switches). `nextCursor` is
75
+ * the max `seq` — the client passes it back as `cursor` to tail incrementally.
76
+ */
77
+ export function normalizePage(raw: unknown): EventsPage {
78
+ if (Array.isArray(raw)) {
79
+ const entries = raw as JournalEntry[]
80
+ return { entries, nextCursor: maxSeq(entries) }
81
+ }
82
+ const obj = (raw ?? {}) as { entries?: unknown; nextCursor?: unknown }
83
+ const entries = Array.isArray(obj.entries) ? (obj.entries as JournalEntry[]) : []
84
+ const nextCursor = typeof obj.nextCursor === 'number' ? obj.nextCursor : maxSeq(entries)
85
+ return { entries, nextCursor }
86
+ }
87
+
88
+ /** Build the JournalFilter (+ optional seq cursor) from typed flags. */
89
+ export function buildEventsParams(opts: LogsOpts): EventsParams {
90
+ const params: EventsParams = {}
91
+ if (opts.topic) params.topic = opts.topic
92
+ if (opts.principal) params.principal = opts.principal as JournalFilter['principal']
93
+ if (opts.since !== undefined) params.since = parseTimeFlag('--since', opts.since)
94
+ if (opts.until !== undefined) params.until = parseTimeFlag('--until', opts.until)
95
+ params.limit = opts.limit !== undefined ? parsePositiveInt('--limit', opts.limit) : DEFAULT_LIMIT
96
+ if (opts.cursor !== undefined) params.cursor = parsePositiveInt('--cursor', opts.cursor)
97
+ return params
98
+ }
99
+
100
+ /** Accept epoch-ms or an ISO-8601 string → epoch ms. */
101
+ export function parseTimeFlag(flag: string, raw: string): number {
102
+ if (/^-?\d+$/.test(raw)) return Number(raw)
103
+ const ms = Date.parse(raw)
104
+ if (Number.isNaN(ms)) throw new Error(`${flag} needs epoch-ms or ISO-8601, got "${raw}"`)
105
+ return ms
106
+ }
107
+
108
+ function parsePositiveInt(flag: string, raw: string): number {
109
+ const n = Number(raw)
110
+ if (!Number.isInteger(n) || n <= 0) {
111
+ throw new Error(`${flag} needs a positive integer, got "${raw}"`)
112
+ }
113
+ return n
114
+ }
115
+
116
+ const TOPIC_COLOR = (s: string): string =>
117
+ s.startsWith('op:') ? chalk.cyan(s) : s.startsWith('sys:') ? chalk.magenta(s) : chalk.dim(s)
118
+
119
+ /** op:*:completed|failed carry `durationMs` in their payload; started does not. */
120
+ const latencyOf = (e: JournalEntry): string => {
121
+ const d = (e.event.payload as { durationMs?: number } | undefined)?.durationMs
122
+ return typeof d === 'number' ? `${d}ms` : ''
123
+ }
124
+
125
+ /** Highlight slow ops: green < 100ms, yellow < 500ms, red beyond. */
126
+ const LATENCY_COLOR = (s: string): string => {
127
+ if (!s) return s
128
+ const ms = Number.parseInt(s, 10)
129
+ if (ms >= 500) return chalk.red(s)
130
+ if (ms >= 100) return chalk.yellow(s)
131
+ return chalk.green(s)
132
+ }
133
+
134
+ /** Short labels for the per-step dispatch timing (payload.timing). */
135
+ const STEP_LABEL: Record<string, string> = {
136
+ authenticate: 'auth',
137
+ validateInput: 'in',
138
+ authorize: 'authz',
139
+ resolve: 'resolve',
140
+ invariants: 'inv',
141
+ execute: 'exec',
142
+ validateOutput: 'out',
143
+ effects: 'fx',
144
+ }
145
+
146
+ /** Compact per-step breakdown, non-zero steps only: e.g. "auth:7 authz:13 exec:12". */
147
+ const timingOf = (e: JournalEntry): string => {
148
+ const t = (e.event.payload as { timing?: Record<string, number> } | undefined)?.timing
149
+ if (!t || typeof t !== 'object') return ''
150
+ return Object.entries(t)
151
+ .filter(([, ms]) => typeof ms === 'number' && ms > 0)
152
+ .map(([k, ms]) => `${STEP_LABEL[k] ?? k}:${ms}`)
153
+ .join(' ')
154
+ }
155
+
156
+ function eventsProjection(entries: JournalEntry[], showTiming = false): ListProjection {
157
+ const columns: Column[] = [
158
+ { key: 'seq', header: 'SEQ', color: chalk.dim },
159
+ { key: 'ts', header: 'TIME', color: chalk.dim },
160
+ { key: 'topic', header: 'TOPIC', color: TOPIC_COLOR },
161
+ { key: 'latency', header: 'LATENCY', color: LATENCY_COLOR },
162
+ ...(showTiming ? [{ key: 'steps', header: 'STEPS', color: chalk.dim } as Column] : []),
163
+ { key: 'principal', header: 'PRINCIPAL', color: chalk.dim },
164
+ ]
165
+ return {
166
+ columns,
167
+ rows: entries.map((e) => ({
168
+ seq: String(e.seq),
169
+ ts: new Date(e.event.metadata.timestamp).toISOString(),
170
+ topic: e.event.topic,
171
+ latency: latencyOf(e),
172
+ ...(showTiming ? { steps: timingOf(e) } : {}),
173
+ principal: String(e.event.metadata.principal),
174
+ })),
175
+ paths: entries.map((e) => String(e.seq)),
176
+ }
177
+ }
178
+
179
+ async function fetchEventsPage(ctx: ClientContext, opts: LogsOpts): Promise<EventsPage> {
180
+ const raw = await ctx.client.call(ROOT_JOURNAL_PATH, buildEventsParams(opts))
181
+ const page = normalizePage(raw)
182
+ // Strip the journal's own read ops by default (but keep nextCursor past them).
183
+ const entries = opts.all
184
+ ? page.entries
185
+ : page.entries.filter((e) => !e.event.topic.startsWith(SELF_READ_PREFIX))
186
+ return { entries, nextCursor: page.nextCursor }
187
+ }
188
+
189
+ function printEventLine(e: JournalEntry, showTiming = false): void {
190
+ const ts = new Date(e.event.metadata.timestamp).toISOString()
191
+ const lat = latencyOf(e)
192
+ const steps = showTiming ? timingOf(e) : ''
193
+ process.stdout.write(
194
+ `${chalk.dim(String(e.seq).padStart(6))} ${chalk.dim(ts)} ${TOPIC_COLOR(e.event.topic)} ${chalk.dim(String(e.event.metadata.principal))}${lat ? ` ${LATENCY_COLOR(lat)}` : ''}${steps ? ` ${chalk.dim(`[${steps}]`)}` : ''}\n`,
195
+ )
196
+ }
197
+
198
+ async function runEvents(opts: LogsOpts): Promise<void> {
199
+ if (opts.follow) return followEvents(opts)
200
+
201
+ await runKernelCommand<EventsPage>({
202
+ opts,
203
+ label: 'Kernel events',
204
+ fn: (ctx) => fetchEventsPage(ctx, opts),
205
+ format: (page, fmtOpts) => {
206
+ if (isMachine(fmtOpts) || fmtOpts.format) {
207
+ // Machine surface: the kernel's own entries array, no projection.
208
+ output(page.entries, fmtOpts)
209
+ return
210
+ }
211
+ presentList(page.entries, fmtOpts, (entries) => eventsProjection(entries, opts.timing))
212
+ if (typeof page.nextCursor === 'number') {
213
+ process.stdout.write(chalk.dim(` tail: --follow (or --cursor ${page.nextCursor})\n`))
214
+ }
215
+ },
216
+ })
217
+ }
218
+
219
+ /**
220
+ * Client-side polling follow (ctx.client exposes no stream transport). Take the
221
+ * max `seq` from each page and pass it back as `cursor` (exclusive cursor) so
222
+ * the next poll only returns new entries.
223
+ */
224
+ async function followEvents(opts: LogsOpts): Promise<void> {
225
+ await withKernelClient(opts, async (ctx) => {
226
+ let cursor = opts.cursor !== undefined ? parsePositiveInt('--cursor', opts.cursor) : undefined
227
+ for (;;) {
228
+ const page = await fetchEventsPage(ctx, { ...opts, cursor: cursor?.toString() })
229
+ for (const e of page.entries) printEventLine(e, opts.timing)
230
+ if (typeof page.nextCursor === 'number') cursor = page.nextCursor
231
+ await new Promise((resolve) => setTimeout(resolve, FOLLOW_INTERVAL_MS))
232
+ }
233
+ })
234
+ }
235
+
236
+ // ── --service (legacy services-domain log buffer) ───────────
237
+
238
+ type ServiceLogs = {
239
+ name: string
240
+ lines: Array<{ ts: number; level: string; line: string }>
241
+ }
242
+
243
+ const LEVEL_COLOR: Record<string, (s: string) => string> = {
244
+ error: chalk.red,
245
+ warn: chalk.yellow,
246
+ access: chalk.cyan,
247
+ debug: chalk.dim,
248
+ }
249
+
250
+ /** Accept a bare service name, a `<name>.…` host, or a full https URL → the service name. */
251
+ export function parseServiceName(ref: string): string {
252
+ let host = ref.trim()
253
+ if (host.includes('://')) {
254
+ try {
255
+ host = new URL(host).hostname
256
+ } catch {
257
+ // keep the raw value — the call will fail loud with the name
258
+ }
259
+ }
260
+ return host.includes('.') ? (host.split('.')[0] ?? host) : host
261
+ }
262
+
263
+ async function runService(opts: LogsOpts): Promise<void> {
264
+ const name = parseServiceName(opts.service as string)
265
+ const origin = opts.servicesOrigin ?? DEFAULT_SERVICES_ORIGIN
266
+ const tail = opts.tail !== undefined ? Number(opts.tail) : undefined
267
+ if (tail !== undefined && (!Number.isInteger(tail) || tail <= 0)) {
268
+ throw new Error(`--tail needs a positive integer, got "${opts.tail}"`)
269
+ }
270
+ const result = await withSpinner(`Fetching logs for ${name}`, !isMachine(opts), () =>
271
+ withKernelClient(
272
+ opts,
273
+ async (ctx) =>
274
+ (await ctx.client.call(
275
+ `/${origin}/services/${name}::logs`,
276
+ tail !== undefined ? { tail } : {},
277
+ )) as ServiceLogs,
278
+ ),
279
+ )
280
+ if (isMachine(opts)) {
281
+ output(result, opts)
282
+ return
283
+ }
284
+ if (result.lines.length === 0) {
285
+ console.log(chalk.dim(`no log lines captured yet for ${result.name}`))
286
+ return
287
+ }
288
+ for (const entry of result.lines) {
289
+ const ts = new Date(entry.ts).toISOString()
290
+ const paint = LEVEL_COLOR[entry.level] ?? ((s: string) => s)
291
+ console.log(`${chalk.dim(ts)} ${paint(entry.level.padEnd(6))} ${entry.line}`)
292
+ }
293
+ }
294
+
295
+ export default {
296
+ name: 'logs',
297
+ description: 'Tail the kernel event journal (or a service log buffer with --service)',
298
+ options: [
299
+ { flags: '--since <t>', description: 'Events at/after this time (epoch-ms or ISO-8601)' },
300
+ { flags: '--until <t>', description: 'Events at/before this time (epoch-ms or ISO-8601)' },
301
+ { flags: '--topic <glob>', description: 'Topic glob (e.g. op:*:failed, graph:node:**)' },
302
+ { flags: '--principal <id>', description: 'Filter by the triggering identity' },
303
+ { flags: '--limit <n>', description: `Max entries (default ${DEFAULT_LIMIT})` },
304
+ { flags: '--cursor <n>', description: 'Start after this journal sequence number' },
305
+ { flags: '--follow', description: 'Poll for new events (Ctrl-C to stop)' },
306
+ { flags: '--all', description: 'Include the journal-read syscall ops (hidden by default)' },
307
+ { flags: '--timing', description: 'Show the per-step dispatch breakdown (auth/authz/exec/…)' },
308
+ { flags: '--service <name>', description: 'Tail a deployed service log buffer instead' },
309
+ { flags: '--tail <n>', description: '[--service] lines to return (default 200, max 500)' },
310
+ {
311
+ flags: '--services-origin <origin>',
312
+ description: `[--service] services-domain origin (default ${DEFAULT_SERVICES_ORIGIN})`,
313
+ },
314
+ ],
315
+ afterHelpText: `
316
+ Default: tails the kernel event journal via ${ROOT_JOURNAL_PATH} on the target
317
+ instance (-i <instance>). Topics use ':'-segmented globs ('*' one segment,
318
+ '**' zero-or-more). Machine output (--json / pipe) emits the JournalEntry[]
319
+ array; a TTY shows a SEQ/TIME/TOPIC/LATENCY/PRINCIPAL table (LATENCY is the
320
+ op's durationMs, present on :completed/:failed). --timing adds a STEPS column
321
+ with the per-step dispatch breakdown (auth/in/authz/resolve/inv/exec/out/fx,
322
+ non-zero only). --follow polls for new entries (client-side, tailing by
323
+ sequence number). The journal-read syscall's own ops are hidden unless --all.
324
+
325
+ --service <name> switches to the per-instance 'services' domain log buffer
326
+ (console output, 5xx accesses, uncaught exceptions). Requires the services
327
+ domain installed on the target instance.
328
+
329
+ Examples:
330
+ astrale logs -i staging
331
+ astrale logs -i staging --topic 'op:*:failed' --limit 50
332
+ astrale logs -i staging --topic 'op:*:completed' --timing
333
+ astrale logs -i staging --since 2026-06-20T00:00:00Z --follow
334
+ astrale logs --service my-notes -i staging --tail 50
335
+ `,
336
+ action: async (opts: LogsOpts) => {
337
+ try {
338
+ if (opts.service) {
339
+ await runService(opts)
340
+ } else {
341
+ await runEvents(opts)
342
+ }
343
+ } catch (e) {
344
+ fatal(e)
345
+ }
346
+ },
347
+ } satisfies CommandDefinition
@@ -0,0 +1,229 @@
1
+ import chalk from 'chalk'
2
+
3
+ import type { CommandDefinition } from '../command'
4
+ import type { KernelCommandOpts, ClientContext, SelfExpansionMeta } from '../kernel'
5
+ import type { ListProjection } from '../lib/output'
6
+
7
+ import {
8
+ expandSelfInPath,
9
+ extractItems,
10
+ formatKernelError,
11
+ runKernelCommand,
12
+ withKernelClient,
13
+ withSelfHint,
14
+ } from '../kernel'
15
+ import { spinner } from '../lib/log'
16
+ import { isMachine, output, presentList } from '../lib/output'
17
+
18
+ type LsOpts = KernelCommandOpts & {
19
+ long?: boolean
20
+ quiet?: boolean
21
+ recursive?: boolean
22
+ count?: boolean
23
+ filter?: string
24
+ }
25
+
26
+ /** A child node as returned by `::listChildren`. */
27
+ type Item = {
28
+ id?: string
29
+ class?: string
30
+ path?: string
31
+ props?: Record<string, unknown>
32
+ __labels?: string[]
33
+ }
34
+
35
+ // ── Display projection ──────────────────────────────────────
36
+
37
+ /** `/dist.astrale.ai` → `dist.astrale.ai`; `/` stays `/`. */
38
+ export function basename(path?: string): string {
39
+ if (!path || path === '/') return path ?? ''
40
+ return path.slice(path.lastIndexOf('/') + 1)
41
+ }
42
+
43
+ /** `/:kernel.astrale.ai:class.Domain` → `Domain`; falls back to the most specific label. */
44
+ export function classNameOf(item: Item): string {
45
+ const tail = item.class?.split(/[/:.]/).pop()
46
+ return tail || item.__labels?.[item.__labels.length - 1] || '?'
47
+ }
48
+
49
+ /** The addressable path of a child (for `-q` / tree descent). */
50
+ function itemPath(item: Item): string {
51
+ return item.path ?? (item.id ? `@${item.id}` : '')
52
+ }
53
+
54
+ function lsProjection(items: Item[]): ListProjection {
55
+ return {
56
+ columns: [
57
+ { key: 'name', header: 'NAME', color: chalk.cyan },
58
+ { key: 'kind', header: 'KIND', color: chalk.dim },
59
+ { key: 'id', header: 'ID', color: chalk.dim },
60
+ ],
61
+ rows: items.map((i) => ({ name: basename(i.path), kind: classNameOf(i), id: i.id ?? '' })),
62
+ paths: items.map(itemPath),
63
+ }
64
+ }
65
+
66
+ function applyFilter(items: Item[], filter: string | undefined): Item[] {
67
+ if (!filter) return items
68
+ const f = filter.toLowerCase()
69
+ return items.filter((i) => {
70
+ const kindMatch = classNameOf(i).toLowerCase() === f
71
+ const labelMatch = i.__labels?.some((l) => l.toLowerCase() === f) ?? false
72
+ return kindMatch || labelMatch
73
+ })
74
+ }
75
+
76
+ // ── Command ─────────────────────────────────────────────────
77
+
78
+ export async function lsCommand(path: string, opts: LsOpts): Promise<void> {
79
+ let expandedPath: string
80
+ let meta
81
+ try {
82
+ ;({ path: expandedPath, meta } = await expandSelfInPath(path, opts))
83
+ } catch (e) {
84
+ process.stderr.write((e instanceof Error ? e.message : 'Invalid @self expansion') + '\n')
85
+ process.exit(1)
86
+ }
87
+
88
+ if (opts.recursive) {
89
+ return recursiveLs(expandedPath, opts, meta)
90
+ }
91
+
92
+ await runKernelCommand({
93
+ opts,
94
+ label: `Children of ${expandedPath}`,
95
+ fn: (ctx) => withSelfHint(() => ctx.client.call(`${expandedPath}::listChildren`, {}), meta),
96
+ format: (result, fmtOpts) => {
97
+ const items = applyFilter(extractItems<Item>(result), opts.filter)
98
+ presentList(
99
+ items,
100
+ { ...fmtOpts, quiet: opts.quiet, count: opts.count, long: opts.long },
101
+ lsProjection,
102
+ )
103
+ },
104
+ })
105
+ }
106
+
107
+ // ── Recursive tree ──────────────────────────────────────────
108
+
109
+ const MAX_DEPTH = 5
110
+ const MAX_NODES = 200
111
+
112
+ type TreeNode = Item & { children?: TreeNode[] }
113
+
114
+ async function recursiveLs(
115
+ path: string,
116
+ opts: LsOpts,
117
+ meta: SelfExpansionMeta | undefined,
118
+ ): Promise<void> {
119
+ const machine = isMachine(opts)
120
+ const spin = !machine ? spinner(`Listing ${path} recursively...`) : null
121
+
122
+ try {
123
+ await withKernelClient(opts, async (ctx) => {
124
+ const counter = { count: 0 }
125
+ const tree = await withSelfHint(() => buildTree(ctx, path, 0, counter), meta)
126
+ spin?.succeed(`Tree of ${path}`)
127
+ if (!machine) console.log('')
128
+
129
+ if (machine || opts.format) {
130
+ output(tree, opts)
131
+ } else if (opts.quiet) {
132
+ printTreeQuiet(tree)
133
+ } else {
134
+ printTree(tree, '')
135
+ }
136
+ })
137
+ } catch (error) {
138
+ if (!machine && spin) spin.fail('Failed')
139
+ await formatKernelError(error, machine, undefined, opts.debug, { credential: opts.creds })
140
+ process.exit(1)
141
+ }
142
+ }
143
+
144
+ async function buildTree(
145
+ ctx: ClientContext,
146
+ path: string,
147
+ depth: number,
148
+ counter: { count: number },
149
+ ): Promise<TreeNode[]> {
150
+ if (depth >= MAX_DEPTH || counter.count >= MAX_NODES) return []
151
+ try {
152
+ const result = await ctx.client.call(`${path}::listChildren`, {})
153
+ const items = extractItems<Item>(result)
154
+
155
+ const nodes: TreeNode[] = []
156
+ for (const item of items) {
157
+ if (counter.count >= MAX_NODES) break
158
+ counter.count++
159
+ const node: TreeNode = { ...item }
160
+ // Descend by the child's absolute path (the kernel returns `path`, not `slug`).
161
+ if (item.path && item.path !== '/') {
162
+ node.children = await buildTree(ctx, item.path, depth + 1, counter)
163
+ }
164
+ nodes.push(node)
165
+ }
166
+ return nodes
167
+ } catch {
168
+ return []
169
+ }
170
+ }
171
+
172
+ function printTree(nodes: TreeNode[], prefix: string): void {
173
+ for (let i = 0; i < nodes.length; i++) {
174
+ const node = nodes[i]
175
+ const isLast = i === nodes.length - 1
176
+ const connector = isLast ? '└── ' : '├── '
177
+ const childPrefix = isLast ? ' ' : '│ '
178
+
179
+ const name = basename(node.path) || node.id || '?'
180
+ console.log(`${prefix}${connector}${chalk.cyan(name)} ${chalk.dim(classNameOf(node))}`)
181
+
182
+ if (node.children && node.children.length > 0) {
183
+ printTree(node.children, prefix + childPrefix)
184
+ }
185
+ }
186
+ }
187
+
188
+ function printTreeQuiet(nodes: TreeNode[]): void {
189
+ for (const node of nodes) {
190
+ process.stdout.write(itemPath(node) + '\n')
191
+ if (node.children) printTreeQuiet(node.children)
192
+ }
193
+ }
194
+
195
+ export default {
196
+ name: 'ls',
197
+ description: 'List children of a node',
198
+ afterHelpText: `
199
+ Behavior:
200
+ Default output is a NAME/KIND/ID table on a TTY, JSON when piped. --filter
201
+ matches a node KIND or label: Folder, Method, Domain. At a domain's tree
202
+ position the children are Folder nodes (class.X), not Class — so --filter
203
+ Class returns nothing; use --filter Folder, or descend into class.<X> and
204
+ --filter Method. -R tree view is TTY-only (raw/JSON emits the nested tree).
205
+ -q prints one absolute path per line (pipeable). Note: ls /<domain> may
206
+ report NOT_FOUND even when it exists — use describe, or ls one of its children.
207
+
208
+ Examples:
209
+ $ astrale ls /
210
+ $ astrale ls /kernel.astrale.ai --filter Folder
211
+ $ astrale ls / -q | xargs -I{} astrale describe {}
212
+ `,
213
+ arguments: [
214
+ { name: 'path', description: 'Node path (/domain/Class) or ID (@nodeId)', required: false },
215
+ ],
216
+ options: [
217
+ { flags: '-l, --long', description: 'Full node dump (default: compact)' },
218
+ { flags: '-q, --quiet', description: 'One path per line (unix-pipeable)' },
219
+ { flags: '-R, --recursive', description: 'List recursively (tree view)' },
220
+ { flags: '--count', description: 'Print only the number of children' },
221
+ {
222
+ flags: '--filter <kind>',
223
+ description: 'Filter children by kind or label (e.g., Folder, Method, Domain)',
224
+ },
225
+ ],
226
+ action: async (path, opts) => {
227
+ await lsCommand((path as string | undefined) ?? '/', opts as Parameters<typeof lsCommand>[1])
228
+ },
229
+ } satisfies CommandDefinition
@@ -0,0 +1,32 @@
1
+ import { K } from '@astrale-os/kernel-core'
2
+
3
+ import type { CommandDefinition } from '../command'
4
+ import type { KernelCommandOpts } from '../kernel'
5
+
6
+ import { runKernelCommand } from '../kernel'
7
+
8
+ export async function queryCommand(cypher: string, opts: KernelCommandOpts): Promise<void> {
9
+ await runKernelCommand({
10
+ opts,
11
+ label: 'Query',
12
+ fn: (ctx) => ctx.client.call(K.Root.query.path.method.raw, { cypher }),
13
+ })
14
+ }
15
+
16
+ export default {
17
+ name: 'query',
18
+ description: 'Run a read-only Cypher query against the kernel graph',
19
+ afterHelpText: `
20
+ Behavior:
21
+ Read-only. The kernel rejects write keywords (CREATE, DELETE, SET,
22
+ MERGE, REMOVE, DETACH); enforcement is kernel-side.
23
+
24
+ Examples:
25
+ $ astrale query 'MATCH (n) RETURN count(n) AS total'
26
+ $ astrale query 'MATCH (n:Domain) RETURN n.slug, n.id'
27
+ `,
28
+ arguments: [{ name: 'cypher', description: 'Cypher query string' }],
29
+ action: async (cypher, opts) => {
30
+ await queryCommand(cypher as string, opts as KernelCommandOpts)
31
+ },
32
+ } satisfies CommandDefinition
@@ -0,0 +1,54 @@
1
+ import type { CommandDefinition } from '../command'
2
+
3
+ import { ADMIN_TARGET_OPTIONS } from '../lib/admin-target'
4
+ import { fatal } from '../lib/log'
5
+ import { RAW_OUTPUT_OPTIONS } from '../lib/output'
6
+ import { runSetup, type SetupOpts } from '../setup/engine'
7
+
8
+ export default {
9
+ name: 'setup',
10
+ description: 'Guided first run: sign in, pick an instance, and equip your workspace',
11
+ arguments: [
12
+ {
13
+ name: 'slug',
14
+ description: 'Instance slug to provision when none is active',
15
+ required: false,
16
+ },
17
+ ],
18
+ options: [
19
+ { flags: '--plan', description: 'Print what setup would do (read-only) and exit' },
20
+ ...ADMIN_TARGET_OPTIONS,
21
+ ...RAW_OUTPUT_OPTIONS,
22
+ ],
23
+ afterHelpText: `
24
+ What it does:
25
+ Walks you from zero to a working instance, then offers to equip your agent:
26
+ 1. Connect sign in (WorkOS), confirm the admin control plane, pick or
27
+ provision an instance — ending on your live instance URL.
28
+ 2. Equip the astrale agent skills (cli + domain), agent-browser, and a
29
+ first domain (a pre-checked multi-select — toggle off any).
30
+
31
+ Idempotent: re-run it anytime; satisfied steps are skipped. Bare \`astrale\`
32
+ in a terminal launches this when you have no active instance yet.
33
+
34
+ Agents / CI:
35
+ Piped, --ci, or --plan → read-only. \`astrale setup --plan --json\` prints each
36
+ step's state and the exact command to fix it; run those granular commands
37
+ (auth login / instance create / …) rather than the interactive wizard.
38
+
39
+ Examples:
40
+ $ astrale setup # the guided flow
41
+ $ astrale setup my-app # pre-fill the instance slug
42
+ $ astrale setup --plan # what's left to do (no changes)
43
+ $ astrale setup --plan --json # machine-readable plan for an agent
44
+ `,
45
+ action: async (slug: string | undefined, opts: SetupOpts) => {
46
+ try {
47
+ await runSetup(opts, slug)
48
+ } catch (e) {
49
+ // fatal() exits 130 quietly on Ctrl-C (ExitPromptError); each step's
50
+ // writes are atomic + idempotent, so nothing half-applied matters.
51
+ fatal(e)
52
+ }
53
+ },
54
+ } satisfies CommandDefinition