@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.
- package/.check-workspace.cjs +40 -0
- package/README.md +151 -0
- package/dist/astrale.js +59749 -0
- package/package.json +90 -0
- package/src/command.ts +40 -0
- package/src/commands/__tests__/admin-instance.test.ts +73 -0
- package/src/commands/__tests__/auth-login.test.ts +178 -0
- package/src/commands/__tests__/auth-token.test.ts +223 -0
- package/src/commands/__tests__/call.test.ts +72 -0
- package/src/commands/__tests__/domain-list.test.ts +74 -0
- package/src/commands/__tests__/help-contract.test.ts +136 -0
- package/src/commands/__tests__/install-identity-override.test.ts +65 -0
- package/src/commands/__tests__/instance-bookmark.test.ts +101 -0
- package/src/commands/__tests__/instance-create-hosts.test.ts +29 -0
- package/src/commands/__tests__/instance-list-rows.test.ts +63 -0
- package/src/commands/__tests__/logs.test.ts +117 -0
- package/src/commands/__tests__/ls.test.ts +25 -0
- package/src/commands/__tests__/setup-plan.test.ts +61 -0
- package/src/commands/admin/status.ts +61 -0
- package/src/commands/admin/use.ts +77 -0
- package/src/commands/auth/login.ts +82 -0
- package/src/commands/auth/logout.ts +50 -0
- package/src/commands/auth/status.ts +86 -0
- package/src/commands/auth/token.ts +162 -0
- package/src/commands/browser.ts +207 -0
- package/src/commands/call.ts +300 -0
- package/src/commands/describe.ts +182 -0
- package/src/commands/domain/install.ts +420 -0
- package/src/commands/domain/list.ts +154 -0
- package/src/commands/domain/publish.ts +155 -0
- package/src/commands/get.ts +60 -0
- package/src/commands/identity/create.ts +26 -0
- package/src/commands/identity/delete.ts +18 -0
- package/src/commands/identity/export.ts +66 -0
- package/src/commands/identity/import.ts +101 -0
- package/src/commands/identity/list.ts +56 -0
- package/src/commands/identity/register.ts +170 -0
- package/src/commands/identity/sync.ts +32 -0
- package/src/commands/identity/unsync.ts +24 -0
- package/src/commands/identity/use.ts +18 -0
- package/src/commands/identity/whoami.ts +34 -0
- package/src/commands/idp/add.ts +150 -0
- package/src/commands/idp/list.ts +57 -0
- package/src/commands/idp/refresh.ts +38 -0
- package/src/commands/idp/remove.ts +36 -0
- package/src/commands/idp/show.ts +29 -0
- package/src/commands/instance/active.ts +64 -0
- package/src/commands/instance/bookmark.ts +72 -0
- package/src/commands/instance/create.ts +69 -0
- package/src/commands/instance/delete.ts +72 -0
- package/src/commands/instance/forget.ts +26 -0
- package/src/commands/instance/list.ts +149 -0
- package/src/commands/instance/status.ts +42 -0
- package/src/commands/instance/use.ts +210 -0
- package/src/commands/logs.ts +347 -0
- package/src/commands/ls.ts +229 -0
- package/src/commands/query.ts +32 -0
- package/src/commands/setup.ts +54 -0
- package/src/commands/status.ts +60 -0
- package/src/commands/studio.ts +401 -0
- package/src/commands/token.ts +77 -0
- package/src/commands/update.ts +267 -0
- package/src/commands/use.ts +87 -0
- package/src/errors.ts +65 -0
- package/src/kernel/__tests__/auth.test.ts +77 -0
- package/src/kernel/__tests__/errors.test.ts +43 -0
- package/src/kernel/__tests__/remote-routing.test.ts +70 -0
- package/src/kernel/auth.ts +234 -0
- package/src/kernel/ca-fetch.ts +119 -0
- package/src/kernel/client.ts +191 -0
- package/src/kernel/errors.ts +280 -0
- package/src/kernel/expand.ts +217 -0
- package/src/kernel/index.ts +14 -0
- package/src/kernel/options.ts +22 -0
- package/src/kernel/remote-routing.ts +88 -0
- package/src/kernel/run.ts +63 -0
- package/src/kernel/types.ts +14 -0
- package/src/lib/__tests__/admin-target.test.ts +112 -0
- package/src/lib/__tests__/binary.test.ts +56 -0
- package/src/lib/__tests__/command-dx.test.ts +58 -0
- package/src/lib/__tests__/concurrency.test.ts +62 -0
- package/src/lib/__tests__/config.test.ts +53 -0
- package/src/lib/__tests__/design.test.ts +99 -0
- package/src/lib/__tests__/domain-identity.test.ts +60 -0
- package/src/lib/__tests__/format.test.ts +22 -0
- package/src/lib/__tests__/fs-atomic.test.ts +104 -0
- package/src/lib/__tests__/identity.test.ts +79 -0
- package/src/lib/__tests__/idp-session.driver.ts +53 -0
- package/src/lib/__tests__/idp-session.test.ts +357 -0
- package/src/lib/__tests__/idp.test.ts +385 -0
- package/src/lib/__tests__/instance-candidates.test.ts +73 -0
- package/src/lib/__tests__/instance-target.test.ts +183 -0
- package/src/lib/__tests__/instance.test.ts +136 -0
- package/src/lib/__tests__/keys.test.ts +129 -0
- package/src/lib/__tests__/local-status.test.ts +202 -0
- package/src/lib/__tests__/output.test.ts +150 -0
- package/src/lib/__tests__/panel.test.ts +40 -0
- package/src/lib/__tests__/port.test.ts +44 -0
- package/src/lib/__tests__/prompt.test.ts +25 -0
- package/src/lib/__tests__/sdk-deps.test.ts +68 -0
- package/src/lib/__tests__/self.test.ts +272 -0
- package/src/lib/__tests__/studio-server-deps.test.ts +74 -0
- package/src/lib/__tests__/table.test.ts +53 -0
- package/src/lib/__tests__/update.test.ts +246 -0
- package/src/lib/__tests__/use-target.test.ts +56 -0
- package/src/lib/__tests__/validation.test.ts +34 -0
- package/src/lib/admin-domain.ts +25 -0
- package/src/lib/admin-instance.ts +26 -0
- package/src/lib/admin-target.ts +217 -0
- package/src/lib/binary.ts +131 -0
- package/src/lib/browser.ts +150 -0
- package/src/lib/command-dx.ts +161 -0
- package/src/lib/concurrency.ts +31 -0
- package/src/lib/config.ts +45 -0
- package/src/lib/domain-identity.ts +49 -0
- package/src/lib/env.ts +49 -0
- package/src/lib/format.ts +4 -0
- package/src/lib/fs-atomic.ts +126 -0
- package/src/lib/identity.ts +256 -0
- package/src/lib/idp-session.ts +134 -0
- package/src/lib/idp.ts +876 -0
- package/src/lib/instance-candidates.ts +49 -0
- package/src/lib/instance-target.ts +182 -0
- package/src/lib/instance.ts +395 -0
- package/src/lib/keys.ts +294 -0
- package/src/lib/local-status.ts +152 -0
- package/src/lib/log.ts +116 -0
- package/src/lib/login-flow.ts +164 -0
- package/src/lib/meta.ts +86 -0
- package/src/lib/output.ts +222 -0
- package/src/lib/panel.ts +61 -0
- package/src/lib/paths.ts +11 -0
- package/src/lib/port.ts +41 -0
- package/src/lib/proc.ts +82 -0
- package/src/lib/prompt.ts +136 -0
- package/src/lib/provision-instance.ts +170 -0
- package/src/lib/sdk-deps.ts +104 -0
- package/src/lib/self.ts +166 -0
- package/src/lib/skills.ts +171 -0
- package/src/lib/table.ts +62 -0
- package/src/lib/update.ts +315 -0
- package/src/lib/use-target.ts +24 -0
- package/src/lib/validation.ts +59 -0
- package/src/program.ts +200 -0
- package/src/registry.ts +59 -0
- package/src/setup/__tests__/util.test.ts +29 -0
- package/src/setup/engine.ts +83 -0
- package/src/setup/render.ts +109 -0
- package/src/setup/steps/admin.ts +78 -0
- package/src/setup/steps/agent-browser.ts +81 -0
- package/src/setup/steps/auth.ts +54 -0
- package/src/setup/steps/domain.ts +68 -0
- package/src/setup/steps/index.ts +18 -0
- package/src/setup/steps/instance.ts +119 -0
- package/src/setup/steps/skills-bridge.ts +70 -0
- package/src/setup/steps/skills.ts +59 -0
- package/src/setup/types.ts +61 -0
- package/src/setup/util.ts +34 -0
- package/src/test-utils.ts +18 -0
- package/studio/client/dist/assets/index-DOwzZAEK.css +1 -0
- package/studio/client/dist/assets/index-wtU0Zxhy.js +183 -0
- package/studio/client/dist/index.html +13 -0
- package/studio/package.json +62 -0
- package/studio/server/agent/ask.ts +68 -0
- package/studio/server/agent/bridge-mcp.ts +182 -0
- package/studio/server/agent/bridge.ts +188 -0
- package/studio/server/agent/claude.ts +666 -0
- package/studio/server/agent/mock.ts +186 -0
- package/studio/server/agent/prompt.ts +202 -0
- package/studio/server/agent/registry.ts +29 -0
- package/studio/server/agent/runner.ts +484 -0
- package/studio/server/agent/schema-map.ts +112 -0
- package/studio/server/agent/types.ts +120 -0
- package/studio/server/api.ts +574 -0
- package/studio/server/cache.ts +138 -0
- package/studio/server/detect.ts +81 -0
- package/studio/server/domain.ts +70 -0
- package/studio/server/index.ts +136 -0
- package/studio/server/introspect/anatomy-extras.ts +398 -0
- package/studio/server/introspect/anatomy.ts +108 -0
- package/studio/server/introspect/bundle.ts +57 -0
- package/studio/server/introspect/core-extractor.ts +119 -0
- package/studio/server/introspect/core.ts +44 -0
- package/studio/server/introspect/diff.ts +133 -0
- package/studio/server/introspect/extractor.ts +102 -0
- package/studio/server/introspect/hash.ts +21 -0
- package/studio/server/introspect/overlay-tsmorph.ts +874 -0
- package/studio/server/introspect/overlay.ts +57 -0
- package/studio/server/introspect/runtime.ts +99 -0
- package/studio/server/introspect/schema-refs.ts +46 -0
- package/studio/server/lifecycle.ts +38 -0
- package/studio/server/sse.ts +57 -0
- package/studio/server/state/baseline.ts +211 -0
- package/studio/server/state/catalog.ts +117 -0
- package/studio/server/state/comments.ts +321 -0
- package/studio/server/state/context.ts +167 -0
- package/studio/server/state/copy.ts +156 -0
- package/studio/server/state/create.ts +156 -0
- package/studio/server/state/documents.ts +70 -0
- package/studio/server/state/env.ts +161 -0
- package/studio/server/state/git.ts +75 -0
- package/studio/server/state/handoff.ts +55 -0
- package/studio/server/state/harness-gateway.ts +181 -0
- package/studio/server/state/harness-token.ts +0 -0
- package/studio/server/state/instance.ts +244 -0
- package/studio/server/state/integrations.ts +55 -0
- package/studio/server/state/layout.ts +55 -0
- package/studio/server/state/settings.ts +39 -0
- package/studio/server/state/store.ts +97 -0
- package/studio/server/state/updates.ts +63 -0
- package/studio/server/state/usage.ts +37 -0
- package/studio/server/state/views.ts +138 -0
- package/studio/server/state/visibility.ts +33 -0
- package/studio/server/watch.ts +81 -0
- package/studio/server/workspace-state.ts +26 -0
- package/studio/server/workspace-watch.ts +101 -0
- package/studio/shared/types.ts +873 -0
- package/studio/tsconfig.json +23 -0
- 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
|