@astrale-os/cli 0.5.0-alpha.0 → 0.6.1-alpha.0
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/dist/astrale.js +12962 -487
- package/package.json +7 -3
- package/src/command.ts +2 -0
- package/src/commands/__tests__/admin-instance.test.ts +3 -2
- package/src/commands/__tests__/domain-list.test.ts +6 -2
- package/src/commands/__tests__/install-identity-override.test.ts +1 -1
- package/src/commands/__tests__/view.test.ts +100 -0
- package/src/commands/call.ts +1 -1
- package/src/commands/domain/install.ts +5 -5
- package/src/commands/domain/list.ts +2 -2
- package/src/commands/domain/publish.ts +3 -3
- package/src/commands/instance/active.ts +2 -2
- package/src/commands/instance/create.ts +1 -1
- package/src/commands/instance/delete.ts +3 -3
- package/src/commands/instance/list.ts +8 -3
- package/src/commands/instance/status.ts +4 -3
- package/src/commands/instance/use.ts +2 -2
- package/src/commands/query.ts +27 -8
- package/src/commands/session/analyze.ts +50 -0
- package/src/commands/session/list.ts +36 -0
- package/src/commands/token.ts +1 -1
- package/src/commands/view-serve.ts +26 -0
- package/src/commands/view.ts +614 -0
- package/src/connect-core.test.ts +42 -0
- package/src/connect-core.ts +53 -0
- package/src/kernel/__tests__/auth.test.ts +1 -0
- package/src/kernel/client.ts +30 -24
- package/src/kernel/expand.ts +3 -2
- package/src/kernel/index.ts +7 -1
- package/src/kernel/options.ts +1 -1
- package/src/lib/__tests__/instance-target.test.ts +34 -0
- package/src/lib/__tests__/view-open-intent.test.ts +308 -0
- package/src/lib/__tests__/view-snapshot.test.ts +77 -0
- package/src/lib/admin-domain.ts +8 -4
- package/src/lib/admin-instance.ts +5 -1
- package/src/lib/config.ts +1 -0
- package/src/lib/instance-target.ts +5 -0
- package/src/lib/instance.ts +11 -0
- package/src/lib/log.ts +12 -2
- package/src/lib/login-flow.ts +41 -4
- package/src/lib/provision-instance.ts +2 -2
- package/src/lib/view/open-intent.ts +97 -0
- package/src/lib/view/resolve.ts +104 -0
- package/src/lib/view/server.ts +294 -0
- package/src/lib/view/session.ts +123 -0
- package/src/lib/view/snapshot.ts +101 -0
- package/src/program.ts +12 -1
- package/src/registry.ts +1 -1
- package/src/setup/steps/instance.ts +2 -2
- package/src/telemetry/__tests__/gate.test.ts +63 -0
- package/src/telemetry/__tests__/recorder.test.ts +110 -0
- package/src/telemetry/__tests__/redact.test.ts +79 -0
- package/src/telemetry/__tests__/session.test.ts +166 -0
- package/src/telemetry/__tests__/trigger.test.ts +49 -0
- package/src/telemetry/adapters/__tests__/claude-code.test.ts +89 -0
- package/src/telemetry/adapters/__tests__/codex.test.ts +138 -0
- package/src/telemetry/adapters/__tests__/index.test.ts +88 -0
- package/src/telemetry/adapters/claude-code.ts +90 -0
- package/src/telemetry/adapters/codex.ts +160 -0
- package/src/telemetry/adapters/index.ts +45 -0
- package/src/telemetry/adapters/types.ts +21 -0
- package/src/telemetry/analyze.ts +227 -0
- package/src/telemetry/gate.ts +79 -0
- package/src/telemetry/recorder.ts +57 -0
- package/src/telemetry/redact.ts +53 -0
- package/src/telemetry/session.ts +88 -0
- package/src/telemetry/settings.ts +26 -0
- package/src/telemetry/store.ts +91 -0
- package/src/telemetry/trigger.ts +119 -0
- package/src/telemetry/types.ts +64 -0
- package/studio/client/dist/assets/index-CyN5G8IA.js +109 -0
- package/studio/client/dist/assets/index-DKKMHBBC.css +1 -0
- package/studio/client/dist/index.html +2 -2
- package/studio/server/agent/ask.ts +2 -1
- package/studio/server/agent/runner.ts +4 -1
- package/studio/server/agent/session-id.ts +13 -0
- package/studio/server/api.ts +43 -25
- package/studio/server/cache.ts +17 -6
- package/studio/server/client-package.test.ts +147 -0
- package/studio/server/client-package.ts +242 -0
- package/studio/server/index.ts +13 -0
- package/studio/server/introspect/anatomy-extras.test.ts +91 -0
- package/studio/server/introspect/anatomy-extras.ts +48 -49
- package/studio/server/introspect/anatomy.ts +7 -7
- package/studio/server/introspect/overlay-tsmorph.test.ts +104 -0
- package/studio/server/introspect/overlay-tsmorph.ts +122 -22
- package/studio/server/state/views.test.ts +90 -0
- package/studio/server/state/views.ts +396 -99
- package/studio/server/state/visibility.ts +5 -5
- package/studio/server/view-dev-server.test.ts +111 -0
- package/studio/server/view-dev-server.ts +372 -0
- package/studio/shared/types.ts +57 -10
- package/viewer/dist/index.html +93 -0
- package/viewer/dist/main.js +71 -0
- package/studio/client/dist/assets/index-BcejyJpa.css +0 -1
- package/studio/client/dist/assets/index-Cqz3Oy_B.js +0 -179
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astrale-os/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1-alpha.0",
|
|
4
4
|
"description": "Astrale CLI — connect to existing Astrale kernels",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"astrale",
|
|
@@ -24,12 +24,14 @@
|
|
|
24
24
|
"studio/shared",
|
|
25
25
|
"studio/client/dist",
|
|
26
26
|
"studio/package.json",
|
|
27
|
+
"viewer/dist",
|
|
27
28
|
"THIRD-PARTY-NOTICES.md"
|
|
28
29
|
],
|
|
29
30
|
"type": "module",
|
|
30
31
|
"exports": {
|
|
31
32
|
"./keys": "./src/lib/keys.ts",
|
|
32
|
-
"./paths": "./src/lib/paths.ts"
|
|
33
|
+
"./paths": "./src/lib/paths.ts",
|
|
34
|
+
"./connect-core": "./src/connect-core.ts"
|
|
33
35
|
},
|
|
34
36
|
"publishConfig": {
|
|
35
37
|
"access": "public",
|
|
@@ -50,6 +52,7 @@
|
|
|
50
52
|
},
|
|
51
53
|
"devDependencies": {
|
|
52
54
|
"@astrale-os/ox": ">=0.1.0 <1.0.0",
|
|
55
|
+
"@astrale-os/shell": ">=0.3.5 <1.0.0",
|
|
53
56
|
"@astrale/commitlint-config": "npm:@jsr/astrale__commitlint-config@~2.0.1",
|
|
54
57
|
"@commitlint/cli": "~20.3.1",
|
|
55
58
|
"@commitlint/config-conventional": "~20.3.1",
|
|
@@ -58,6 +61,7 @@
|
|
|
58
61
|
"@typescript/native-preview": "latest",
|
|
59
62
|
"husky": "~9.1.7",
|
|
60
63
|
"lint-staged": "~16.2.7",
|
|
64
|
+
"msgpackr": "^1.12.1",
|
|
61
65
|
"oxfmt": "0.52.0",
|
|
62
66
|
"oxlint": "latest",
|
|
63
67
|
"typescript": "~6.0.1-rc"
|
|
@@ -77,7 +81,7 @@
|
|
|
77
81
|
"scripts": {
|
|
78
82
|
"preinstall": "node .check-workspace.cjs",
|
|
79
83
|
"build": "bun scripts/build.ts",
|
|
80
|
-
"typecheck": "tsgo --noEmit",
|
|
84
|
+
"typecheck": "tsgo --noEmit && tsgo --noEmit -p viewer",
|
|
81
85
|
"lint": "oxlint .",
|
|
82
86
|
"lint:fix": "oxlint --fix .",
|
|
83
87
|
"format": "pnpm exec oxfmt --write .",
|
package/src/command.ts
CHANGED
|
@@ -16,6 +16,8 @@ export type CommandDefinition = {
|
|
|
16
16
|
description: string
|
|
17
17
|
/** One-line summary shown in the parent's command list (Commander `.summary()`). */
|
|
18
18
|
summary?: string
|
|
19
|
+
/** Hide from help listings (internal entries, e.g. `__view-serve`). */
|
|
20
|
+
hidden?: boolean
|
|
19
21
|
aliases?: string[]
|
|
20
22
|
arguments?: CommandArgument[]
|
|
21
23
|
options?: CommandOption[]
|
|
@@ -3,12 +3,13 @@ import { describe, expect, test } from 'bun:test'
|
|
|
3
3
|
import type { IdentityStore } from '../../lib/identity'
|
|
4
4
|
|
|
5
5
|
import { AuthError } from '../../errors'
|
|
6
|
-
import { ADMIN_INSTANCE } from '../../lib/admin-instance'
|
|
6
|
+
import { ADMIN_INSTANCE, adminInstanceMethod } from '../../lib/admin-instance'
|
|
7
7
|
import { assertAlphaCreateIdentity } from '../instance/create'
|
|
8
8
|
|
|
9
9
|
describe('admin-backed instance commands', () => {
|
|
10
10
|
test('target the merged Instance class', () => {
|
|
11
|
-
expect(ADMIN_INSTANCE).toBe('
|
|
11
|
+
expect(ADMIN_INSTANCE).toBe('/:admin.astrale.ai:class.Instance')
|
|
12
|
+
expect(adminInstanceMethod('list')).toBe('/:admin.astrale.ai:class.Instance:list')
|
|
12
13
|
})
|
|
13
14
|
|
|
14
15
|
test('instance create preflight points fresh installs at WorkOS login', () => {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
2
|
|
|
3
|
-
import type
|
|
4
|
-
|
|
3
|
+
import { ADMIN_DOMAIN, adminDomainMethod, type DomainInfo } from '../../lib/admin-domain'
|
|
5
4
|
import { byDefaultThenName, domainProjection, type DomainRow } from '../domain/list'
|
|
6
5
|
|
|
7
6
|
const strip = (s: string): string => s.replace(/\[[0-9;]*m/g, '')
|
|
@@ -18,6 +17,11 @@ function entry(over: Partial<DomainInfo>): DomainInfo {
|
|
|
18
17
|
}
|
|
19
18
|
|
|
20
19
|
describe('domain list — ordering', () => {
|
|
20
|
+
test('uses the semantic static method path', () => {
|
|
21
|
+
expect(ADMIN_DOMAIN).toBe('/:admin.astrale.ai:class.DomainEntry')
|
|
22
|
+
expect(adminDomainMethod('list')).toBe('/:admin.astrale.ai:class.DomainEntry:list')
|
|
23
|
+
})
|
|
24
|
+
|
|
21
25
|
test('install-by-default sorts first, then alphabetical by origin', () => {
|
|
22
26
|
const rows = [
|
|
23
27
|
entry({ origin: 'zeta.astrale.ai' }),
|
|
@@ -23,7 +23,7 @@ describe('identity-override detection', () => {
|
|
|
23
23
|
|
|
24
24
|
test('origin differing from the serving host is an override', () => {
|
|
25
25
|
// The spec §5 attack shape: a fork on workers.dev claiming a well-known origin.
|
|
26
|
-
expect(isIdentityOverride('
|
|
26
|
+
expect(isIdentityOverride('shell.astrale.ai', 'crm.workers.dev')).toBe(true)
|
|
27
27
|
// The scaffold default also aliases until the placeholder origin is edited.
|
|
28
28
|
expect(isIdentityOverride('hldom.example.dev', 'hldom-example-dev.acme.workers.dev')).toBe(true)
|
|
29
29
|
})
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, mock, test } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
const getMock = mock(async (_path: string) => ({ id: 'issue-id' }))
|
|
4
|
+
const clientCallMock = mock(async (): Promise<unknown> => {
|
|
5
|
+
throw new Error('View:resolve must not run for a bare --view-url')
|
|
6
|
+
})
|
|
7
|
+
const abMock = mock(async (_args: string[]) => ({
|
|
8
|
+
ok: true,
|
|
9
|
+
data: { snapshot: '- button "Ready" [ref=e1]' },
|
|
10
|
+
error: null,
|
|
11
|
+
}))
|
|
12
|
+
|
|
13
|
+
mock.module('../../kernel', () => ({
|
|
14
|
+
bindGraph: () => ({ get: getMock }),
|
|
15
|
+
expandSelfInPath: async (path: string) => ({ path }),
|
|
16
|
+
resolveKernelTarget: mock(),
|
|
17
|
+
withKernelClient: async (
|
|
18
|
+
_opts: unknown,
|
|
19
|
+
run: (ctx: { client: { call: typeof clientCallMock } }) => Promise<unknown>,
|
|
20
|
+
) => run({ client: { call: clientCallMock } }),
|
|
21
|
+
}))
|
|
22
|
+
|
|
23
|
+
mock.module('../../lib/browser', () => ({
|
|
24
|
+
ab: abMock,
|
|
25
|
+
AGENT_BROWSER_REPO: 'vercel-labs/agent-browser',
|
|
26
|
+
BROWSER_DIR: '/tmp/astrale-browser',
|
|
27
|
+
findAgentBrowser: mock(async () => '/usr/bin/agent-browser'),
|
|
28
|
+
}))
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
getMock.mockClear()
|
|
32
|
+
clientCallMock.mockClear()
|
|
33
|
+
abMock.mockClear()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
describe('view session resolution', () => {
|
|
37
|
+
test('uses a bare --view-url target only as shell context', async () => {
|
|
38
|
+
const { resolveSession } = await import('../view')
|
|
39
|
+
const targetPath = '/users/user-1/issue-1'
|
|
40
|
+
|
|
41
|
+
const result = await resolveSession(undefined, {
|
|
42
|
+
viewUrl: 'http://127.0.0.1:5173/ui/issues',
|
|
43
|
+
handshake: 'shell',
|
|
44
|
+
target: targetPath,
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
expect(getMock).toHaveBeenCalledWith(targetPath)
|
|
48
|
+
expect(clientCallMock).not.toHaveBeenCalled()
|
|
49
|
+
expect(result).toEqual({
|
|
50
|
+
view: {
|
|
51
|
+
url: 'http://127.0.0.1:5173/ui/issues',
|
|
52
|
+
functionId: 'dev-view',
|
|
53
|
+
handshake: 'shell',
|
|
54
|
+
name: 'dev',
|
|
55
|
+
},
|
|
56
|
+
target: { id: 'issue-id', path: targetPath },
|
|
57
|
+
candidates: [],
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('lists every candidate without requiring a view selection', async () => {
|
|
62
|
+
const { resolveSession } = await import('../view')
|
|
63
|
+
const targetPath = '/domains/ai-gateway.astrale.ai/core/gemini-3-5-flash'
|
|
64
|
+
const candidates = [
|
|
65
|
+
{
|
|
66
|
+
id: 'chat-id',
|
|
67
|
+
path: '/domains/ai-gateway.astrale.ai/views/chat',
|
|
68
|
+
url: 'https://ai-gateway.astrale.ai/ui/chat',
|
|
69
|
+
origin: 'class' as const,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: 'model-id',
|
|
73
|
+
path: '/domains/ai-gateway.astrale.ai/views/model',
|
|
74
|
+
url: 'https://ai-gateway.astrale.ai/ui/model',
|
|
75
|
+
origin: 'class' as const,
|
|
76
|
+
},
|
|
77
|
+
]
|
|
78
|
+
clientCallMock.mockImplementationOnce(async () => candidates)
|
|
79
|
+
|
|
80
|
+
const result = await resolveSession(targetPath, { list: true })
|
|
81
|
+
|
|
82
|
+
expect(result).toEqual({
|
|
83
|
+
target: { id: 'issue-id', path: targetPath },
|
|
84
|
+
candidates,
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
describe('view capture timing', () => {
|
|
90
|
+
test('settles the view before taking a screenshot', async () => {
|
|
91
|
+
const { runSnapshotExtras } = await import('../view')
|
|
92
|
+
|
|
93
|
+
await runSnapshotExtras({ screenshot: '/tmp/view.png' })
|
|
94
|
+
|
|
95
|
+
const commands = abMock.mock.calls.map(([args]) => args[0])
|
|
96
|
+
expect(commands.at(-1)).toBe('screenshot')
|
|
97
|
+
expect(commands.slice(0, -1).every((command) => command === 'snapshot')).toBe(true)
|
|
98
|
+
expect(commands.length).toBeGreaterThan(2)
|
|
99
|
+
})
|
|
100
|
+
})
|
package/src/commands/call.ts
CHANGED
|
@@ -261,7 +261,7 @@ Examples:
|
|
|
261
261
|
$ astrale call /:host.astrale.ai:class.KernelInstance:list
|
|
262
262
|
$ astrale call /:blog.acme.com:class.Author:list limit=10
|
|
263
263
|
$ astrale call '@self::deactivate'
|
|
264
|
-
$ astrale call /:
|
|
264
|
+
$ astrale call /:shell.astrale.ai:class.Application:available --json
|
|
265
265
|
`,
|
|
266
266
|
arguments: [
|
|
267
267
|
{
|
|
@@ -7,8 +7,8 @@ import type { KernelCommandOpts } from '../../kernel'
|
|
|
7
7
|
import { AstraleError } from '../../errors'
|
|
8
8
|
import { runKernelCommand } from '../../kernel'
|
|
9
9
|
import { withAdminKernelClient } from '../../kernel/client'
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { adminDomainMethod, type DomainInfo } from '../../lib/admin-domain'
|
|
11
|
+
import { adminInstanceMethod, type InstanceInfo } from '../../lib/admin-instance'
|
|
12
12
|
import { ADMIN_TARGET_OPTIONS, type AdminTargetCommandOpts } from '../../lib/admin-target'
|
|
13
13
|
import { getActive } from '../../lib/instance'
|
|
14
14
|
import { fatal, log, withSpinner } from '../../lib/log'
|
|
@@ -125,7 +125,7 @@ async function installViaAdmin(target: string | undefined, opts: InstallOpts): P
|
|
|
125
125
|
|
|
126
126
|
try {
|
|
127
127
|
await withAdminKernelClient(adminOpts, async (ctx) => {
|
|
128
|
-
const instances = (await ctx.client.call(
|
|
128
|
+
const instances = (await ctx.client.call(adminInstanceMethod('list'), {})) as InstanceInfo[]
|
|
129
129
|
|
|
130
130
|
const ref = await resolveDomainRef(ctx, target, interactive)
|
|
131
131
|
const slug = await resolveTargetSlug(opts, target, interactive, instances)
|
|
@@ -145,7 +145,7 @@ async function installViaAdmin(target: string | undefined, opts: InstallOpts): P
|
|
|
145
145
|
`Installing ${label} on ${match.slug}`,
|
|
146
146
|
!isMachine(opts),
|
|
147
147
|
() =>
|
|
148
|
-
ctx.client.call(
|
|
148
|
+
ctx.client.call(adminDomainMethod('install'), {
|
|
149
149
|
instanceId: match.slug,
|
|
150
150
|
...ref,
|
|
151
151
|
}) as Promise<DomainInstallResult>,
|
|
@@ -190,7 +190,7 @@ async function resolveDomainRef(
|
|
|
190
190
|
if (target) return domainRefFromTarget(target)
|
|
191
191
|
if (!interactive) throw new AstraleError('MISSING_ARG', 'No domain given.')
|
|
192
192
|
|
|
193
|
-
const catalog = (await ctx.client.call(
|
|
193
|
+
const catalog = (await ctx.client.call(adminDomainMethod('list'), {})) as DomainInfo[]
|
|
194
194
|
const installable = catalog.filter((d) => d.url)
|
|
195
195
|
if (installable.length === 0) {
|
|
196
196
|
throw new AstraleError(
|
|
@@ -5,7 +5,7 @@ import type { KernelCommandOpts } from '../../kernel'
|
|
|
5
5
|
import type { ListProjection, RawOutputOpts } from '../../lib/output'
|
|
6
6
|
|
|
7
7
|
import { withAdminKernelClient } from '../../kernel/client'
|
|
8
|
-
import {
|
|
8
|
+
import { adminDomainMethod, type DomainInfo } from '../../lib/admin-domain'
|
|
9
9
|
import { ADMIN_TARGET_OPTIONS, type AdminTargetCommandOpts } from '../../lib/admin-target'
|
|
10
10
|
import { fatal, withSpinner } from '../../lib/log'
|
|
11
11
|
import { isMachine, presentList } from '../../lib/output'
|
|
@@ -102,7 +102,7 @@ Examples:
|
|
|
102
102
|
async (): Promise<DomainRow[]> => {
|
|
103
103
|
const list = await withAdminKernelClient(
|
|
104
104
|
opts,
|
|
105
|
-
async (ctx) => (await ctx.client.call(
|
|
105
|
+
async (ctx) => (await ctx.client.call(adminDomainMethod('list'), {})) as DomainInfo[],
|
|
106
106
|
)
|
|
107
107
|
const filtered = opts.defaultOnly ? list.filter((d) => d.installByDefault) : list
|
|
108
108
|
filtered.sort(byDefaultThenName)
|
|
@@ -4,7 +4,7 @@ import type { CommandDefinition } from '../../command'
|
|
|
4
4
|
import type { KernelCommandOpts } from '../../kernel'
|
|
5
5
|
|
|
6
6
|
import { withAdminKernelClient } from '../../kernel/client'
|
|
7
|
-
import {
|
|
7
|
+
import { adminDomainMethod, type DomainInfo } from '../../lib/admin-domain'
|
|
8
8
|
import { ADMIN_TARGET_OPTIONS, type AdminTargetCommandOpts } from '../../lib/admin-target'
|
|
9
9
|
import { fatal, withSpinner } from '../../lib/log'
|
|
10
10
|
import { isMachine, output } from '../../lib/output'
|
|
@@ -115,7 +115,7 @@ Examples:
|
|
|
115
115
|
// silently bumping `updatedAt`. `info` throws when absent → treat as
|
|
116
116
|
// a fresh publish.
|
|
117
117
|
const existing = (await ctx.client
|
|
118
|
-
.call(
|
|
118
|
+
.call(adminDomainMethod('info'), { origin })
|
|
119
119
|
.catch(() => null)) as DomainInfo | null
|
|
120
120
|
if (
|
|
121
121
|
existing &&
|
|
@@ -127,7 +127,7 @@ Examples:
|
|
|
127
127
|
) {
|
|
128
128
|
return { entry: existing, changed: false as const, isNew: false }
|
|
129
129
|
}
|
|
130
|
-
const entry = (await ctx.client.call(
|
|
130
|
+
const entry = (await ctx.client.call(adminDomainMethod('publish'), {
|
|
131
131
|
origin,
|
|
132
132
|
name,
|
|
133
133
|
url: publicUrl,
|
|
@@ -3,7 +3,7 @@ import chalk from 'chalk'
|
|
|
3
3
|
import type { CommandDefinition } from '../../command'
|
|
4
4
|
|
|
5
5
|
import { withAdminKernelClient } from '../../kernel/client'
|
|
6
|
-
import {
|
|
6
|
+
import { adminInstanceMethod, type InstanceInfo } from '../../lib/admin-instance'
|
|
7
7
|
import { getActive, normalizeInstanceKernelUrl } from '../../lib/instance'
|
|
8
8
|
import { log } from '../../lib/log'
|
|
9
9
|
import { RAW_OUTPUT_OPTIONS, isMachine, output, type RawOutputOpts } from '../../lib/output'
|
|
@@ -51,7 +51,7 @@ async function resolveActiveForDisplay(): Promise<{
|
|
|
51
51
|
const managed = await withAdminKernelClient(
|
|
52
52
|
{},
|
|
53
53
|
async (ctx) =>
|
|
54
|
-
(await ctx.client.call(
|
|
54
|
+
(await ctx.client.call(adminInstanceMethod('info'), { id: active.name })) as InstanceInfo,
|
|
55
55
|
)
|
|
56
56
|
return {
|
|
57
57
|
name: managed.slug,
|
|
@@ -2,7 +2,7 @@ import type { CommandDefinition } from '../../command'
|
|
|
2
2
|
import type { KernelCommandOpts } from '../../kernel'
|
|
3
3
|
|
|
4
4
|
import { withAdminKernelClient } from '../../kernel/client'
|
|
5
|
-
import {
|
|
5
|
+
import { adminInstanceMethod, type InstanceInfo } from '../../lib/admin-instance'
|
|
6
6
|
import { ADMIN_TARGET_OPTIONS, type AdminTargetCommandOpts } from '../../lib/admin-target'
|
|
7
7
|
import { clearActive, readInstances, removeInstance, resolveInstanceKey } from '../../lib/instance'
|
|
8
8
|
import { fatal, log, withSpinner } from '../../lib/log'
|
|
@@ -40,7 +40,7 @@ Behavior:
|
|
|
40
40
|
withAdminKernelClient(
|
|
41
41
|
opts,
|
|
42
42
|
async (ctx) =>
|
|
43
|
-
(await ctx.client.call(
|
|
43
|
+
(await ctx.client.call(adminInstanceMethod('delete'), { id })) as InstanceInfo,
|
|
44
44
|
),
|
|
45
45
|
{ success: (deleted) => `Deleted instance: ${deleted.slug}` },
|
|
46
46
|
)
|
|
@@ -66,7 +66,7 @@ Behavior:
|
|
|
66
66
|
log.dim(` hint: it's a local bookmark — drop it with: astrale instance forget ${id}`)
|
|
67
67
|
process.exit(1)
|
|
68
68
|
}
|
|
69
|
-
fatal(e)
|
|
69
|
+
fatal(e, opts)
|
|
70
70
|
}
|
|
71
71
|
},
|
|
72
72
|
} satisfies CommandDefinition
|
|
@@ -5,7 +5,11 @@ import type { KernelCommandOpts } from '../../kernel'
|
|
|
5
5
|
import type { Column } from '../../lib/output'
|
|
6
6
|
|
|
7
7
|
import { withAdminKernelClient } from '../../kernel/client'
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
adminInstanceMethod,
|
|
10
|
+
formatInstanceLocation,
|
|
11
|
+
type InstanceInfo,
|
|
12
|
+
} from '../../lib/admin-instance'
|
|
9
13
|
import { ADMIN_TARGET_OPTIONS, type AdminTargetCommandOpts } from '../../lib/admin-target'
|
|
10
14
|
import { normalizeInstanceKernelUrl, readInstances } from '../../lib/instance'
|
|
11
15
|
import { fatal, log, withSpinner } from '../../lib/log'
|
|
@@ -60,7 +64,8 @@ export default {
|
|
|
60
64
|
managed = await withSpinner('Fetching instances', !isMachine(opts), () =>
|
|
61
65
|
withAdminKernelClient(
|
|
62
66
|
opts,
|
|
63
|
-
async (ctx) =>
|
|
67
|
+
async (ctx) =>
|
|
68
|
+
(await ctx.client.call(adminInstanceMethod('list'), {})) as InstanceInfo[],
|
|
64
69
|
),
|
|
65
70
|
)
|
|
66
71
|
}
|
|
@@ -87,7 +92,7 @@ export default {
|
|
|
87
92
|
}
|
|
88
93
|
process.stdout.write(renderTable(rows, { columns: COLUMNS }) + '\n')
|
|
89
94
|
} catch (e) {
|
|
90
|
-
fatal(e)
|
|
95
|
+
fatal(e, opts)
|
|
91
96
|
}
|
|
92
97
|
},
|
|
93
98
|
} satisfies CommandDefinition
|
|
@@ -4,7 +4,7 @@ import type { CommandDefinition } from '../../command'
|
|
|
4
4
|
import type { KernelCommandOpts } from '../../kernel'
|
|
5
5
|
|
|
6
6
|
import { withAdminKernelClient } from '../../kernel/client'
|
|
7
|
-
import {
|
|
7
|
+
import { adminInstanceMethod, type InstanceInfo } from '../../lib/admin-instance'
|
|
8
8
|
import { ADMIN_TARGET_OPTIONS, type AdminTargetCommandOpts } from '../../lib/admin-target'
|
|
9
9
|
import { fatal, log, withSpinner } from '../../lib/log'
|
|
10
10
|
import { isMachine, output, type RawOutputOpts } from '../../lib/output'
|
|
@@ -21,7 +21,8 @@ export default {
|
|
|
21
21
|
const result = await withSpinner(`Fetching instance ${id}`, !isMachine(opts), () =>
|
|
22
22
|
withAdminKernelClient(
|
|
23
23
|
opts,
|
|
24
|
-
async (ctx) =>
|
|
24
|
+
async (ctx) =>
|
|
25
|
+
(await ctx.client.call(adminInstanceMethod('info'), { id })) as InstanceInfo,
|
|
25
26
|
),
|
|
26
27
|
)
|
|
27
28
|
if (isMachine(opts)) {
|
|
@@ -36,7 +37,7 @@ export default {
|
|
|
36
37
|
if (result.hostId) log.dim(` host: ${result.hostId}`)
|
|
37
38
|
if (result.createdAt) log.dim(` created: ${result.createdAt}`)
|
|
38
39
|
} catch (e) {
|
|
39
|
-
fatal(e)
|
|
40
|
+
fatal(e, opts)
|
|
40
41
|
}
|
|
41
42
|
},
|
|
42
43
|
} satisfies CommandDefinition
|
|
@@ -2,7 +2,7 @@ import type { CommandDefinition } from '../../command'
|
|
|
2
2
|
|
|
3
3
|
import { AstraleError } from '../../errors'
|
|
4
4
|
import { withAdminKernelClient } from '../../kernel/client'
|
|
5
|
-
import {
|
|
5
|
+
import { adminInstanceMethod, type InstanceInfo } from '../../lib/admin-instance'
|
|
6
6
|
import { ADMIN_TARGET_OPTIONS } from '../../lib/admin-target'
|
|
7
7
|
import { getDefault, setDefault } from '../../lib/identity'
|
|
8
8
|
import {
|
|
@@ -142,7 +142,7 @@ async function fetchManagedInstances(name: string, opts: UseOpts): Promise<Insta
|
|
|
142
142
|
try {
|
|
143
143
|
return await withAdminKernelClient(
|
|
144
144
|
opts,
|
|
145
|
-
async (ctx) => (await ctx.client.call(
|
|
145
|
+
async (ctx) => (await ctx.client.call(adminInstanceMethod('list'), {})) as InstanceInfo[],
|
|
146
146
|
)
|
|
147
147
|
} catch {
|
|
148
148
|
return []
|
package/src/commands/query.ts
CHANGED
|
@@ -46,8 +46,7 @@ export async function queryCommand(paths: string[], opts: QueryOpts): Promise<vo
|
|
|
46
46
|
label: mode.kind === 'roots' ? 'Query ' + mode.roots.join(' ') : 'Query',
|
|
47
47
|
fn: async (ctx) => {
|
|
48
48
|
const read = () => bindGraph(ctx).query(mode.kind === 'roots' ? mode.query.ast : mode.ast)
|
|
49
|
-
const result =
|
|
50
|
-
mode.kind === 'roots' ? await withSelfHint(read, mode.meta) : await read()
|
|
49
|
+
const result = mode.kind === 'roots' ? await withSelfHint(read, mode.meta) : await read()
|
|
51
50
|
return result.wire
|
|
52
51
|
},
|
|
53
52
|
format: (result, fmtOpts) => {
|
|
@@ -62,7 +61,8 @@ async function parseMode(paths: string[], opts: QueryOpts): Promise<QueryMode> {
|
|
|
62
61
|
const hasRoots = rootsInput.length > 0
|
|
63
62
|
const hasAst = opts.ast !== undefined
|
|
64
63
|
const hasCypher = opts.cypher !== undefined
|
|
65
|
-
const hasSelectors =
|
|
64
|
+
const hasSelectors =
|
|
65
|
+
opts.depth !== undefined || opts.children !== undefined || opts.edges !== undefined
|
|
66
66
|
|
|
67
67
|
if (hasCypher) {
|
|
68
68
|
if (hasAst || hasRoots || hasSelectors) {
|
|
@@ -215,7 +215,9 @@ function parseSelector<T>(flag: string, raw: string, schema: SelectorSchema): T
|
|
|
215
215
|
throw new Error(flag + ' must be JSON: ' + raw)
|
|
216
216
|
}
|
|
217
217
|
if (parsed === null || typeof parsed !== 'object') {
|
|
218
|
-
throw new Error(
|
|
218
|
+
throw new Error(
|
|
219
|
+
flag + ' must be a JSON selector object' + (flag === '--edges' ? ' or array' : ''),
|
|
220
|
+
)
|
|
219
221
|
}
|
|
220
222
|
const check = schema.safeParse(parsed)
|
|
221
223
|
if (!check.success) {
|
|
@@ -232,11 +234,21 @@ function printCursorFooter(next: NonNullable<GetResultWire['next']>): void {
|
|
|
232
234
|
if (entries.length === 1) {
|
|
233
235
|
const cursors = entries[0]?.[1]
|
|
234
236
|
if (cursors?.children) {
|
|
235
|
-
process.stdout.write(
|
|
237
|
+
process.stdout.write(
|
|
238
|
+
chalk.dim(' more: --children \'{"cursor":"' + cursors.children + '"}\'\n'),
|
|
239
|
+
)
|
|
236
240
|
}
|
|
237
241
|
for (const [alias, cursor] of Object.entries(cursors?.edges ?? {})) {
|
|
238
242
|
process.stdout.write(
|
|
239
|
-
chalk.dim(
|
|
243
|
+
chalk.dim(
|
|
244
|
+
' more edges[' +
|
|
245
|
+
alias +
|
|
246
|
+
']: --edges \'{"as":"' +
|
|
247
|
+
alias +
|
|
248
|
+
'","cursor":"' +
|
|
249
|
+
cursor +
|
|
250
|
+
'"}\'\n',
|
|
251
|
+
),
|
|
240
252
|
)
|
|
241
253
|
}
|
|
242
254
|
return
|
|
@@ -279,7 +291,11 @@ export default {
|
|
|
279
291
|
'',
|
|
280
292
|
].join('\n'),
|
|
281
293
|
arguments: [
|
|
282
|
-
{
|
|
294
|
+
{
|
|
295
|
+
name: 'paths...',
|
|
296
|
+
description: 'One or more root paths (/domain/Class) or IDs (@nodeId)',
|
|
297
|
+
required: false,
|
|
298
|
+
},
|
|
283
299
|
],
|
|
284
300
|
options: [
|
|
285
301
|
{ flags: '--depth <n>', description: 'Subtree depth to fetch (0-5, default 0)' },
|
|
@@ -291,7 +307,10 @@ export default {
|
|
|
291
307
|
flags: '--edges <json>',
|
|
292
308
|
description: 'Edge selector (or JSON array of selectors) to include',
|
|
293
309
|
},
|
|
294
|
-
{
|
|
310
|
+
{
|
|
311
|
+
flags: '--ast <json>',
|
|
312
|
+
description: 'Raw QueryASTInput JSON object (experimental, unstable shape)',
|
|
313
|
+
},
|
|
295
314
|
{ flags: '--cypher <query>', description: 'Read-only Cypher escape hatch' },
|
|
296
315
|
],
|
|
297
316
|
action: async (paths, opts) => {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
|
|
3
|
+
import type { CommandDefinition } from '../../command'
|
|
4
|
+
|
|
5
|
+
import { log } from '../../lib/log'
|
|
6
|
+
import { analyzeSession } from '../../telemetry/analyze'
|
|
7
|
+
import { listSessions } from '../../telemetry/store'
|
|
8
|
+
import { restampLock, releaseLock } from '../../telemetry/trigger'
|
|
9
|
+
|
|
10
|
+
export default {
|
|
11
|
+
name: 'analyze',
|
|
12
|
+
description: 'Analyze a recorded session for DX frictions (dry-run: writes report.md)',
|
|
13
|
+
arguments: [{ name: 'id', description: 'Session id (default: most recent closed, unanalyzed)' }],
|
|
14
|
+
options: [
|
|
15
|
+
{ flags: '--file', description: 'File cleared findings as issues on the admin tracker' },
|
|
16
|
+
{ flags: '--model <model>', description: 'Model for the analyzer pass' },
|
|
17
|
+
{ flags: '--force', description: 'Re-analyze even if already analyzed' },
|
|
18
|
+
{ flags: '--auto', description: 'Triggered mode: quiet, used by the opportunistic trigger' },
|
|
19
|
+
],
|
|
20
|
+
action: async (
|
|
21
|
+
id: string | undefined,
|
|
22
|
+
opts: { file?: boolean; model?: string; force?: boolean; auto?: boolean },
|
|
23
|
+
) => {
|
|
24
|
+
if (opts.auto) restampLock()
|
|
25
|
+
try {
|
|
26
|
+
let target = id
|
|
27
|
+
if (!target) {
|
|
28
|
+
const candidate = listSessions().find((s) => s.closed && (opts.force || !s.analyzed))
|
|
29
|
+
if (!candidate) {
|
|
30
|
+
if (!opts.auto) log.dim('Nothing to analyze — no closed, unanalyzed session.')
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
target = candidate.id
|
|
34
|
+
}
|
|
35
|
+
if (!opts.auto) log.step(`analyzing session ${target} …`)
|
|
36
|
+
const outcome = await analyzeSession(target, opts)
|
|
37
|
+
if (opts.auto) return
|
|
38
|
+
if (outcome.outcome === 'skipped-quiet') {
|
|
39
|
+
log.dim(`quiet session (${outcome.note}) — nothing to analyze`)
|
|
40
|
+
} else if (outcome.outcome === 'error') {
|
|
41
|
+
log.error(`analyzer failed: ${outcome.note ?? 'unknown'}`)
|
|
42
|
+
} else {
|
|
43
|
+
log.success(`${outcome.outcome} (${outcome.note ?? ''})`)
|
|
44
|
+
if (outcome.reportPath) console.log(chalk.dim(` ${outcome.reportPath}`))
|
|
45
|
+
}
|
|
46
|
+
} finally {
|
|
47
|
+
if (opts.auto) releaseLock()
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
} satisfies CommandDefinition
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
|
|
3
|
+
import type { CommandDefinition } from '../../command'
|
|
4
|
+
|
|
5
|
+
import { log } from '../../lib/log'
|
|
6
|
+
import { isMachine, output, RAW_OUTPUT_OPTIONS } from '../../lib/output'
|
|
7
|
+
import { listSessions } from '../../telemetry/store'
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
name: 'list',
|
|
11
|
+
description: 'List locally recorded work sessions and their analysis state',
|
|
12
|
+
options: [...RAW_OUTPUT_OPTIONS],
|
|
13
|
+
action: async (opts: { raw?: boolean; json?: boolean }) => {
|
|
14
|
+
const sessions = listSessions()
|
|
15
|
+
if (isMachine(opts)) {
|
|
16
|
+
output(sessions, opts)
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
if (sessions.length === 0) {
|
|
20
|
+
log.dim('No sessions recorded yet. Run any astrale command in a workspace to start one.')
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
for (const s of sessions) {
|
|
24
|
+
const state = s.analyzed
|
|
25
|
+
? s.analyzed.outcome === 'error'
|
|
26
|
+
? chalk.red(s.analyzed.outcome)
|
|
27
|
+
: chalk.green(s.analyzed.outcome)
|
|
28
|
+
: s.closed
|
|
29
|
+
? chalk.yellow('closed, unanalyzed')
|
|
30
|
+
: chalk.cyan('open')
|
|
31
|
+
const when = s.lastEventAt ? s.lastEventAt.toISOString().slice(0, 16) : 'no events'
|
|
32
|
+
console.log(`${chalk.bold(s.id)} ${state}`)
|
|
33
|
+
console.log(chalk.dim(` ${s.meta?.root ?? '(unknown root)'} — last activity ${when}`))
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
} satisfies CommandDefinition
|
package/src/commands/token.ts
CHANGED
|
@@ -60,7 +60,7 @@ What this token is FOR — worker-direct HTTP calls:
|
|
|
60
60
|
use 'astrale call' (which signs per-call) instead.
|
|
61
61
|
|
|
62
62
|
Examples:
|
|
63
|
-
$ export TOKEN=$(astrale token --audience
|
|
63
|
+
$ export TOKEN=$(astrale token --audience shell.astrale.ai --raw)
|
|
64
64
|
$ astrale token --audience worker.example.com --for alice -i staging
|
|
65
65
|
`,
|
|
66
66
|
options: [
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
|
|
3
|
+
import type { CommandDefinition } from '../command'
|
|
4
|
+
import type { ViewServeConfig } from '../lib/view/session'
|
|
5
|
+
|
|
6
|
+
import { startViewServer } from '../lib/view/server'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Internal (hidden) entry for the detached view-session server: `astrale view`
|
|
10
|
+
* re-invokes the CLI with `__view-serve --config <file>` so the server runs
|
|
11
|
+
* under the same runtime and codebase as the CLI itself.
|
|
12
|
+
*/
|
|
13
|
+
export default {
|
|
14
|
+
name: '__view-serve',
|
|
15
|
+
hidden: true,
|
|
16
|
+
description: 'Internal: run a view-session server from a config file',
|
|
17
|
+
options: [{ flags: '--config <path>', description: 'ViewServeConfig JSON path' }],
|
|
18
|
+
action: async (opts: { config?: string }) => {
|
|
19
|
+
if (!opts.config) throw new Error('--config is required')
|
|
20
|
+
const config = JSON.parse(await readFile(opts.config, 'utf8')) as ViewServeConfig
|
|
21
|
+
startViewServer(config)
|
|
22
|
+
console.log(`view session ${config.session.id} listening on ${config.session.pageUrl}`)
|
|
23
|
+
// Keep the process alive; startViewServer owns shutdown (signals + idle).
|
|
24
|
+
await new Promise(() => {})
|
|
25
|
+
},
|
|
26
|
+
} satisfies CommandDefinition
|