@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,267 @@
|
|
|
1
|
+
import type { CommandDefinition } from '../command'
|
|
2
|
+
|
|
3
|
+
import pkg from '../../package.json' with { type: 'json' }
|
|
4
|
+
import { fatal, log } from '../lib/log'
|
|
5
|
+
import { isMachine, output, RAW_OUTPUT_OPTIONS, type RawOutputOpts } from '../lib/output'
|
|
6
|
+
import { confirmDefaultYes } from '../lib/prompt'
|
|
7
|
+
import {
|
|
8
|
+
applySdkUpdate,
|
|
9
|
+
findSdkOutdated,
|
|
10
|
+
foreignPackageManager,
|
|
11
|
+
inDomainProject,
|
|
12
|
+
type SdkOutdated,
|
|
13
|
+
} from '../lib/sdk-deps'
|
|
14
|
+
import { ASTRALE_CLI_SKILL, detectSkill, installSkills, SKILL_INSTALL_HINT } from '../lib/skills'
|
|
15
|
+
import { updateAstrale } from '../lib/update'
|
|
16
|
+
|
|
17
|
+
type UpdateOpts = RawOutputOpts & {
|
|
18
|
+
check?: boolean
|
|
19
|
+
channel?: string
|
|
20
|
+
version?: string
|
|
21
|
+
skills?: boolean
|
|
22
|
+
deps?: boolean
|
|
23
|
+
yes?: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Refresh the astrale agent skills alongside the binary. Delegates to the same
|
|
28
|
+
* `npx skills add` installer `astrale setup` uses; re-running it updates an
|
|
29
|
+
* existing install to the latest published SKILL.md.
|
|
30
|
+
*
|
|
31
|
+
* Only refreshes skills the user already has — `astrale update` keeps an existing
|
|
32
|
+
* install current, it does not foist skills on a non-agent user; fresh installs
|
|
33
|
+
* go through `astrale setup`. Best-effort: a failure here never fails the update
|
|
34
|
+
* (the binary is already swapped), so we warn and move on.
|
|
35
|
+
*/
|
|
36
|
+
async function refreshSkills(): Promise<void> {
|
|
37
|
+
if (!detectSkill(ASTRALE_CLI_SKILL).installed) {
|
|
38
|
+
log.dim(` Agent skills not installed — get them with: ${SKILL_INSTALL_HINT}`)
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
log.step(`Refreshing the astrale agent skills — ${SKILL_INSTALL_HINT}`)
|
|
42
|
+
if (await installSkills()) {
|
|
43
|
+
log.success('astrale agent skills up to date')
|
|
44
|
+
} else {
|
|
45
|
+
log.warn(`Skill refresh did not complete — run it later: ${SKILL_INSTALL_HINT}`)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Keep the project's first-party `@astrale-os/*` dependencies current by
|
|
51
|
+
* delegating to pnpm (`pnpm outdated` to detect, `pnpm update --latest` to
|
|
52
|
+
* apply). Runs only inside a domain project (the `astrale.config.ts` marker) and
|
|
53
|
+
* only when pnpm owns it; elsewhere it's a silent no-op. On a dry run (`check`)
|
|
54
|
+
* it only reports. Otherwise it lists the available bumps and applies them behind
|
|
55
|
+
* a single confirm — pnpm rewrites package.json AND the lockfile (no build scripts
|
|
56
|
+
* run; the user runs `pnpm install` to materialize). Returns true when bumps were
|
|
57
|
+
* available (so `--check` can exit 10). Best-effort: pnpm missing or any error
|
|
58
|
+
* surfaces as "up to date".
|
|
59
|
+
*/
|
|
60
|
+
async function refreshSdkDeps(check: boolean, assumeYes = false): Promise<boolean> {
|
|
61
|
+
if (!inDomainProject()) return false
|
|
62
|
+
|
|
63
|
+
const foreign = foreignPackageManager()
|
|
64
|
+
if (foreign) {
|
|
65
|
+
if (!check) {
|
|
66
|
+
log.dim(
|
|
67
|
+
` SDK dep check uses pnpm; this project uses ${foreign} — update it with ${foreign}.`,
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
return false
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const outdated = await findSdkOutdated()
|
|
74
|
+
if (outdated.length === 0) {
|
|
75
|
+
if (!check) log.dim(' @astrale-os SDK packages are up to date')
|
|
76
|
+
return false
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const plural = outdated.length === 1 ? '' : 's'
|
|
80
|
+
log.info(`@astrale-os SDK package${plural} with a newer release (${outdated.length}):`)
|
|
81
|
+
for (const o of outdated) log.dim(` ${o.pkg} ${o.current} → ${o.latest}`)
|
|
82
|
+
|
|
83
|
+
if (check) return true // dry run — report only
|
|
84
|
+
|
|
85
|
+
if (
|
|
86
|
+
!assumeYes &&
|
|
87
|
+
!(await confirmDefaultYes(
|
|
88
|
+
`Update ${outdated.length} @astrale-os dep${plural} (pnpm update --latest)?`,
|
|
89
|
+
))
|
|
90
|
+
) {
|
|
91
|
+
log.dim(' Skipped — your package.json is unchanged.')
|
|
92
|
+
return true
|
|
93
|
+
}
|
|
94
|
+
log.step('pnpm update --latest --lockfile-only "@astrale-os/*"')
|
|
95
|
+
if (await applySdkUpdate()) {
|
|
96
|
+
log.success(`Updated ${outdated.length} @astrale-os dep${plural} in package.json + lockfile`)
|
|
97
|
+
log.dim(' Run `pnpm install` to materialize the new versions.')
|
|
98
|
+
} else {
|
|
99
|
+
log.warn(' Update did not complete — run it yourself: pnpm update --latest "@astrale-os/*"')
|
|
100
|
+
}
|
|
101
|
+
return true
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Read-only staleness report for tooling — `astrale update --check --json`.
|
|
106
|
+
* domain-studio polls this on load to drive its "update available" badge. It is
|
|
107
|
+
* unified and NON-THROWING: the CLI axis catches the package-managed / no-metadata
|
|
108
|
+
* case (reported as `managed`, never stale), and the SDK axis is already
|
|
109
|
+
* best-effort. Skills are intentionally absent — they ride along with `astrale
|
|
110
|
+
* update`, so a stale CLI or SDK is the only signal worth surfacing.
|
|
111
|
+
*/
|
|
112
|
+
export type StaleReport = {
|
|
113
|
+
stale: boolean
|
|
114
|
+
cli: { stale: boolean; managed: boolean; current?: string; latest?: string; channel?: string }
|
|
115
|
+
sdk: { stale: boolean; inProject: boolean; outdated: SdkOutdated[] }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function cliStale(opts: UpdateOpts): Promise<StaleReport['cli']> {
|
|
119
|
+
try {
|
|
120
|
+
const r = await updateAstrale({
|
|
121
|
+
check: true,
|
|
122
|
+
channel: opts.channel,
|
|
123
|
+
version: opts.version,
|
|
124
|
+
currentVersion: pkg.version,
|
|
125
|
+
})
|
|
126
|
+
// `check: true` never returns 'updated'; narrow for the type checker.
|
|
127
|
+
if (r.status === 'updated') return { stale: false, managed: false, current: r.currentVersion }
|
|
128
|
+
return {
|
|
129
|
+
stale: r.status === 'available',
|
|
130
|
+
managed: false,
|
|
131
|
+
current: r.currentVersion,
|
|
132
|
+
latest: r.latestVersion,
|
|
133
|
+
channel: r.channel,
|
|
134
|
+
}
|
|
135
|
+
} catch {
|
|
136
|
+
// A package-managed install (or missing metadata) can't self-update — not stale.
|
|
137
|
+
return { stale: false, managed: true }
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function sdkStale(): Promise<StaleReport['sdk']> {
|
|
142
|
+
if (!inDomainProject() || foreignPackageManager()) {
|
|
143
|
+
return { stale: false, inProject: inDomainProject(), outdated: [] }
|
|
144
|
+
}
|
|
145
|
+
const outdated = await findSdkOutdated()
|
|
146
|
+
return { stale: outdated.length > 0, inProject: true, outdated }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export default {
|
|
150
|
+
name: 'update',
|
|
151
|
+
description: 'Check for and install Astrale CLI updates',
|
|
152
|
+
options: [
|
|
153
|
+
{ flags: '--check', description: 'Only check whether an update is available' },
|
|
154
|
+
{
|
|
155
|
+
flags: '--channel <name>',
|
|
156
|
+
description: 'Update from a release channel (alpha, beta, rc, canary, stable)',
|
|
157
|
+
},
|
|
158
|
+
{ flags: '--version <version>', description: 'Update to an exact version tag' },
|
|
159
|
+
{ flags: '--no-skills', description: 'Skip refreshing the astrale agent skills' },
|
|
160
|
+
{ flags: '--no-deps', description: 'Skip checking @astrale-os SDK dependency versions' },
|
|
161
|
+
{
|
|
162
|
+
flags: '--yes',
|
|
163
|
+
description: 'Non-interactive: apply CLI + skills + SDK deps without prompts',
|
|
164
|
+
},
|
|
165
|
+
...RAW_OUTPUT_OPTIONS,
|
|
166
|
+
],
|
|
167
|
+
afterHelpText: `
|
|
168
|
+
Behavior:
|
|
169
|
+
Keeps three things current, in order. (1) The CLI binary: updates official
|
|
170
|
+
script installs only — if Astrale was installed by another package manager this
|
|
171
|
+
command refuses so that manager stays in charge; downloads are checksum-verified
|
|
172
|
+
before the binary is replaced. (2) The agent skills: if the astrale skills (cli +
|
|
173
|
+
domain) are already installed, refreshes them to the latest by delegating to
|
|
174
|
+
"npx skills add astrale-os/cli -g" — the same installer "astrale setup" uses;
|
|
175
|
+
fresh installs go through "astrale setup". (3) SDK deps: inside a pnpm domain
|
|
176
|
+
project, proposes any @astrale-os/* dependency with a newer release and, on
|
|
177
|
+
confirm, runs "pnpm update --latest --lockfile-only" (updates package.json AND
|
|
178
|
+
the lockfile, honoring your registry + supply-chain age policy; run "pnpm
|
|
179
|
+
install" to materialize).
|
|
180
|
+
|
|
181
|
+
--check is a dry run (binary + SDK deps; exit 10 if anything is available) and
|
|
182
|
+
never writes. With --json it emits a unified staleness report
|
|
183
|
+
({ stale, cli, sdk }) for tooling — non-throwing, skills omitted (they ride
|
|
184
|
+
along with an update). --yes applies all three non-interactively (no prompts) and
|
|
185
|
+
is resilient — a binary that can't self-update (package-managed) warns but never
|
|
186
|
+
blocks the skills/deps steps; this is what domain-studio's "Update now" runs.
|
|
187
|
+
--no-skills / --no-deps skip those steps in a real run.
|
|
188
|
+
|
|
189
|
+
Examples:
|
|
190
|
+
$ astrale update
|
|
191
|
+
$ astrale update --check
|
|
192
|
+
$ astrale update --yes
|
|
193
|
+
$ astrale update --no-skills
|
|
194
|
+
$ astrale update --no-deps
|
|
195
|
+
$ astrale update --channel canary
|
|
196
|
+
$ astrale update --version 0.4.0-alpha.10
|
|
197
|
+
`,
|
|
198
|
+
action: async (opts: UpdateOpts) => {
|
|
199
|
+
try {
|
|
200
|
+
// Tooling path: a machine-readable `--check` (e.g. domain-studio's update
|
|
201
|
+
// badge polling `astrale update --check --json`) gets a unified,
|
|
202
|
+
// non-throwing staleness report — CLI + SDK only — and exits.
|
|
203
|
+
if (opts.check && isMachine(opts)) {
|
|
204
|
+
const cli = await cliStale(opts)
|
|
205
|
+
const sdk = await sdkStale()
|
|
206
|
+
const report: StaleReport = { stale: cli.stale || sdk.stale, cli, sdk }
|
|
207
|
+
output(report, opts)
|
|
208
|
+
if (report.stale) process.exitCode = 10
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Axis A — the CLI binary. Isolated so that under --yes (a non-interactive
|
|
213
|
+
// full update, e.g. domain-studio's "Update now") a binary that can't
|
|
214
|
+
// self-update — package-managed, no metadata — WARNS but does not abort the
|
|
215
|
+
// skills/deps axes. Without --yes the error surfaces as before.
|
|
216
|
+
let result: Awaited<ReturnType<typeof updateAstrale>> | null = null
|
|
217
|
+
try {
|
|
218
|
+
result = await updateAstrale({
|
|
219
|
+
check: opts.check,
|
|
220
|
+
channel: opts.channel,
|
|
221
|
+
version: opts.version,
|
|
222
|
+
currentVersion: pkg.version,
|
|
223
|
+
})
|
|
224
|
+
} catch (e) {
|
|
225
|
+
if (!opts.yes) throw e
|
|
226
|
+
log.warn(`CLI self-update skipped: ${e instanceof Error ? e.message : String(e)}`)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Machine mode WITHOUT --yes (e.g. `astrale update --json`): emit the binary
|
|
230
|
+
// result and stop — never silently refresh skills / edit deps for a pipe.
|
|
231
|
+
if (result && isMachine(opts) && !opts.yes) {
|
|
232
|
+
output(result, opts)
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// `available` only happens under --check (a real run already swapped the
|
|
237
|
+
// binary and returns `updated`).
|
|
238
|
+
let anyAvailable = false
|
|
239
|
+
if (result?.status === 'available') {
|
|
240
|
+
log.info(
|
|
241
|
+
`Astrale update available: ${result.currentVersion} -> ${result.latestVersion} (${result.channel})`,
|
|
242
|
+
)
|
|
243
|
+
anyAvailable = true
|
|
244
|
+
} else if (result?.status === 'up-to-date') {
|
|
245
|
+
log.success(`Astrale is up to date: ${result.currentVersion}`)
|
|
246
|
+
} else if (result?.status === 'updated') {
|
|
247
|
+
log.success(`Updated astrale ${result.previousVersion} -> ${result.currentVersion}`)
|
|
248
|
+
log.dim(` channel: ${result.channel}`)
|
|
249
|
+
log.dim(` binary: ${result.bin}`)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Axis B — agent skills. Keep an existing install current with the CLI.
|
|
253
|
+
// Skip on --check (a dry run) and when the user opted out with --no-skills.
|
|
254
|
+
if (!opts.check && opts.skills !== false) await refreshSkills()
|
|
255
|
+
|
|
256
|
+
// Axis C — first-party @astrale-os/* deps in the current domain project.
|
|
257
|
+
// Runs on --check too (reports availability); --yes applies without a prompt.
|
|
258
|
+
if (opts.deps !== false && (await refreshSdkDeps(opts.check === true, opts.yes === true))) {
|
|
259
|
+
anyAvailable = true
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (opts.check && anyAvailable) process.exitCode = 10
|
|
263
|
+
} catch (e) {
|
|
264
|
+
fatal(e)
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
} satisfies CommandDefinition
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { CommandDefinition } from '../command'
|
|
2
|
+
|
|
3
|
+
import { setDefault } from '../lib/identity'
|
|
4
|
+
import { readIdentities } from '../lib/identity'
|
|
5
|
+
import { readInstances } from '../lib/instance'
|
|
6
|
+
import { fatal, log } from '../lib/log'
|
|
7
|
+
import { resolveUseTarget } from '../lib/use-target'
|
|
8
|
+
import instanceUseCommand from './instance/use'
|
|
9
|
+
|
|
10
|
+
type UseOpts = {
|
|
11
|
+
identity?: boolean
|
|
12
|
+
instance?: boolean
|
|
13
|
+
adoptDefault?: boolean
|
|
14
|
+
skipJwksCheck?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function useIdentity(name: string): Promise<void> {
|
|
18
|
+
await setDefault(name)
|
|
19
|
+
log.success(`Active identity set to "${name}"`)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default {
|
|
23
|
+
name: 'use',
|
|
24
|
+
description: 'Set the active identity or instance when the name is unambiguous',
|
|
25
|
+
afterHelpText: `
|
|
26
|
+
Behavior:
|
|
27
|
+
\`astrale use <name>\` is a convenience resolver for local CLI context. It
|
|
28
|
+
switches identity when <name> is only an identity, switches instance when
|
|
29
|
+
<name> is only an instance, and refuses ambiguous names. In scripts, prefer
|
|
30
|
+
the explicit namespace commands or pass --identity / --instance.
|
|
31
|
+
|
|
32
|
+
Examples:
|
|
33
|
+
$ astrale use staging
|
|
34
|
+
$ astrale use alice
|
|
35
|
+
$ astrale use staging --instance
|
|
36
|
+
$ astrale use alice --identity
|
|
37
|
+
`,
|
|
38
|
+
arguments: [{ name: 'name', description: 'Identity or instance name', required: true }],
|
|
39
|
+
options: [
|
|
40
|
+
{ flags: '--identity', description: 'Treat <name> as an identity' },
|
|
41
|
+
{ flags: '--instance', description: 'Treat <name> as an instance' },
|
|
42
|
+
{ flags: '--adopt-default', description: 'When using an instance, adopt its default identity' },
|
|
43
|
+
{ flags: '--skip-jwks-check', description: 'When using an instance, skip the JWKS check' },
|
|
44
|
+
],
|
|
45
|
+
action: async (name: string, opts: UseOpts) => {
|
|
46
|
+
try {
|
|
47
|
+
if (opts.identity && opts.instance) {
|
|
48
|
+
fatal('Choose either --identity or --instance, not both')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (opts.identity) {
|
|
52
|
+
await useIdentity(name)
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (opts.instance) {
|
|
57
|
+
await instanceUseCommand.action(name, opts)
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const [instances, identities] = await Promise.all([readInstances(), readIdentities()])
|
|
62
|
+
const target = resolveUseTarget(name, instances, identities)
|
|
63
|
+
|
|
64
|
+
if (target.kind === 'identity') {
|
|
65
|
+
await useIdentity(target.name)
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (target.kind === 'instance') {
|
|
70
|
+
await instanceUseCommand.action(target.name, opts)
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (target.kind === 'ambiguous') {
|
|
75
|
+
fatal(
|
|
76
|
+
`Both an identity and an instance are named "${name}". Use: astrale identity use ${name} or astrale instance use ${name}`,
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
fatal(
|
|
81
|
+
`No identity or instance named "${name}". Try: astrale identity list or astrale instance list`,
|
|
82
|
+
)
|
|
83
|
+
} catch (e) {
|
|
84
|
+
fatal(e)
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
} satisfies CommandDefinition
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export class AstraleError extends Error {
|
|
2
|
+
code: string
|
|
3
|
+
hint?: string
|
|
4
|
+
|
|
5
|
+
constructor(code: string, message: string, hint?: string) {
|
|
6
|
+
super(message)
|
|
7
|
+
this.name = 'AstraleError'
|
|
8
|
+
this.code = code
|
|
9
|
+
this.hint = hint
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class AuthError extends AstraleError {
|
|
14
|
+
constructor(message: string, hint?: string) {
|
|
15
|
+
super('AUTH_ERROR', message, hint ?? 'Run `astrale identity create <name>` to set up keys')
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ── Typed errors (DESIGN.md §4.6, §4.7, §5.1, §10, §11, §12) ─────────
|
|
20
|
+
|
|
21
|
+
export class IssuerUnreachableError extends AstraleError {
|
|
22
|
+
constructor(url: string, hint?: string) {
|
|
23
|
+
super(
|
|
24
|
+
'ISSUER_UNREACHABLE',
|
|
25
|
+
`Issuer endpoint "${url}" not reachable`,
|
|
26
|
+
hint ?? 'Check the target URL and issuer configuration',
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class ReservedSlugError extends AstraleError {
|
|
32
|
+
constructor(slug: string) {
|
|
33
|
+
super('RESERVED_SLUG', `Slug "${slug}" is reserved`, 'Pick a different slug')
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class IdentifierCollisionError extends AstraleError {
|
|
38
|
+
constructor(identifier: string, existingEntity: string) {
|
|
39
|
+
super(
|
|
40
|
+
'IDENTIFIER_COLLISION',
|
|
41
|
+
`Identifier "${identifier}" collides with existing ${existingEntity}`,
|
|
42
|
+
'Slug and name share the same CLI namespace (§4.7). Pick a unique value.',
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class IdentityKeyMissingError extends AstraleError {
|
|
48
|
+
constructor(subject: string) {
|
|
49
|
+
super(
|
|
50
|
+
'IDENTITY_KEY_MISSING',
|
|
51
|
+
`No private key on disk for identity "${subject}"`,
|
|
52
|
+
`Run \`astrale identity create ${subject}\` to generate one.`,
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class NotImplementedError extends AstraleError {
|
|
58
|
+
constructor(feature: string, hint?: string) {
|
|
59
|
+
super(
|
|
60
|
+
'NOT_IMPLEMENTED',
|
|
61
|
+
`"${feature}" is not implemented in v1`,
|
|
62
|
+
hint ?? 'Track the design implementation progress for updates',
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import type { AstraleConfig } from '../../lib/config'
|
|
4
|
+
import type { Identity } from '../../lib/identity'
|
|
5
|
+
|
|
6
|
+
import { resolveKeyIdentityAuthOptions } from '../auth'
|
|
7
|
+
|
|
8
|
+
const config: AstraleConfig = {
|
|
9
|
+
issuer: 'https://unregistered.invalid',
|
|
10
|
+
admin: {
|
|
11
|
+
name: 'admin',
|
|
12
|
+
url: 'https://admin.eu.astrale.ai/api',
|
|
13
|
+
issuer: 'https://admin.eu.astrale.ai/api',
|
|
14
|
+
},
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('resolveKeyIdentityAuthOptions', () => {
|
|
18
|
+
test('uses key identity issuer before global config issuer', () => {
|
|
19
|
+
const identity: Identity = {
|
|
20
|
+
subject: 'system',
|
|
21
|
+
source: 'key',
|
|
22
|
+
mode: 'local',
|
|
23
|
+
issuer: 'https://51-15-255-161.sslip.io',
|
|
24
|
+
createdAt: '2026-01-01T00:00:00.000Z',
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
expect(
|
|
28
|
+
resolveKeyIdentityAuthOptions(identity, config, 'https://51-15-255-161.sslip.io', 'scw-e2e'),
|
|
29
|
+
).toEqual({
|
|
30
|
+
issuer: 'https://51-15-255-161.sslip.io',
|
|
31
|
+
subject: undefined,
|
|
32
|
+
audience: 'https://51-15-255-161.sslip.io',
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('uses target audience as issuer for imported system bootstrap identities', () => {
|
|
37
|
+
const identity: Identity = {
|
|
38
|
+
subject: 'system',
|
|
39
|
+
source: 'key',
|
|
40
|
+
mode: 'local',
|
|
41
|
+
createdAt: '2026-01-01T00:00:00.000Z',
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
expect(
|
|
45
|
+
resolveKeyIdentityAuthOptions(identity, config, 'https://51-15-255-161.sslip.io', 'scw-e2e'),
|
|
46
|
+
).toEqual({
|
|
47
|
+
issuer: 'https://51-15-255-161.sslip.io',
|
|
48
|
+
subject: undefined,
|
|
49
|
+
audience: 'https://51-15-255-161.sslip.io',
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('registration issuer and subject still override identity issuer', () => {
|
|
54
|
+
const identity: Identity = {
|
|
55
|
+
subject: 'alice',
|
|
56
|
+
source: 'key',
|
|
57
|
+
mode: 'local',
|
|
58
|
+
issuer: 'https://identity.example.com',
|
|
59
|
+
createdAt: '2026-01-01T00:00:00.000Z',
|
|
60
|
+
registrations: {
|
|
61
|
+
prod: {
|
|
62
|
+
iss: 'https://prod.example.com/iss/thumbprint',
|
|
63
|
+
sub: 'node-1',
|
|
64
|
+
registeredAt: '2026-01-01T00:00:00.000Z',
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
expect(
|
|
70
|
+
resolveKeyIdentityAuthOptions(identity, config, 'https://prod.example.com', 'prod'),
|
|
71
|
+
).toEqual({
|
|
72
|
+
issuer: 'https://prod.example.com/iss/thumbprint',
|
|
73
|
+
subject: 'node-1',
|
|
74
|
+
audience: 'https://prod.example.com',
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
// We test the stripMethodSuffix behavior indirectly through the module's exports.
|
|
4
|
+
// Since stripMethodSuffix is private, we test the pattern directly.
|
|
5
|
+
const stripMethodSuffix = (msg: string): string => {
|
|
6
|
+
return msg.replace(/(\/[^"\s:]+)::([a-zA-Z]\w*)/g, '$1')
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe('stripMethodSuffix', () => {
|
|
10
|
+
test('strips ::listChildren from path', () => {
|
|
11
|
+
expect(stripMethodSuffix('Path not found: "/nonexistent::listChildren"')).toBe(
|
|
12
|
+
'Path not found: "/nonexistent"',
|
|
13
|
+
)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('strips ::get from path', () => {
|
|
17
|
+
expect(stripMethodSuffix('Path not found: "/some/path::get"')).toBe(
|
|
18
|
+
'Path not found: "/some/path"',
|
|
19
|
+
)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('strips any method name', () => {
|
|
23
|
+
expect(stripMethodSuffix('"/kernel.astrale.ai/Root::describe"')).toBe(
|
|
24
|
+
'"/kernel.astrale.ai/Root"',
|
|
25
|
+
)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('does not strip from non-path strings', () => {
|
|
29
|
+
expect(stripMethodSuffix('regular::text')).toBe('regular::text')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('handles multiple paths in one message', () => {
|
|
33
|
+
expect(stripMethodSuffix('"/a/b::get" and "/c/d::list"')).toBe('"/a/b" and "/c/d"')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('preserves messages without method suffixes', () => {
|
|
37
|
+
expect(stripMethodSuffix('No path found')).toBe('No path found')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('does not strip single colon (not a method dispatch)', () => {
|
|
41
|
+
expect(stripMethodSuffix('"/a/b:notMethod"')).toBe('"/a/b:notMethod"')
|
|
42
|
+
})
|
|
43
|
+
})
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { FnMap } from '@astrale-os/kernel-client'
|
|
2
|
+
import type { ClientSession } from '@astrale-os/kernel-client/session'
|
|
3
|
+
|
|
4
|
+
import { describe, expect, test } from 'bun:test'
|
|
5
|
+
|
|
6
|
+
import { mintDelegationPath } from '../remote-routing'
|
|
7
|
+
|
|
8
|
+
function unsignedJwt(payload: Record<string, unknown>): string {
|
|
9
|
+
const encode = (value: unknown) =>
|
|
10
|
+
Buffer.from(JSON.stringify(value)).toString('base64url').replace(/=+$/, '')
|
|
11
|
+
return `${encode({ alg: 'none' })}.${encode(payload)}.signature`
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** A ClientSession stub that records call paths and returns a canned whoami node. */
|
|
15
|
+
function stubClient(whoamiNode: unknown): {
|
|
16
|
+
client: ClientSession<FnMap>
|
|
17
|
+
calls: Array<{ path: string }>
|
|
18
|
+
} {
|
|
19
|
+
const calls: Array<{ path: string }> = []
|
|
20
|
+
const client = {
|
|
21
|
+
call: async (path: string) => {
|
|
22
|
+
calls.push({ path })
|
|
23
|
+
return whoamiNode
|
|
24
|
+
},
|
|
25
|
+
} as unknown as ClientSession<FnMap>
|
|
26
|
+
return { client, calls }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('mintDelegationPath — caller self-anchor resolution', () => {
|
|
30
|
+
test('resolves an IdP identity to its NODE id via whoami (not the JWT sub)', async () => {
|
|
31
|
+
// A WorkOS subject is the upstream user id, NOT the kernel node id — `@<sub>`
|
|
32
|
+
// would 404. whoami returns the caller's own identity node; its id is the anchor.
|
|
33
|
+
const { client, calls } = stubClient({ id: 'ef280077-6039-4a1a-9d06-403718d8e112' })
|
|
34
|
+
const credential = unsignedJwt({ sub: 'user_01KC9MW1M6S5J6V9ERSRJ8RDYF' })
|
|
35
|
+
|
|
36
|
+
expect(await mintDelegationPath(client, credential)).toBe(
|
|
37
|
+
'@ef280077-6039-4a1a-9d06-403718d8e112::mintDelegationCredential',
|
|
38
|
+
)
|
|
39
|
+
expect(calls).toEqual([{ path: '/:kernel.astrale.ai:interface.Identity:whoami' }])
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('legacy system subject maps to the __system__ graph node WITHOUT a whoami call', async () => {
|
|
43
|
+
const { client, calls } = stubClient({ id: 'unused' })
|
|
44
|
+
expect(await mintDelegationPath(client, unsignedJwt({ sub: 'system' }))).toBe(
|
|
45
|
+
'@__system__::mintDelegationCredential',
|
|
46
|
+
)
|
|
47
|
+
expect(calls).toEqual([])
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('opaque non-JWT credential falls back to __system__ (no whoami)', async () => {
|
|
51
|
+
const { client, calls } = stubClient({ id: 'unused' })
|
|
52
|
+
expect(await mintDelegationPath(client, 'not-a-jwt')).toBe(
|
|
53
|
+
'@__system__::mintDelegationCredential',
|
|
54
|
+
)
|
|
55
|
+
expect(calls).toEqual([])
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('throws when whoami returns no id (cannot address the caller)', async () => {
|
|
59
|
+
const { client } = stubClient({ notAnId: true })
|
|
60
|
+
await expect(mintDelegationPath(client, unsignedJwt({ sub: 'abc' }))).rejects.toThrow(
|
|
61
|
+
/could not resolve the caller identity/i,
|
|
62
|
+
)
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
// `lookupRemoteBinding` (the proactive `<path>::get` resolver) was removed: the
|
|
67
|
+
// kernel now carries the worker's `iss` on the redirect and the ClientSession
|
|
68
|
+
// follows it reactively, minting via `mintRemoteCredential` (the cache mint
|
|
69
|
+
// wired in `kernel/client.ts`). The slug→iss audience derivation it tested now
|
|
70
|
+
// lives in the kernel resolver (`runtime/__tests__/resolver-remote.test.ts`).
|