@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,136 @@
|
|
|
1
|
+
import { checkbox, confirm as confirmPrompt, input, select } from '@inquirer/prompts'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import { createInterface } from 'node:readline/promises'
|
|
4
|
+
|
|
5
|
+
// All interactive prompts go through `@inquirer/prompts` so a single library
|
|
6
|
+
// owns stdin (raw mode, keypress handling, cleanup). Hand-rolling some prompts
|
|
7
|
+
// with `node:readline` alongside inquirer used to fight over the TTY — the
|
|
8
|
+
// raw↔line-mode handoff swallowed the first keystroke, so a step right after an
|
|
9
|
+
// inquirer prompt needed a double Enter. One owner, no handoff, no lost keys.
|
|
10
|
+
//
|
|
11
|
+
// Every helper guards on `process.stdin.isTTY` BEFORE touching inquirer (which
|
|
12
|
+
// requires a TTY): a piped / CI / no-TTY (LLM) run returns the default
|
|
13
|
+
// immediately and renders nothing, so callers never hang on a read.
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Prompt the user for Y/N confirmation (default No). Returns false in non-TTY
|
|
17
|
+
* environments (use --yes / a flag to bypass).
|
|
18
|
+
*/
|
|
19
|
+
export async function confirm(message: string): Promise<boolean> {
|
|
20
|
+
if (!process.stdin.isTTY) return false
|
|
21
|
+
return confirmPrompt({ message, default: false })
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Prompt with a Y default — "Y/n" semantics. Returns true unless the user
|
|
26
|
+
* explicitly declines; returns true in non-TTY.
|
|
27
|
+
*/
|
|
28
|
+
export async function confirmDefaultYes(message: string): Promise<boolean> {
|
|
29
|
+
if (!process.stdin.isTTY) return true
|
|
30
|
+
return confirmPrompt({ message, default: true })
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Prompt the user to type a specific string to confirm a dangerous action.
|
|
35
|
+
* Returns false in non-TTY environments (use a flag to bypass).
|
|
36
|
+
*/
|
|
37
|
+
export async function confirmWithInput(message: string, expected: string): Promise<boolean> {
|
|
38
|
+
if (!process.stdin.isTTY) return false
|
|
39
|
+
process.stdout.write(chalk.yellow(`${message}\n`))
|
|
40
|
+
const answer = await input({ message: `Type "${expected}" to confirm:` })
|
|
41
|
+
return answer.trim() === expected
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Free-text prompt (a styled `@inquirer/prompts` input — shows the default,
|
|
46
|
+
* supports inline `validate` with live re-ask). Returns the typed value, or the
|
|
47
|
+
* default on empty input (`undefined` when none) in a non-TTY run.
|
|
48
|
+
*/
|
|
49
|
+
export async function promptText(
|
|
50
|
+
message: string,
|
|
51
|
+
opts: { default?: string; validate?: (value: string) => boolean | string } = {},
|
|
52
|
+
): Promise<string | undefined> {
|
|
53
|
+
if (!process.stdin.isTTY) return opts.default
|
|
54
|
+
const answer = await input({
|
|
55
|
+
message,
|
|
56
|
+
...(opts.default !== undefined ? { default: opts.default } : {}),
|
|
57
|
+
...(opts.validate ? { validate: opts.validate } : {}),
|
|
58
|
+
})
|
|
59
|
+
return answer.trim() || opts.default
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Single-choice selector (arrow-key `@inquirer/prompts` select). Returns the
|
|
64
|
+
* chosen value, or `undefined` in a non-TTY environment.
|
|
65
|
+
*/
|
|
66
|
+
export async function promptSelect<T>(
|
|
67
|
+
message: string,
|
|
68
|
+
choices: Array<{ name: string; value: T; description?: string }>,
|
|
69
|
+
): Promise<T | undefined> {
|
|
70
|
+
if (!process.stdin.isTTY) return undefined
|
|
71
|
+
return select({ message, choices })
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Multi-choice selector (a styled `@inquirer/prompts` checkbox — space toggles,
|
|
76
|
+
* enter confirms). Pre-check options with `checked: true`. Returns the chosen
|
|
77
|
+
* values, or `undefined` in a non-TTY environment.
|
|
78
|
+
*/
|
|
79
|
+
export async function promptMultiSelect<T>(
|
|
80
|
+
message: string,
|
|
81
|
+
choices: Array<{ name: string; value: T; checked?: boolean; description?: string }>,
|
|
82
|
+
): Promise<T[] | undefined> {
|
|
83
|
+
if (!process.stdin.isTTY) return undefined
|
|
84
|
+
return checkbox({ message, choices })
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Single-choice selector over labeled values. Returns the chosen value, or
|
|
89
|
+
* `null` in a non-TTY environment (callers decide how to fail). In a terminal
|
|
90
|
+
* the user always picks one (Enter selects the highlighted option; Ctrl-C
|
|
91
|
+
* aborts the command), so `null` only ever signals "no TTY".
|
|
92
|
+
*/
|
|
93
|
+
export async function selectFrom<T>(
|
|
94
|
+
message: string,
|
|
95
|
+
choices: Array<{ label: string; value: T }>,
|
|
96
|
+
): Promise<T | null> {
|
|
97
|
+
if (!process.stdin.isTTY) return null
|
|
98
|
+
return select({ message, choices: choices.map((c) => ({ name: c.label, value: c.value })) })
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Prompt a passphrase without echoing. Fails in non-TTY unless env override. */
|
|
102
|
+
export async function readPassphrase(
|
|
103
|
+
message: string,
|
|
104
|
+
opts: { minLength?: number } = {},
|
|
105
|
+
): Promise<string> {
|
|
106
|
+
const env = process.env.ASTRALE_PASSPHRASE
|
|
107
|
+
if (env) return env
|
|
108
|
+
if (!process.stdin.isTTY) {
|
|
109
|
+
throw new Error('Passphrase required but no TTY. Pipe via ASTRALE_PASSPHRASE env var.')
|
|
110
|
+
}
|
|
111
|
+
// v1 note: passphrase echoes on interactive terminals. Pipe
|
|
112
|
+
// ASTRALE_PASSPHRASE=... for scripted flows. Silent stdin with raw
|
|
113
|
+
// mode is roadmap (requires terminal capabilities handling).
|
|
114
|
+
process.stdout.write(chalk.yellow(message))
|
|
115
|
+
const answer = await readLine()
|
|
116
|
+
process.stdout.write('\n')
|
|
117
|
+
if (opts.minLength && answer.length < opts.minLength) {
|
|
118
|
+
throw new Error(`Passphrase too short (min ${opts.minLength} chars)`)
|
|
119
|
+
}
|
|
120
|
+
return answer
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Read one line from stdin via Node's own line reader (stdlib, zero-dep).
|
|
125
|
+
* Used only by `readPassphrase` (a single-shot prompt that intentionally echoes
|
|
126
|
+
* and honors ASTRALE_PASSPHRASE) — every navigational prompt uses inquirer.
|
|
127
|
+
* Only ever reached behind an `isTTY` gate, so it never blocks a piped run.
|
|
128
|
+
*/
|
|
129
|
+
async function readLine(): Promise<string> {
|
|
130
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
131
|
+
try {
|
|
132
|
+
return (await rl.question('')).trim()
|
|
133
|
+
} finally {
|
|
134
|
+
rl.close()
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
|
|
3
|
+
import type { KernelCommandOpts } from '../kernel'
|
|
4
|
+
import type { AdminTargetCommandOpts } from './admin-target'
|
|
5
|
+
|
|
6
|
+
import { AuthError } from '../errors'
|
|
7
|
+
import { withAdminKernelClient } from '../kernel/client'
|
|
8
|
+
import { ADMIN_INSTANCE } from './admin-instance'
|
|
9
|
+
import { readIdentities, type IdentityStore } from './identity'
|
|
10
|
+
import { setActive, upsertManagedBookmark } from './instance'
|
|
11
|
+
import { withSpinner } from './log'
|
|
12
|
+
import { isMachine } from './output'
|
|
13
|
+
import { promptSelect } from './prompt'
|
|
14
|
+
import { validateSlug } from './validation'
|
|
15
|
+
|
|
16
|
+
export type ProvisionOpts = KernelCommandOpts &
|
|
17
|
+
AdminTargetCommandOpts & {
|
|
18
|
+
hostId?: string
|
|
19
|
+
// Global flags (program.ts) that force non-interactive — mirrors `instance use`.
|
|
20
|
+
ci?: boolean
|
|
21
|
+
noPrompt?: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The created instance plus the local-bookmark side effects of provisioning. */
|
|
25
|
+
export type ProvisionResult = {
|
|
26
|
+
/** The raw admin-kernel response — the stable machine surface for `--json`. */
|
|
27
|
+
created: { url: string; organizationId?: string }
|
|
28
|
+
slug: string
|
|
29
|
+
/** Set when an existing bookmark of the same name was repointed to a new kernel. */
|
|
30
|
+
repointedFrom?: string
|
|
31
|
+
/** Set when bookmarking/activating the new instance failed (non-fatal). */
|
|
32
|
+
selectionError?: unknown
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Provisioning a child instance runs a multi-step saga (1-3 min). */
|
|
36
|
+
const SAGA_TIMEOUT_MS = '240000'
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Provision an alpha instance through the admin kernel, then bookmark it and
|
|
40
|
+
* make it the active target. Extracted from `instance create` so `astrale
|
|
41
|
+
* setup` provisions through the exact same saga (auth assertion → alphaCreate →
|
|
42
|
+
* bookmark → set-active), including the interactive multi-host picker.
|
|
43
|
+
*
|
|
44
|
+
* Presentation is deliberately minimal here (a spinner + a one-line success);
|
|
45
|
+
* the caller renders anything richer — `setup` follows this with a hero panel.
|
|
46
|
+
*/
|
|
47
|
+
export async function provisionInstance(
|
|
48
|
+
slug: string,
|
|
49
|
+
opts: ProvisionOpts,
|
|
50
|
+
): Promise<ProvisionResult> {
|
|
51
|
+
validateSlug(slug)
|
|
52
|
+
await assertAlphaCreateAuth(opts)
|
|
53
|
+
// The created instance must belong to the LOGGED-IN identity — never to an
|
|
54
|
+
// identity pinned on the admin bookmark (that mismatch silently made a fresh
|
|
55
|
+
// user's instance unusable). `--as` still wins.
|
|
56
|
+
if (!opts.as) opts = { ...opts, as: (await readIdentities()).default }
|
|
57
|
+
|
|
58
|
+
const machine = isMachine(opts)
|
|
59
|
+
const interactive = !!process.stdin.isTTY && !(opts.ci || opts.noPrompt || process.env.CI)
|
|
60
|
+
|
|
61
|
+
let repointedFrom: string | undefined
|
|
62
|
+
let selectionError: unknown = null
|
|
63
|
+
// The global 30s default doesn't just fail the CLIENT: the disconnect kills
|
|
64
|
+
// the worker's request mid-saga and leaves TORN state (slug taken, routing
|
|
65
|
+
// live, no instance node — unrecoverable by retry). Default to a saga-sized
|
|
66
|
+
// timeout; an explicit --timeout still wins.
|
|
67
|
+
const createOpts = { ...opts, timeout: opts.timeout ?? SAGA_TIMEOUT_MS }
|
|
68
|
+
|
|
69
|
+
const runProvision = (hostId: string | undefined) =>
|
|
70
|
+
withSpinner(
|
|
71
|
+
`Provisioning instance ${slug}`,
|
|
72
|
+
!machine,
|
|
73
|
+
async () => {
|
|
74
|
+
const created = await withAdminKernelClient(
|
|
75
|
+
createOpts,
|
|
76
|
+
async (ctx) =>
|
|
77
|
+
(await ctx.client.call(`${ADMIN_INSTANCE}/alphaCreate`, {
|
|
78
|
+
slug,
|
|
79
|
+
...(hostId ? { host_id: hostId } : {}),
|
|
80
|
+
})) as { url: string; organizationId?: string },
|
|
81
|
+
)
|
|
82
|
+
try {
|
|
83
|
+
// Org id from the create response — authoritative for token scoping
|
|
84
|
+
// (the router's /auth/org is eventually consistent).
|
|
85
|
+
const bookmarked = await upsertManagedBookmark(
|
|
86
|
+
slug,
|
|
87
|
+
slug,
|
|
88
|
+
created.url,
|
|
89
|
+
created.organizationId,
|
|
90
|
+
)
|
|
91
|
+
repointedFrom = bookmarked.repointedFrom
|
|
92
|
+
await setActive(slug)
|
|
93
|
+
} catch (e) {
|
|
94
|
+
selectionError = e
|
|
95
|
+
}
|
|
96
|
+
return created
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
success: (created) =>
|
|
100
|
+
`Instance provisioned: ${slug} ${chalk.dim(`(${created.url})${selectionError ? '' : ' · active'}`)}`,
|
|
101
|
+
},
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
// Host placement is chosen SERVER-side (the caller's ready + USE-granted
|
|
105
|
+
// hosts). We recover ONLY from its multi-host ambiguity: pop a picker and
|
|
106
|
+
// retry with the chosen host_id — the ambiguity error throws before any
|
|
107
|
+
// provisioning side effect, so the retry is clean. No host / permission /
|
|
108
|
+
// capacity / a non-interactive run all reject through to the caller's `fatal`,
|
|
109
|
+
// surfacing the server's own message plus the listed ids. An explicit
|
|
110
|
+
// `--host-id` skips all of this.
|
|
111
|
+
const created = await runProvision(opts.hostId).catch(async (e: unknown) => {
|
|
112
|
+
const hostIds = !opts.hostId && interactive ? parseEligibleHostIds(e) : null
|
|
113
|
+
if (!hostIds) throw e
|
|
114
|
+
const chosen = await promptSelect(
|
|
115
|
+
`${hostIds.length} hosts available — pick one to provision on`,
|
|
116
|
+
hostIds.map((hid) => ({ name: hid, value: hid })),
|
|
117
|
+
)
|
|
118
|
+
if (!chosen) throw e
|
|
119
|
+
return runProvision(chosen)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
// Warnings go to stderr so machine-readable stdout stays clean.
|
|
123
|
+
const warn = (msg: string) => console.error(chalk.yellow('⚠'), msg)
|
|
124
|
+
if (selectionError) {
|
|
125
|
+
const message =
|
|
126
|
+
selectionError instanceof Error ? selectionError.message : String(selectionError)
|
|
127
|
+
warn(`Could not select the new instance: ${message}`)
|
|
128
|
+
} else if (repointedFrom) {
|
|
129
|
+
warn(`Bookmark "${slug}" repointed: ${repointedFrom} → ${created.url}`)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { created, slug, repointedFrom, ...(selectionError ? { selectionError } : {}) }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Recover the eligible host ids from alphaCreate's multi-host error
|
|
137
|
+
* ("N ready hosts are assigned (id1, id2). Specify host_id…") — only when
|
|
138
|
+
* there's a real choice (>1). Returns null for any other error (no host,
|
|
139
|
+
* permission, capacity). Deliberately coupled to that message wording (Option
|
|
140
|
+
* B: no admin-side change); if it ever drifts we simply stop offering the
|
|
141
|
+
* picker and the raw error is shown instead — no worse than before.
|
|
142
|
+
*/
|
|
143
|
+
export function parseEligibleHostIds(error: unknown): string[] | null {
|
|
144
|
+
const text = error instanceof Error ? error.message : String(error)
|
|
145
|
+
const match = /ready hosts are assigned \(([^)]+)\)/.exec(text)
|
|
146
|
+
if (!match) return null
|
|
147
|
+
const ids = match[1]!
|
|
148
|
+
.split(',')
|
|
149
|
+
.map((s) => s.trim())
|
|
150
|
+
.filter(Boolean)
|
|
151
|
+
return ids.length > 1 ? ids : null
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function assertAlphaCreateAuth(opts: Pick<ProvisionOpts, 'as' | 'creds'>): Promise<void> {
|
|
155
|
+
if (opts.creds) return
|
|
156
|
+
assertAlphaCreateIdentity(await readIdentities(), opts)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function assertAlphaCreateIdentity(
|
|
160
|
+
store: IdentityStore,
|
|
161
|
+
opts: Pick<ProvisionOpts, 'as'> = {},
|
|
162
|
+
): void {
|
|
163
|
+
const name = opts.as ?? store.default
|
|
164
|
+
const identity = store.identities[name]
|
|
165
|
+
if (identity && identity.source === 'idp') return
|
|
166
|
+
throw new AuthError(
|
|
167
|
+
'WorkOS login required for `astrale instance create`',
|
|
168
|
+
'Run: astrale auth login',
|
|
169
|
+
)
|
|
170
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { run } from './proc'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `astrale update`, Axis C — keep a domain project's first-party `@astrale-os/*`
|
|
8
|
+
* dependencies current. We DELEGATE to pnpm rather than reimplement a package
|
|
9
|
+
* manager: `pnpm outdated` to detect, `pnpm update --latest` to apply. pnpm owns
|
|
10
|
+
* the hard, easy-to-get-wrong parts — registry/`.npmrc`/auth resolution, the
|
|
11
|
+
* `minimumReleaseAge` supply-chain gate, workspace members, semver, and the
|
|
12
|
+
* lockfile — so this stays a thin, robust shim (mirrors how the CLI delegates
|
|
13
|
+
* skills to `npx skills`). Every call is best-effort: pnpm missing or any error
|
|
14
|
+
* means "nothing to do", never a failed update.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** The dependency pattern pnpm matches both subcommands against. */
|
|
18
|
+
const ASTRALE_DEP_PATTERN = '@astrale-os/*'
|
|
19
|
+
|
|
20
|
+
export type SdkOutdated = { pkg: string; current: string; latest: string }
|
|
21
|
+
|
|
22
|
+
/** Is the CLI running at the root of a scaffolded domain project? */
|
|
23
|
+
export function inDomainProject(cwd: string = process.cwd()): boolean {
|
|
24
|
+
return existsSync(join(cwd, 'astrale.config.ts'))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A non-pnpm lockfile means another package manager owns this project; we won't
|
|
29
|
+
* run pnpm against it (that would write a stray `pnpm-lock.yaml`). Returns the
|
|
30
|
+
* foreign PM name, or null when it's pnpm / fresh (Astrale projects are pnpm-first).
|
|
31
|
+
*/
|
|
32
|
+
export function foreignPackageManager(cwd: string = process.cwd()): 'npm' | 'yarn' | 'bun' | null {
|
|
33
|
+
if (existsSync(join(cwd, 'pnpm-lock.yaml'))) return null
|
|
34
|
+
if (existsSync(join(cwd, 'package-lock.json'))) return 'npm'
|
|
35
|
+
if (existsSync(join(cwd, 'yarn.lock'))) return 'yarn'
|
|
36
|
+
if (existsSync(join(cwd, 'bun.lockb')) || existsSync(join(cwd, 'bun.lock'))) return 'bun'
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type PnpmOutdatedEntry = { current?: string; wanted?: string; latest?: string }
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Parse `pnpm outdated --format json` into the deps that actually have a newer
|
|
44
|
+
* release. pnpm only lists outdated packages and never proposes a downgrade, so
|
|
45
|
+
* we just drop entries with no `latest` or where `latest` equals the current
|
|
46
|
+
* version (it lists `wanted` when nothing is installed yet — use that as the
|
|
47
|
+
* "current" we display). Pure (no I/O) so it's unit-testable without spawning.
|
|
48
|
+
*/
|
|
49
|
+
export function parseSdkOutdated(stdout: string): SdkOutdated[] {
|
|
50
|
+
let parsed: Record<string, PnpmOutdatedEntry>
|
|
51
|
+
try {
|
|
52
|
+
parsed = JSON.parse(stdout || '{}') as Record<string, PnpmOutdatedEntry>
|
|
53
|
+
} catch {
|
|
54
|
+
return []
|
|
55
|
+
}
|
|
56
|
+
const out: SdkOutdated[] = []
|
|
57
|
+
for (const [pkg, entry] of Object.entries(parsed)) {
|
|
58
|
+
const current = entry.current ?? entry.wanted
|
|
59
|
+
if (entry.latest && current && entry.latest !== current) {
|
|
60
|
+
out.push({ pkg, current, latest: entry.latest })
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return out
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Ask pnpm which `@astrale-os/*` deps have a newer release. `pnpm outdated`
|
|
68
|
+
* exits 1 when outdated deps exist — that's not an error, so we parse stdout
|
|
69
|
+
* regardless of exit code. Returns [] when none, pnpm is absent, or anything fails.
|
|
70
|
+
*/
|
|
71
|
+
export async function findSdkOutdated(cwd: string = process.cwd()): Promise<SdkOutdated[]> {
|
|
72
|
+
try {
|
|
73
|
+
const { stdout } = await run('pnpm', ['outdated', '--format', 'json', ASTRALE_DEP_PATTERN], {
|
|
74
|
+
cwd,
|
|
75
|
+
})
|
|
76
|
+
return parseSdkOutdated(stdout)
|
|
77
|
+
} catch {
|
|
78
|
+
return [] // pnpm not on PATH, etc.
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Apply via `pnpm update --latest --lockfile-only` — pnpm rewrites package.json
|
|
84
|
+
* (preserving exact pins, the scaffold style) and the lockfile, honoring the
|
|
85
|
+
* registry + supply-chain age policy. `--lockfile-only` is deliberate: it updates
|
|
86
|
+
* the manifest + lockfile WITHOUT running install/build scripts, so an `astrale
|
|
87
|
+
* update` never executes a dependency's postinstall (pnpm would otherwise exit
|
|
88
|
+
* non-zero on `ERR_PNPM_IGNORED_BUILDS`) or churns node_modules — the user runs
|
|
89
|
+
* `pnpm install` to materialize, honoring their own build approvals. Surfaces
|
|
90
|
+
* pnpm's output only on failure. Returns true on success.
|
|
91
|
+
*/
|
|
92
|
+
export async function applySdkUpdate(cwd: string = process.cwd()): Promise<boolean> {
|
|
93
|
+
try {
|
|
94
|
+
const { code, stdout, stderr } = await run(
|
|
95
|
+
'pnpm',
|
|
96
|
+
['update', '--latest', '--lockfile-only', ASTRALE_DEP_PATTERN],
|
|
97
|
+
{ cwd },
|
|
98
|
+
)
|
|
99
|
+
if (code !== 0) process.stderr.write(stdout + stderr)
|
|
100
|
+
return code === 0
|
|
101
|
+
} catch {
|
|
102
|
+
return false
|
|
103
|
+
}
|
|
104
|
+
}
|
package/src/lib/self.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@self` shorthand: expands to the nodeId behind the JWT `sub` claim that
|
|
3
|
+
* the current CLI invocation would ship. Pure helpers — no I/O.
|
|
4
|
+
*
|
|
5
|
+
* Kernel-side, `Identity::registerIdentity` writes `sub = String(self.id)`
|
|
6
|
+
* (kernel/runtime/syscalls/identity/index.ts), so for any properly-registered
|
|
7
|
+
* identity the JWT `sub` IS the calling node's id on the target kernel.
|
|
8
|
+
* Delegation tokens follow the same shape (outer-envelope `sub` = identityId).
|
|
9
|
+
*
|
|
10
|
+
* The expansion is fail-loud: when no `sub` is resolvable, return a typed
|
|
11
|
+
* refusal carrying the exact next-step command. Never silently fall back to
|
|
12
|
+
* a label-ish `identity.subject` like "manager" or "alice".
|
|
13
|
+
*/
|
|
14
|
+
import { decodeJwt } from 'jose'
|
|
15
|
+
|
|
16
|
+
import type { Identity } from './identity'
|
|
17
|
+
|
|
18
|
+
/** Inputs to `resolveSelfNodeId`. All local — no kernel round-trip. */
|
|
19
|
+
export type SelfResolverContext = {
|
|
20
|
+
/** Identity that will sign this call (default identity or the one from `--as`). */
|
|
21
|
+
identity?: Identity & { name: string }
|
|
22
|
+
/** Resolved instance slug, if any (absent when `--url` is used without `-i`). */
|
|
23
|
+
instanceSlug?: string
|
|
24
|
+
/** Raw `--creds` JWT, if the user provided one. */
|
|
25
|
+
credsJwt?: string
|
|
26
|
+
/** True when `auth.ts` will sign as the instance itself (per-instance keypair, not user identity). */
|
|
27
|
+
instanceSigned: boolean
|
|
28
|
+
/** `sub` decoded from a cached IdP token for source=idp identities, when available. */
|
|
29
|
+
idpSubject?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type SelfRefusal =
|
|
33
|
+
| { reason: 'manager' }
|
|
34
|
+
| { reason: 'no-registration'; identityName: string; instanceSlug: string }
|
|
35
|
+
| { reason: 'instance-signed'; instanceSlug: string }
|
|
36
|
+
| { reason: 'url-no-slug' }
|
|
37
|
+
| { reason: 'creds-no-sub' }
|
|
38
|
+
| { reason: 'idp-no-sub'; identityName: string }
|
|
39
|
+
|
|
40
|
+
export type SelfResolution = { id: string } | SelfRefusal
|
|
41
|
+
|
|
42
|
+
// Anchors:
|
|
43
|
+
// left: start-of-string OR right after `=` (param-value head)
|
|
44
|
+
// right: end-of-string OR `::` (instance method) OR `/` (path navigation)
|
|
45
|
+
// The `/` lookahead is required so `@self/functions` etc. expand — the
|
|
46
|
+
// `astrale ls @self/functions` form is documented in SKILL.md and the
|
|
47
|
+
// sandbox prefix. Without it the regex silently no-ops on those inputs.
|
|
48
|
+
const SELF_RE = /(?:^|(?<=[=]))@self(?=$|::|\/)/g
|
|
49
|
+
|
|
50
|
+
/** Resolve `@self` → `{ id: <nodeId> }`, or a typed refusal. */
|
|
51
|
+
export function resolveSelfNodeId(ctx: SelfResolverContext): SelfResolution {
|
|
52
|
+
// 1. `--creds <jwt>` — bypasses every identity lookup. Read the sub claim.
|
|
53
|
+
if (ctx.credsJwt) {
|
|
54
|
+
try {
|
|
55
|
+
const sub = decodeJwt(ctx.credsJwt).sub
|
|
56
|
+
// `.trim().length` rejects whitespace-only subs (`' '`, `'\t'`).
|
|
57
|
+
// A hand-crafted JWT with `sub: ' '` previously round-tripped into
|
|
58
|
+
// `@ ::method`, producing a malformed kernel call instead of the
|
|
59
|
+
// typed `creds-no-sub` refusal.
|
|
60
|
+
if (typeof sub === 'string' && sub.trim().length > 0) return { id: sub }
|
|
61
|
+
} catch {
|
|
62
|
+
// fall through to creds-no-sub
|
|
63
|
+
}
|
|
64
|
+
return { reason: 'creds-no-sub' }
|
|
65
|
+
}
|
|
66
|
+
// 2. Instance-signed: no user identity context — `@self` is undefined.
|
|
67
|
+
if (ctx.instanceSigned) {
|
|
68
|
+
return { reason: 'instance-signed', instanceSlug: ctx.instanceSlug ?? 'unknown' }
|
|
69
|
+
}
|
|
70
|
+
// 3. IdP-backed identities: the provider token's `sub` is the IdP USER id
|
|
71
|
+
// (e.g. `user_01K…`), NEVER a graph node id — expanding to it always
|
|
72
|
+
// produced NOT_FOUND. Use a cached registration when one exists (a future
|
|
73
|
+
// `whoami`-backed cache can populate it); otherwise refuse with the recipe.
|
|
74
|
+
if ((ctx.identity?.source ?? 'key') === 'idp') {
|
|
75
|
+
const cached = ctx.instanceSlug
|
|
76
|
+
? ctx.identity?.registrations?.[ctx.instanceSlug]?.sub
|
|
77
|
+
: undefined
|
|
78
|
+
if (cached) return { id: cached }
|
|
79
|
+
return { reason: 'idp-no-sub', identityName: ctx.identity?.name ?? '(unknown)' }
|
|
80
|
+
}
|
|
81
|
+
// 4. `--url` without `-i`: no slug to look up registration against.
|
|
82
|
+
if (!ctx.instanceSlug) return { reason: 'url-no-slug' }
|
|
83
|
+
// 5. Bootstrap `manager` identity has no graph node by construction.
|
|
84
|
+
if (
|
|
85
|
+
ctx.identity?.name === 'manager' &&
|
|
86
|
+
(!ctx.identity.registrations || Object.keys(ctx.identity.registrations).length === 0)
|
|
87
|
+
) {
|
|
88
|
+
return { reason: 'manager' }
|
|
89
|
+
}
|
|
90
|
+
// 6. Default path: read the registration entry for the resolved slug.
|
|
91
|
+
// `sub` IS the node id by construction for entries written by
|
|
92
|
+
// `registerIdentity` (kernel/runtime/syscalls/identity/index.ts:207).
|
|
93
|
+
const id = ctx.identity?.registrations?.[ctx.instanceSlug]?.sub
|
|
94
|
+
if (!id) {
|
|
95
|
+
return {
|
|
96
|
+
reason: 'no-registration',
|
|
97
|
+
identityName: ctx.identity?.name ?? '(unknown)',
|
|
98
|
+
instanceSlug: ctx.instanceSlug,
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return { id }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Cheap pre-check before invoking the resolver — skip when no `@self` appears. */
|
|
105
|
+
export function containsSelfRef(input: string): boolean {
|
|
106
|
+
SELF_RE.lastIndex = 0
|
|
107
|
+
return SELF_RE.test(input)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Replace every `@self` token (path head or `=@self`) with `@<selfId>`.
|
|
112
|
+
*
|
|
113
|
+
* Uses the function-form replacement so a `selfId` containing `$&`, `$$`,
|
|
114
|
+
* `$<n>`, etc. is not interpreted as a `String.replace` substitution
|
|
115
|
+
* pattern. A delegation-token `sub` claim or future opaque kernel id can
|
|
116
|
+
* legitimately carry `$`.
|
|
117
|
+
*/
|
|
118
|
+
export function expandSelfReferences(input: string, selfId: string): string {
|
|
119
|
+
const replacement = `@${selfId}`
|
|
120
|
+
return input.replace(SELF_RE, () => replacement)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Build a human-facing error carrying the typed refusal as metadata. */
|
|
124
|
+
export function selfRefusalError(r: SelfRefusal): Error {
|
|
125
|
+
const e = new Error(refusalMessage(r))
|
|
126
|
+
e.name = 'SelfRefusalError'
|
|
127
|
+
;(e as Error & { selfRefusal: SelfRefusal }).selfRefusal = r
|
|
128
|
+
return e
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function refusalMessage(r: SelfRefusal): string {
|
|
132
|
+
switch (r.reason) {
|
|
133
|
+
case 'manager':
|
|
134
|
+
return [
|
|
135
|
+
"`@self` not available: you're signed in as the bootstrap `manager` identity, which has no graph node.",
|
|
136
|
+
'Run `astrale identity create <name>` then `astrale identity register <name>` to enable `@self`.',
|
|
137
|
+
].join('\n ')
|
|
138
|
+
case 'no-registration':
|
|
139
|
+
return [
|
|
140
|
+
`\`@self\` not available: identity "${r.identityName}" has no registration on instance "${r.instanceSlug}".`,
|
|
141
|
+
`Run \`astrale identity register ${r.identityName} -i ${r.instanceSlug}\`.`,
|
|
142
|
+
].join('\n ')
|
|
143
|
+
case 'instance-signed':
|
|
144
|
+
return [
|
|
145
|
+
`\`@self\` not available: this call signs as instance "${r.instanceSlug}" itself, not a user identity.`,
|
|
146
|
+
'Use `--as <name>` to sign as a registered identity, or pass a literal `@<nodeId>`.',
|
|
147
|
+
].join('\n ')
|
|
148
|
+
case 'url-no-slug':
|
|
149
|
+
return [
|
|
150
|
+
'`@self` needs an instance slug to look up the registration.',
|
|
151
|
+
'Add `-i <slug>`, or if this URL maps to a bookmark, use that slug instead of `--url`.',
|
|
152
|
+
].join('\n ')
|
|
153
|
+
case 'creds-no-sub':
|
|
154
|
+
return [
|
|
155
|
+
'`@self` not available: the `--creds` JWT has no usable `sub` claim.',
|
|
156
|
+
'Pass a literal `@<nodeId>` instead.',
|
|
157
|
+
].join('\n ')
|
|
158
|
+
case 'idp-no-sub':
|
|
159
|
+
return [
|
|
160
|
+
`\`@self\` could not be resolved for IdP identity "${r.identityName}": no cached registration, and the automatic whoami lookup did not return a node id (offline, auth failure, or unprovisioned identity).`,
|
|
161
|
+
'Get your node id manually, then use it directly:',
|
|
162
|
+
' astrale call "/:kernel.astrale.ai:interface.Identity:whoami" --json # → { id: <nodeId> }',
|
|
163
|
+
'and address yourself as `@<nodeId>`.',
|
|
164
|
+
].join('\n ')
|
|
165
|
+
}
|
|
166
|
+
}
|