@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,72 @@
|
|
|
1
|
+
import type { CommandDefinition } from '../../command'
|
|
2
|
+
import type { KernelCommandOpts } from '../../kernel'
|
|
3
|
+
|
|
4
|
+
import { withAdminKernelClient } from '../../kernel/client'
|
|
5
|
+
import { ADMIN_INSTANCE, type InstanceInfo } from '../../lib/admin-instance'
|
|
6
|
+
import { ADMIN_TARGET_OPTIONS, type AdminTargetCommandOpts } from '../../lib/admin-target'
|
|
7
|
+
import { clearActive, readInstances, removeInstance, resolveInstanceKey } from '../../lib/instance'
|
|
8
|
+
import { fatal, log, withSpinner } from '../../lib/log'
|
|
9
|
+
import { isMachine, output } from '../../lib/output'
|
|
10
|
+
|
|
11
|
+
type DeleteOpts = KernelCommandOpts &
|
|
12
|
+
AdminTargetCommandOpts & {
|
|
13
|
+
keepBookmark?: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default {
|
|
17
|
+
name: 'delete',
|
|
18
|
+
description: 'Delete an admin-managed instance through the admin kernel',
|
|
19
|
+
afterHelpText: `
|
|
20
|
+
Behavior:
|
|
21
|
+
Calls Instance.delete on the configured admin kernel: best-effort decommission
|
|
22
|
+
of the instance's routing-registry entry + WorkOS org (frees the slug), then
|
|
23
|
+
removes the Instance record. The host kernel itself is NOT deregistered (the
|
|
24
|
+
deprovisioning saga is a follow-up). A same-name local bookmark is removed
|
|
25
|
+
after the delete succeeds unless --keep-bookmark is passed.
|
|
26
|
+
`,
|
|
27
|
+
arguments: [{ name: 'id', description: 'Instance slug', required: true }],
|
|
28
|
+
options: [
|
|
29
|
+
...ADMIN_TARGET_OPTIONS,
|
|
30
|
+
{ flags: '--keep-bookmark', description: 'Do not remove a same-name local bookmark' },
|
|
31
|
+
],
|
|
32
|
+
action: async (id: string, opts: DeleteOpts) => {
|
|
33
|
+
// Teardown walks services + deprovisions — same saga-sized budget as create.
|
|
34
|
+
opts = { ...opts, timeout: opts.timeout ?? '240000' }
|
|
35
|
+
try {
|
|
36
|
+
const result = await withSpinner(
|
|
37
|
+
`Deleting instance ${id}`,
|
|
38
|
+
!isMachine(opts),
|
|
39
|
+
() =>
|
|
40
|
+
withAdminKernelClient(
|
|
41
|
+
opts,
|
|
42
|
+
async (ctx) =>
|
|
43
|
+
(await ctx.client.call(`${ADMIN_INSTANCE}/delete`, { id })) as InstanceInfo,
|
|
44
|
+
),
|
|
45
|
+
{ success: (deleted) => `Deleted instance: ${deleted.slug}` },
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
if (!opts.keepBookmark) {
|
|
49
|
+
const store = await readInstances()
|
|
50
|
+
const key = resolveInstanceKey(store, id)
|
|
51
|
+
if (key) await removeInstance(key)
|
|
52
|
+
}
|
|
53
|
+
await clearActive(id)
|
|
54
|
+
|
|
55
|
+
if (isMachine(opts)) {
|
|
56
|
+
output(result, opts)
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
} catch (e) {
|
|
60
|
+
// Bookmark-only id → no admin Instance → kernel NOT_FOUND. Point to
|
|
61
|
+
// `forget` (local drop) instead of surfacing the raw path error.
|
|
62
|
+
const notManaged =
|
|
63
|
+
e instanceof Error && (e.name === 'NotFoundError' || e.message.startsWith('Path not found'))
|
|
64
|
+
if (notManaged && resolveInstanceKey(await readInstances(), id)) {
|
|
65
|
+
log.error(`No admin-managed instance "${id}".`)
|
|
66
|
+
log.dim(` hint: it's a local bookmark — drop it with: astrale instance forget ${id}`)
|
|
67
|
+
process.exit(1)
|
|
68
|
+
}
|
|
69
|
+
fatal(e)
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
} satisfies CommandDefinition
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { CommandDefinition } from '../../command'
|
|
2
|
+
|
|
3
|
+
import { readInstances, removeInstance, resolveInstanceKey } from '../../lib/instance'
|
|
4
|
+
import { fatal, log } from '../../lib/log'
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
name: 'forget',
|
|
8
|
+
description: 'Drop a bookmark reference (never destructive kernel-side)',
|
|
9
|
+
afterHelpText: `
|
|
10
|
+
Behavior:
|
|
11
|
+
Drops a bookmark reference only — never touches the remote kernel.
|
|
12
|
+
To destructively remove an admin-managed instance use \`astrale instance delete\`.
|
|
13
|
+
`,
|
|
14
|
+
arguments: [{ name: 'name', description: 'Bookmark name (slug or name)', required: true }],
|
|
15
|
+
action: async (name: string) => {
|
|
16
|
+
try {
|
|
17
|
+
const store = await readInstances()
|
|
18
|
+
const key = resolveInstanceKey(store, name)
|
|
19
|
+
if (!key) fatal(new Error(`Instance "${name}" not found`))
|
|
20
|
+
await removeInstance(key!)
|
|
21
|
+
log.success(`Forgot bookmark "${name}" (no kernel-side change)`)
|
|
22
|
+
} catch (e) {
|
|
23
|
+
fatal(e)
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
} satisfies CommandDefinition
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
|
|
3
|
+
import type { CommandDefinition } from '../../command'
|
|
4
|
+
import type { KernelCommandOpts } from '../../kernel'
|
|
5
|
+
import type { Column } from '../../lib/output'
|
|
6
|
+
|
|
7
|
+
import { withAdminKernelClient } from '../../kernel/client'
|
|
8
|
+
import { ADMIN_INSTANCE, formatInstanceLocation, type InstanceInfo } from '../../lib/admin-instance'
|
|
9
|
+
import { ADMIN_TARGET_OPTIONS, type AdminTargetCommandOpts } from '../../lib/admin-target'
|
|
10
|
+
import { normalizeInstanceKernelUrl, readInstances } from '../../lib/instance'
|
|
11
|
+
import { fatal, log, withSpinner } from '../../lib/log'
|
|
12
|
+
import { isMachine, output, type RawOutputOpts } from '../../lib/output'
|
|
13
|
+
import { renderTable } from '../../lib/table'
|
|
14
|
+
|
|
15
|
+
type ListOpts = KernelCommandOpts &
|
|
16
|
+
AdminTargetCommandOpts &
|
|
17
|
+
RawOutputOpts & {
|
|
18
|
+
bookmarked?: boolean
|
|
19
|
+
adminOnly?: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type Bookmark = {
|
|
23
|
+
name: string
|
|
24
|
+
url: string | null
|
|
25
|
+
issuer: string | null
|
|
26
|
+
active: boolean
|
|
27
|
+
defaultIdentity: string | null
|
|
28
|
+
createdAt: string | null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const COLUMNS: Column[] = [
|
|
32
|
+
{ key: 'name', header: 'NAME', color: chalk.bold },
|
|
33
|
+
{ key: 'kind', header: 'KIND', color: chalk.dim },
|
|
34
|
+
{ key: 'url', header: 'URL', color: chalk.dim },
|
|
35
|
+
{ key: 'extra', header: '', color: chalk.dim },
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
export default {
|
|
39
|
+
name: 'list',
|
|
40
|
+
description: 'List admin-managed instances and local bookmarks',
|
|
41
|
+
options: [
|
|
42
|
+
...ADMIN_TARGET_OPTIONS,
|
|
43
|
+
{ flags: '--bookmarked', description: 'Only show locally bookmarked kernel connections' },
|
|
44
|
+
{ flags: '--admin-only', description: 'Only show instances returned by the admin kernel' },
|
|
45
|
+
],
|
|
46
|
+
action: async (opts: ListOpts) => {
|
|
47
|
+
try {
|
|
48
|
+
const store = await readInstances()
|
|
49
|
+
const bookmarks: Bookmark[] = Object.entries(store.instances).map(([name, entry]) => ({
|
|
50
|
+
name,
|
|
51
|
+
url: entry.url ?? null,
|
|
52
|
+
issuer: entry.issuer ?? null,
|
|
53
|
+
active: name === store.active,
|
|
54
|
+
defaultIdentity: entry.defaultIdentity ?? null,
|
|
55
|
+
createdAt: entry.createdAt ?? null,
|
|
56
|
+
}))
|
|
57
|
+
|
|
58
|
+
let managed: InstanceInfo[] = []
|
|
59
|
+
if (!opts.bookmarked) {
|
|
60
|
+
managed = await withSpinner('Fetching instances', !isMachine(opts), () =>
|
|
61
|
+
withAdminKernelClient(
|
|
62
|
+
opts,
|
|
63
|
+
async (ctx) => (await ctx.client.call(`${ADMIN_INSTANCE}/list`, {})) as InstanceInfo[],
|
|
64
|
+
),
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
if (isMachine(opts)) {
|
|
68
|
+
output(
|
|
69
|
+
{
|
|
70
|
+
active: store.active || null,
|
|
71
|
+
...(opts.adminOnly ? {} : { bookmarks }),
|
|
72
|
+
...(opts.bookmarked ? {} : { instances: managed }),
|
|
73
|
+
},
|
|
74
|
+
opts,
|
|
75
|
+
)
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const rows = buildInstanceRows(managed, bookmarks, {
|
|
80
|
+
managed: !opts.bookmarked,
|
|
81
|
+
bookmarks: !opts.adminOnly,
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
if (rows.length === 0) {
|
|
85
|
+
log.dim(' No instances. Run: astrale instance bookmark <name> --url <url>')
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
process.stdout.write(renderTable(rows, { columns: COLUMNS }) + '\n')
|
|
89
|
+
} catch (e) {
|
|
90
|
+
fatal(e)
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
} satisfies CommandDefinition
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* One row per instance. A bookmark that points at a managed instance (same
|
|
97
|
+
* name and same kernel URL) merges into the managed row — carrying the active
|
|
98
|
+
* marker — instead of listing the instance twice.
|
|
99
|
+
*/
|
|
100
|
+
export function buildInstanceRows(
|
|
101
|
+
managed: InstanceInfo[],
|
|
102
|
+
bookmarks: Bookmark[],
|
|
103
|
+
show: { managed: boolean; bookmarks: boolean },
|
|
104
|
+
): Array<Record<string, string>> {
|
|
105
|
+
const rows: Array<Record<string, string>> = []
|
|
106
|
+
const merged = new Set<string>()
|
|
107
|
+
|
|
108
|
+
const bookmarkByName = new Map<string, { url: string; active: boolean }>()
|
|
109
|
+
if (show.managed && show.bookmarks) {
|
|
110
|
+
for (const bookmark of bookmarks) {
|
|
111
|
+
if (bookmark.url === null) continue
|
|
112
|
+
bookmarkByName.set(bookmark.name, {
|
|
113
|
+
url: normalizeInstanceKernelUrl(bookmark.url),
|
|
114
|
+
active: bookmark.active,
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (show.managed) {
|
|
120
|
+
for (const item of managed) {
|
|
121
|
+
const candidate = item.url ? bookmarkByName.get(item.slug) : undefined
|
|
122
|
+
const twin =
|
|
123
|
+
candidate && item.url && candidate.url === normalizeInstanceKernelUrl(item.url)
|
|
124
|
+
? candidate
|
|
125
|
+
: undefined
|
|
126
|
+
if (twin) merged.add(item.slug)
|
|
127
|
+
rows.push({
|
|
128
|
+
name: twin?.active ? `${item.slug} ${chalk.green('*')}` : item.slug,
|
|
129
|
+
kind: 'managed',
|
|
130
|
+
url: item.url ?? '',
|
|
131
|
+
extra: formatInstanceLocation(item),
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (show.bookmarks) {
|
|
137
|
+
for (const item of bookmarks) {
|
|
138
|
+
if (merged.has(item.name)) continue
|
|
139
|
+
rows.push({
|
|
140
|
+
name: item.active ? `${item.name} ${chalk.green('*')}` : item.name,
|
|
141
|
+
kind: 'bookmark',
|
|
142
|
+
url: String(item.url ?? ''),
|
|
143
|
+
extra: '',
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return rows
|
|
149
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
|
|
3
|
+
import type { CommandDefinition } from '../../command'
|
|
4
|
+
import type { KernelCommandOpts } from '../../kernel'
|
|
5
|
+
|
|
6
|
+
import { withAdminKernelClient } from '../../kernel/client'
|
|
7
|
+
import { ADMIN_INSTANCE, type InstanceInfo } from '../../lib/admin-instance'
|
|
8
|
+
import { ADMIN_TARGET_OPTIONS, type AdminTargetCommandOpts } from '../../lib/admin-target'
|
|
9
|
+
import { fatal, log, withSpinner } from '../../lib/log'
|
|
10
|
+
import { isMachine, output, type RawOutputOpts } from '../../lib/output'
|
|
11
|
+
|
|
12
|
+
type StatusOpts = KernelCommandOpts & AdminTargetCommandOpts & RawOutputOpts
|
|
13
|
+
|
|
14
|
+
export default {
|
|
15
|
+
name: 'status',
|
|
16
|
+
description: 'Show admin instance status',
|
|
17
|
+
arguments: [{ name: 'id', description: 'Instance slug', required: true }],
|
|
18
|
+
options: [...ADMIN_TARGET_OPTIONS],
|
|
19
|
+
action: async (id: string, opts: StatusOpts) => {
|
|
20
|
+
try {
|
|
21
|
+
const result = await withSpinner(`Fetching instance ${id}`, !isMachine(opts), () =>
|
|
22
|
+
withAdminKernelClient(
|
|
23
|
+
opts,
|
|
24
|
+
async (ctx) => (await ctx.client.call(`${ADMIN_INSTANCE}/info`, { id })) as InstanceInfo,
|
|
25
|
+
),
|
|
26
|
+
)
|
|
27
|
+
if (isMachine(opts)) {
|
|
28
|
+
output(result, opts)
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
console.log(`${chalk.bold(result.slug)} ${chalk.dim(result.url)}`)
|
|
32
|
+
if (result.state)
|
|
33
|
+
log.dim(` state: ${result.state}${result.phase ? ` (${result.phase})` : ''}`)
|
|
34
|
+
if (result.error) log.dim(` error: ${result.error}`)
|
|
35
|
+
if (result.region) log.dim(` region: ${result.region}`)
|
|
36
|
+
if (result.hostId) log.dim(` host: ${result.hostId}`)
|
|
37
|
+
if (result.createdAt) log.dim(` created: ${result.createdAt}`)
|
|
38
|
+
} catch (e) {
|
|
39
|
+
fatal(e)
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
} satisfies CommandDefinition
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import type { CommandDefinition } from '../../command'
|
|
2
|
+
|
|
3
|
+
import { AstraleError } from '../../errors'
|
|
4
|
+
import { withAdminKernelClient } from '../../kernel/client'
|
|
5
|
+
import { ADMIN_INSTANCE, type InstanceInfo } from '../../lib/admin-instance'
|
|
6
|
+
import { ADMIN_TARGET_OPTIONS } from '../../lib/admin-target'
|
|
7
|
+
import { getDefault, setDefault } from '../../lib/identity'
|
|
8
|
+
import {
|
|
9
|
+
getActive,
|
|
10
|
+
readInstances,
|
|
11
|
+
resolveInstance,
|
|
12
|
+
resolveInstanceKey,
|
|
13
|
+
setActive,
|
|
14
|
+
upsertManagedBookmark,
|
|
15
|
+
type ResolvedInstance,
|
|
16
|
+
} from '../../lib/instance'
|
|
17
|
+
import {
|
|
18
|
+
collectInstanceCandidates,
|
|
19
|
+
describeInstanceCandidate,
|
|
20
|
+
type InstanceCandidate,
|
|
21
|
+
} from '../../lib/instance-candidates'
|
|
22
|
+
import { fatal, log } from '../../lib/log'
|
|
23
|
+
import { checkIssuerReachability } from '../../lib/meta'
|
|
24
|
+
import { confirmDefaultYes, selectFrom } from '../../lib/prompt'
|
|
25
|
+
import { validateSlug } from '../../lib/validation'
|
|
26
|
+
|
|
27
|
+
type UseOpts = {
|
|
28
|
+
admin?: string
|
|
29
|
+
adminUrl?: string
|
|
30
|
+
url?: string
|
|
31
|
+
timeout?: string
|
|
32
|
+
as?: string
|
|
33
|
+
creds?: string
|
|
34
|
+
debug?: boolean
|
|
35
|
+
ci?: boolean
|
|
36
|
+
noPrompt?: boolean
|
|
37
|
+
adoptDefault?: boolean
|
|
38
|
+
skipJwksCheck?: boolean
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function useInstance(name?: string, opts: UseOpts = {}): Promise<void> {
|
|
42
|
+
try {
|
|
43
|
+
if (!name) {
|
|
44
|
+
const active = await getActive()
|
|
45
|
+
console.log(`${active.name} (${active.url})`)
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const resolved = await resolveUseTarget(name, opts)
|
|
50
|
+
|
|
51
|
+
if (!opts.skipJwksCheck && resolved.issuer) {
|
|
52
|
+
try {
|
|
53
|
+
await checkIssuerReachability(resolved.url, resolved.issuer)
|
|
54
|
+
} catch (e) {
|
|
55
|
+
fatal(e)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
await setActive(resolved.name)
|
|
60
|
+
log.success(`Active instance: ${resolved.name} (${resolved.url})`)
|
|
61
|
+
|
|
62
|
+
// §7.1 identity-adoption prompt (DX). Orthogonality preserved — we
|
|
63
|
+
// only switch on explicit user consent (or --adopt-default in CI).
|
|
64
|
+
const store = await readInstances()
|
|
65
|
+
const key = resolveInstanceKey(store, resolved.name)
|
|
66
|
+
const identityCandidate =
|
|
67
|
+
resolved.defaultIdentity ?? (key ? store.instances[key]?.defaultIdentity : undefined)
|
|
68
|
+
if (!identityCandidate) return
|
|
69
|
+
const active = await getDefault().catch(() => null)
|
|
70
|
+
if (active?.name === identityCandidate) return
|
|
71
|
+
|
|
72
|
+
const inCi = opts.ci || opts.noPrompt || !!process.env.CI
|
|
73
|
+
if (inCi) {
|
|
74
|
+
if (opts.adoptDefault) {
|
|
75
|
+
await setDefault(identityCandidate)
|
|
76
|
+
log.success(`Identity switched to "${identityCandidate}" (--adopt-default)`)
|
|
77
|
+
} else {
|
|
78
|
+
log.dim(
|
|
79
|
+
` (instance default identity "${identityCandidate}" — keep active ${active?.name ?? 'none'})`,
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const msg = `Instance "${resolved.name}" has default identity "${identityCandidate}". Active: "${active?.name ?? 'none'}". Switch identity too?`
|
|
86
|
+
if (await confirmDefaultYes(msg)) {
|
|
87
|
+
await setDefault(identityCandidate)
|
|
88
|
+
log.success(`Identity switched to "${identityCandidate}"`)
|
|
89
|
+
} else {
|
|
90
|
+
log.dim(' kept active identity (orthogonal)')
|
|
91
|
+
}
|
|
92
|
+
} catch (e) {
|
|
93
|
+
fatal(e)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function resolveUseTarget(name: string, opts: UseOpts): Promise<ResolvedInstance> {
|
|
98
|
+
const [store, managed] = await Promise.all([readInstances(), fetchManagedInstances(name, opts)])
|
|
99
|
+
const candidates = collectInstanceCandidates(name, store, managed)
|
|
100
|
+
|
|
101
|
+
if (candidates.length === 0) {
|
|
102
|
+
throw new AstraleError(
|
|
103
|
+
'INSTANCE_NOT_FOUND',
|
|
104
|
+
`Instance "${name}" is not bookmarked and not admin-managed.\n` +
|
|
105
|
+
` Bookmark: astrale instance bookmark ${name} --url <url>\n` +
|
|
106
|
+
` Or check: astrale instance list`,
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const interactive = !(opts.ci || opts.noPrompt || process.env.CI)
|
|
111
|
+
const chosen =
|
|
112
|
+
candidates.length === 1
|
|
113
|
+
? candidates[0]
|
|
114
|
+
: interactive
|
|
115
|
+
? await pickCandidate(name, candidates)
|
|
116
|
+
: null
|
|
117
|
+
if (!chosen) throw ambiguousError(name, candidates)
|
|
118
|
+
|
|
119
|
+
if (chosen.source === 'bookmark') {
|
|
120
|
+
return await resolveInstance(chosen.key)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
assertManagedReady(chosen.info)
|
|
124
|
+
const { repointedFrom } = await upsertManagedBookmark(chosen.key, chosen.info.slug, chosen.url)
|
|
125
|
+
if (repointedFrom) {
|
|
126
|
+
log.warn(`Bookmark "${chosen.key}" repointed: ${repointedFrom} → ${chosen.url}`)
|
|
127
|
+
}
|
|
128
|
+
return await resolveInstance(chosen.key)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* All admin-managed instances (collectInstanceCandidates filters by name).
|
|
133
|
+
* Best-effort: an unreachable or unauthenticated admin kernel degrades to
|
|
134
|
+
* bookmark-only resolution.
|
|
135
|
+
*/
|
|
136
|
+
async function fetchManagedInstances(name: string, opts: UseOpts): Promise<InstanceInfo[]> {
|
|
137
|
+
try {
|
|
138
|
+
validateSlug(name)
|
|
139
|
+
} catch {
|
|
140
|
+
return []
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
return await withAdminKernelClient(
|
|
144
|
+
opts,
|
|
145
|
+
async (ctx) => (await ctx.client.call(`${ADMIN_INSTANCE}/list`, {})) as InstanceInfo[],
|
|
146
|
+
)
|
|
147
|
+
} catch {
|
|
148
|
+
return []
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function assertManagedReady(info: InstanceInfo): void {
|
|
153
|
+
if (!info.state || info.state === 'ready') return
|
|
154
|
+
const detail = info.phase && info.phase !== info.state ? ` (${info.phase})` : ''
|
|
155
|
+
throw new AstraleError(
|
|
156
|
+
'INSTANCE_NOT_READY',
|
|
157
|
+
`Instance "${info.slug}" is ${info.state}${detail}; it cannot be selected yet.`,
|
|
158
|
+
info.error ?? undefined,
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function pickCandidate(
|
|
163
|
+
name: string,
|
|
164
|
+
candidates: InstanceCandidate[],
|
|
165
|
+
): Promise<InstanceCandidate | null> {
|
|
166
|
+
return selectFrom(
|
|
167
|
+
`Multiple instances match "${name}":`,
|
|
168
|
+
candidates.map((candidate) => ({
|
|
169
|
+
label: describeInstanceCandidate(candidate),
|
|
170
|
+
value: candidate,
|
|
171
|
+
})),
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function ambiguousError(name: string, candidates: InstanceCandidate[]): AstraleError {
|
|
176
|
+
const lines = candidates.map((candidate) => ` - ${describeInstanceCandidate(candidate)}`)
|
|
177
|
+
return new AstraleError(
|
|
178
|
+
'INSTANCE_AMBIGUOUS',
|
|
179
|
+
`Multiple instances match "${name}":\n${lines.join('\n')}`,
|
|
180
|
+
'Run interactively to pick one, or `astrale instance forget <name>` to drop the stale bookmark.',
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export default {
|
|
185
|
+
name: 'use',
|
|
186
|
+
description: 'Set the active kernel instance (no args: show current)',
|
|
187
|
+
afterHelpText: `
|
|
188
|
+
Behavior:
|
|
189
|
+
The active instance lives in ~/.astrale/instances.json (a
|
|
190
|
+
process-global file). Concurrent instance:prepare or parallel test
|
|
191
|
+
runs can rewrite it under you — in scripted/parallel flows pass
|
|
192
|
+
-i <instance> on every command instead of relying on \`use\`.
|
|
193
|
+
|
|
194
|
+
Examples:
|
|
195
|
+
$ astrale instance use staging
|
|
196
|
+
$ astrale instance use staging --adopt-default
|
|
197
|
+
`,
|
|
198
|
+
arguments: [{ name: 'name', description: 'Registered instance name', required: false }],
|
|
199
|
+
options: [
|
|
200
|
+
...ADMIN_TARGET_OPTIONS,
|
|
201
|
+
{
|
|
202
|
+
flags: '--adopt-default',
|
|
203
|
+
description: 'Adopt instance default identity without prompt',
|
|
204
|
+
},
|
|
205
|
+
{ flags: '--skip-jwks-check', description: 'Skip the /meta ↔ JWKS match check' },
|
|
206
|
+
],
|
|
207
|
+
action: async (name: string | undefined, opts: UseOpts) => {
|
|
208
|
+
await useInstance(name, opts)
|
|
209
|
+
},
|
|
210
|
+
} satisfies CommandDefinition
|