@astrale-os/cli 1.0.0-beta.0 → 1.0.0-beta.10
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/README.md +23 -12
- package/dist/astrale.js +51494 -56307
- package/dist/public/connect-core.js +2028 -26846
- package/dist/public/keys/index.js +1755 -40694
- package/dist/public/paths/index.js +859 -39518
- package/dist/types/admin/binding.d.ts +19 -0
- package/dist/types/admin/instance/client.d.ts +22 -0
- package/dist/types/admin/instance/index.d.ts +2 -0
- package/dist/types/admin/instance/model.d.ts +5 -12
- package/dist/types/connection/auth.d.ts +3 -0
- package/dist/types/connection/call.d.ts +3 -0
- package/dist/types/connection/command.d.ts +27 -0
- package/dist/types/connection/credential.d.ts +14 -0
- package/dist/types/connection/errors.d.ts +1 -0
- package/dist/types/connection/exchange.d.ts +9 -0
- package/dist/types/connection/failure/classify.d.ts +2 -0
- package/dist/types/connection/failure/index.d.ts +5 -0
- package/dist/types/connection/failure/model.d.ts +25 -0
- package/dist/types/connection/failure/render.d.ts +3 -0
- package/dist/types/connection/index.d.ts +8 -0
- package/dist/types/connection/reasons.d.ts +37 -0
- package/dist/types/connection/self.d.ts +37 -0
- package/dist/types/connection/session.d.ts +31 -0
- package/dist/types/connection/target.d.ts +38 -0
- package/dist/types/errors.d.ts +1 -1
- package/dist/types/lib/admin-instance.d.ts +17 -0
- package/dist/types/lib/admin-target.d.ts +6 -2
- package/dist/types/lib/failure-debug.d.ts +1 -0
- package/dist/types/lib/idp.d.ts +7 -0
- package/dist/types/lib/instance-target.d.ts +1 -1
- package/dist/types/lib/instance.d.ts +10 -2
- package/dist/types/lib/log.d.ts +5 -3
- package/dist/types/lib/meta.d.ts +2 -2
- package/dist/types/lib/proc.d.ts +43 -0
- package/dist/types/lib/self.d.ts +9 -0
- package/dist/types/lib/update.d.ts +131 -0
- package/package.json +8 -10
- package/src/__tests__/fixtures/publication.ts +20 -0
- package/src/admin/__tests__/binding.test.ts +31 -0
- package/src/admin/__tests__/fixture.ts +101 -0
- package/src/admin/binding.ts +98 -0
- package/src/admin/catalog/.spec/api.d.ts +2 -2
- package/src/admin/catalog/__tests__/client.test.ts +20 -49
- package/src/admin/catalog/client.ts +36 -32
- package/src/admin/catalog/model.ts +3 -4
- package/src/admin/graph/.spec/api.d.ts +4 -10
- package/src/admin/graph/nodes.ts +1 -1
- package/src/admin/instance/.spec/api.d.ts +2 -2
- package/src/admin/instance/__tests__/client.test.ts +40 -163
- package/src/admin/instance/client.ts +35 -137
- package/src/admin/instance/index.ts +1 -2
- package/src/admin/instance/model.ts +11 -20
- package/src/commands/__tests__/admin-instance.test.ts +11 -8
- package/src/commands/__tests__/auth-login.test.ts +54 -1
- package/src/commands/__tests__/call-describe.test.ts +46 -33
- package/src/commands/__tests__/call.test.ts +5 -1
- package/src/commands/__tests__/domain-install-operation.test.ts +12 -7
- package/src/commands/__tests__/domain-install-owned.test.ts +6 -2
- package/src/commands/__tests__/domain-list.test.ts +63 -4
- package/src/commands/__tests__/install-identity-override.test.ts +19 -19
- package/src/commands/__tests__/instance-list-rows.test.ts +1 -1
- package/src/commands/__tests__/instance-status.test.ts +139 -0
- package/src/commands/__tests__/read-commands.test.ts +35 -10
- package/src/commands/__tests__/studio-descriptor.test.ts +22 -0
- package/src/commands/__tests__/update.test.ts +95 -0
- package/src/commands/call-describe.ts +60 -68
- package/src/commands/call.ts +43 -56
- package/src/commands/domain/install.ts +13 -17
- package/src/commands/domain/list.ts +18 -13
- package/src/commands/domain/publish.ts +14 -11
- package/src/commands/domain/uninstall.ts +2 -2
- package/src/commands/get.ts +14 -15
- package/src/commands/identity/__tests__/register.test.ts +3 -3
- package/src/commands/identity/register.ts +5 -4
- package/src/commands/instance/create.ts +8 -16
- package/src/commands/instance/delete.ts +12 -9
- package/src/commands/instance/list.ts +2 -2
- package/src/commands/instance/status.ts +66 -20
- package/src/commands/instance/use.ts +16 -12
- package/src/commands/introspect.ts +9 -16
- package/src/commands/logs.ts +24 -25
- package/src/commands/mutate.ts +6 -5
- package/src/commands/query.ts +27 -30
- package/src/commands/studio.ts +20 -0
- package/src/commands/token.ts +8 -7
- package/src/commands/update.ts +68 -22
- package/src/commands/view.ts +7 -6
- package/src/connection/.spec/api.d.ts +7 -8
- package/src/connection/.spec/architecture.md +5 -4
- package/src/connection/.spec/flows/session.ts +2 -2
- package/src/connection/.spec/laws/connection.ts +6 -2
- package/src/connection/.spec/layout.ts +1 -0
- package/src/connection/__tests__/credential.test.ts +20 -1
- package/src/connection/__tests__/errors.test.ts +43 -32
- package/src/connection/__tests__/exchange.test.ts +36 -6
- package/src/connection/__tests__/failure-fixtures.ts +39 -0
- package/src/connection/__tests__/failure-safety.test.ts +75 -0
- package/src/connection/__tests__/self.test.ts +31 -11
- package/src/connection/__tests__/session.test.ts +3 -3
- package/src/connection/__tests__/target.test.ts +1 -0
- package/src/connection/auth.ts +13 -0
- package/src/connection/call.ts +1 -1
- package/src/connection/credential.ts +2 -2
- package/src/connection/errors.ts +1 -355
- package/src/connection/exchange.ts +52 -27
- package/src/connection/failure/classify.ts +110 -0
- package/src/connection/failure/index.ts +18 -0
- package/src/connection/failure/model.ts +28 -0
- package/src/connection/failure/render.ts +116 -0
- package/src/connection/reasons.ts +16 -0
- package/src/connection/self.ts +39 -21
- package/src/connection/session.ts +19 -8
- package/src/errors.ts +3 -2
- package/src/graph/.spec/api.d.ts +12 -5
- package/src/graph/.spec/layout.ts +9 -1
- package/src/graph/__tests__/mutation.test.ts +2 -2
- package/src/graph/__tests__/query.test.ts +25 -6
- package/src/graph/class.ts +30 -0
- package/src/graph/index.ts +1 -0
- package/src/graph/query.ts +21 -32
- package/src/identity/.spec/api.d.ts +2 -2
- package/src/identity/__tests__/registration.test.ts +1 -1
- package/src/identity/registration.ts +3 -3
- package/src/keys/__tests__/keys.test.ts +4 -0
- package/src/keys/credential.ts +1 -0
- package/src/lib/__tests__/admin-target.test.ts +12 -0
- package/src/lib/__tests__/command-dx.test.ts +1 -1
- package/src/lib/__tests__/domain-publication.test.ts +138 -0
- package/src/lib/__tests__/idp-session.test.ts +22 -0
- package/src/lib/__tests__/idp.test.ts +36 -4
- package/src/lib/__tests__/instance-candidates.test.ts +1 -3
- package/src/lib/__tests__/instance-target.test.ts +7 -39
- package/src/lib/__tests__/instance.test.ts +6 -0
- package/src/lib/__tests__/log-errors.test.ts +64 -0
- package/src/lib/__tests__/studio-server-deps.test.ts +53 -16
- package/src/lib/__tests__/update.test.ts +113 -8
- package/src/lib/__tests__/view-open-intent.test.ts +0 -2
- package/src/lib/__tests__/view-server.test.ts +1 -1
- package/src/lib/__tests__/view-session.test.ts +1 -1
- package/src/lib/admin-instance.ts +14 -8
- package/src/lib/admin-target.ts +21 -2
- package/src/lib/command-dx.ts +9 -9
- package/src/lib/domain-publication.ts +104 -0
- package/src/lib/failure-debug.ts +26 -0
- package/src/lib/idp.ts +39 -4
- package/src/lib/instance-candidates.ts +2 -2
- package/src/lib/instance-target.ts +3 -14
- package/src/lib/instance.ts +22 -12
- package/src/lib/log.ts +16 -9
- package/src/lib/login-flow.ts +13 -3
- package/src/lib/meta.ts +2 -2
- package/src/lib/provision-instance.ts +34 -57
- package/src/lib/update.ts +124 -30
- package/src/lib/validation.ts +2 -2
- package/src/lib/view/resolve.ts +1 -1
- package/src/program/__tests__/program.test.ts +20 -4
- package/src/program/build.ts +2 -2
- package/src/setup/__tests__/instance-step.test.ts +27 -10
- package/src/setup/steps/instance.ts +22 -15
- package/studio/client/dist/assets/{elk-api-D2xgMJvi.js → elk-api-ByzoDZWH.js} +1 -1
- package/studio/client/dist/assets/index-B0RCgUOT.js +8 -0
- package/studio/client/dist/assets/index-BiGR8Oo9.css +1 -0
- package/studio/client/dist/assets/index-N8J8EKlB.js +81 -0
- package/studio/client/dist/index.html +2 -2
- package/studio/package.json +5 -10
- package/studio/server/agent/ask.test.ts +1 -1
- package/studio/server/agent/bridge/grant.test.ts +1 -1
- package/studio/server/agent/bridge/routes.test.ts +1 -1
- package/studio/server/agent/bridge/stdio.ts +8 -1
- package/studio/server/agent/conversation.test.ts +10 -5
- package/studio/server/agent/conversation.ts +56 -8
- package/studio/server/agent/harness/claude/adapter.test.ts +15 -5
- package/studio/server/agent/harness/codex/loadout.ts +18 -21
- package/studio/server/agent/harness/gateway/config.ts +20 -18
- package/studio/server/agent/harness/selection.ts +9 -1
- package/studio/server/agent/prompts/anchors.test.ts +32 -36
- package/studio/server/agent/prompts/anchors.ts +20 -49
- package/studio/server/agent/prompts/system.test.ts +3 -2
- package/studio/server/agent/prompts/system.ts +6 -5
- package/studio/server/agent/prompts/turn.ts +13 -4
- package/studio/server/agent/routes.test.ts +1 -1
- package/studio/server/agent/run/completion.ts +2 -2
- package/studio/server/agent/run/coordinator.test.ts +1 -1
- package/studio/server/agent/run/preparation.ts +6 -5
- package/studio/server/agent/run/transcript.test.ts +35 -1
- package/studio/server/agent/run/transcript.ts +169 -3
- package/studio/server/agent/run/usage.test.ts +24 -0
- package/studio/server/agent/run/usage.ts +26 -1
- package/studio/server/api/canvas.ts +43 -0
- package/studio/server/api/comments.ts +85 -0
- package/studio/server/api/context.ts +61 -0
- package/studio/server/api/deployment.ts +39 -0
- package/studio/server/api/documents.ts +55 -0
- package/studio/server/api/domain.ts +59 -0
- package/studio/server/api/http.ts +45 -0
- package/studio/server/api/project.ts +57 -0
- package/studio/server/api/schema.ts +12 -0
- package/studio/server/api/views.ts +57 -0
- package/studio/server/api/workspace.ts +60 -0
- package/studio/server/api-agent-loadout.test.ts +1 -1
- package/studio/server/api.test.ts +147 -0
- package/studio/server/api.ts +20 -422
- package/studio/server/cache.test.ts +122 -0
- package/studio/server/cache.ts +162 -11
- package/studio/server/cli-consumers.test.ts +494 -0
- package/studio/server/cli.test.ts +88 -0
- package/studio/server/cli.ts +240 -0
- package/studio/server/client-package.ts +25 -1
- package/studio/server/domain.test.ts +50 -40
- package/studio/server/domain.ts +83 -52
- package/studio/server/environment/dotenv-preview.test.ts +39 -0
- package/studio/server/environment/dotenv-preview.ts +27 -0
- package/studio/server/environment/files.test.ts +53 -0
- package/studio/server/{state/env.ts → environment/files.ts} +7 -48
- package/studio/server/{state → handoff}/copy.ts +21 -6
- package/studio/server/handoff/service.test.ts +22 -0
- package/studio/server/{state/handoff.ts → handoff/service.ts} +21 -14
- package/studio/server/index.ts +1 -1
- package/studio/server/instances/active.ts +72 -0
- package/studio/server/instances/deploy-record.test.ts +31 -0
- package/studio/server/instances/deploy-record.ts +37 -0
- package/studio/server/instances/deploy.ts +56 -0
- package/studio/server/{state/instance.test.ts → instances/probe.test.ts} +25 -21
- package/studio/server/instances/probe.ts +67 -0
- package/studio/server/instances/status.test.ts +12 -0
- package/studio/server/instances/status.ts +49 -0
- package/studio/server/introspect/anatomy/client-tree.ts +57 -0
- package/studio/server/introspect/anatomy/env-fields.ts +66 -0
- package/studio/server/introspect/anatomy/schema-definition.ts +53 -0
- package/studio/server/introspect/anatomy/source.ts +181 -0
- package/studio/server/introspect/anatomy/views/routes.ts +114 -0
- package/studio/server/introspect/anatomy/views.ts +53 -0
- package/studio/server/introspect/anatomy-extras.test.ts +106 -35
- package/studio/server/introspect/anatomy-extras.ts +7 -726
- package/studio/server/introspect/anatomy.test.ts +22 -14
- package/studio/server/introspect/anatomy.ts +52 -28
- package/studio/server/introspect/bundle.ts +15 -9
- package/studio/server/introspect/canonical-schema.test.ts +93 -309
- package/studio/server/introspect/canonical-schema.ts +257 -425
- package/studio/server/introspect/config-preview.test.ts +32 -0
- package/studio/server/introspect/config-preview.ts +119 -0
- package/studio/server/introspect/core-extractor.ts +18 -127
- package/studio/server/introspect/core.ts +1 -2
- package/studio/server/introspect/diff.test.ts +18 -18
- package/studio/server/introspect/diff.ts +44 -90
- package/studio/server/introspect/extractor.ts +23 -116
- package/studio/server/introspect/hash.ts +5 -2
- package/studio/server/introspect/overlay-tsmorph.test.ts +119 -240
- package/studio/server/introspect/overlay-tsmorph.ts +7 -1305
- package/studio/server/introspect/overlay.test.ts +26 -54
- package/studio/server/introspect/overlay.ts +10 -37
- package/studio/server/introspect/revision.test.ts +54 -0
- package/studio/server/introspect/revision.ts +49 -0
- package/studio/server/introspect/runtime.test.ts +83 -143
- package/studio/server/introspect/runtime.ts +40 -11
- package/studio/server/introspect/schema-ir-json.test.ts +70 -0
- package/studio/server/introspect/schema-ir-json.ts +151 -0
- package/studio/server/introspect/schema-refs.test.ts +43 -88
- package/studio/server/introspect/schema-refs.ts +14 -49
- package/studio/server/introspect/source-overlay/annotations.ts +14 -0
- package/studio/server/introspect/source-overlay/handlers.ts +161 -0
- package/studio/server/introspect/source-overlay/kernel-calls.ts +46 -0
- package/studio/server/introspect/source-overlay/project.ts +248 -0
- package/studio/server/introspect/source-overlay/spans.ts +330 -0
- package/studio/server/json.ts +46 -0
- package/studio/server/lifecycle.ts +5 -1
- package/studio/server/sse.test.ts +26 -0
- package/studio/server/sse.ts +4 -1
- package/studio/server/state/baseline.test.ts +223 -7
- package/studio/server/state/baseline.ts +148 -34
- package/studio/server/state/comments.test.ts +66 -1
- package/studio/server/state/comments.ts +173 -10
- package/studio/server/state/context.ts +34 -2
- package/studio/server/state/documents.ts +42 -1
- package/studio/server/state/integrations.ts +27 -1
- package/studio/server/state/layout.test.ts +34 -0
- package/studio/server/state/layout.ts +44 -17
- package/studio/server/state/settings.ts +29 -22
- package/studio/server/state/store.test.ts +122 -0
- package/studio/server/state/store.ts +66 -17
- package/studio/server/state/visibility.ts +22 -11
- package/studio/server/views/model.test.ts +57 -0
- package/studio/server/views/model.ts +66 -0
- package/studio/server/views/runtime.ts +40 -0
- package/studio/server/views/selection-repository.test.ts +31 -0
- package/studio/server/views/selection-repository.ts +60 -0
- package/studio/server/views/session.test.ts +73 -0
- package/studio/server/views/session.ts +166 -0
- package/studio/server/views/target.test.ts +78 -0
- package/studio/server/views/target.ts +222 -0
- package/studio/server/watch.test.ts +11 -11
- package/studio/server/watch.ts +9 -5
- package/studio/server/workspace/catalog.test.ts +17 -0
- package/studio/server/workspace/catalog.ts +43 -0
- package/studio/server/{state → workspace}/create.test.ts +1 -1
- package/studio/server/{state → workspace}/create.ts +1 -1
- package/studio/server/{state → workspace}/git.ts +1 -1
- package/studio/server/workspace/updates.ts +83 -0
- package/studio/server/workspace-state.ts +1 -1
- package/studio/server/workspace-watch.ts +2 -2
- package/studio/shared/contracts/README.md +16 -0
- package/studio/shared/contracts/agent.ts +270 -0
- package/studio/shared/contracts/runtime.ts +50 -0
- package/studio/shared/contracts/schema.ts +393 -0
- package/studio/shared/contracts/surface.test.ts +317 -0
- package/studio/shared/contracts/workspace.ts +265 -0
- package/studio/shared/layout.test.ts +116 -0
- package/studio/shared/schema/identity.test.ts +34 -0
- package/studio/shared/schema/identity.ts +68 -0
- package/studio/shared/types.ts +11 -1099
- package/studio/tsconfig.json +1 -1
- package/viewer/dist/main.js +40 -61
- package/.check-workspace.cjs +0 -40
- package/src/commands/__tests__/instance-create-hosts.test.ts +0 -29
- package/studio/client/dist/assets/index-BMdnsIJA.css +0 -1
- package/studio/client/dist/assets/index-D-vRV8w7.js +0 -8
- package/studio/client/dist/assets/index-LGSWRrk8.js +0 -81
- package/studio/server/state/catalog.ts +0 -117
- package/studio/server/state/instance.ts +0 -280
- package/studio/server/state/updates.ts +0 -63
- package/studio/server/state/views.test.ts +0 -286
- package/studio/server/state/views.ts +0 -535
package/src/lib/command-dx.ts
CHANGED
|
@@ -2,8 +2,6 @@ import type { Command, CommanderError } from 'commander'
|
|
|
2
2
|
|
|
3
3
|
import chalk from 'chalk'
|
|
4
4
|
|
|
5
|
-
import { isMachine } from './output'
|
|
6
|
-
|
|
7
5
|
export type CommandCatalogEntry = {
|
|
8
6
|
path: string[]
|
|
9
7
|
usage: string
|
|
@@ -30,27 +28,27 @@ export function renderCommanderError(
|
|
|
30
28
|
program: Command,
|
|
31
29
|
error: CommanderError,
|
|
32
30
|
argv = process.argv.slice(2),
|
|
31
|
+
machine = argv.some((token) => token === '--ci' || token === '--json' || token === '--raw') ||
|
|
32
|
+
!(process.stdout.isTTY ?? false),
|
|
33
33
|
): string {
|
|
34
34
|
const tokens = stripOptions(argv)
|
|
35
35
|
const catalog = collectCommandCatalog(program)
|
|
36
36
|
const matched = matchRegisteredPrefix(program, tokens)
|
|
37
37
|
|
|
38
38
|
if (matched.path.length === 0 && tokens.length > 0) {
|
|
39
|
-
return maybeMachine(renderUnknownCommand(tokens, catalog))
|
|
39
|
+
return maybeMachine(renderUnknownCommand(tokens, catalog), machine)
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
if (error.code === 'commander.missingArgument') {
|
|
43
43
|
const usage = usageFor(matched.path, matched.command)
|
|
44
|
-
const argName = error.message.match(/'([^']+)'/)?.[1]
|
|
45
44
|
return maybeMachine(
|
|
46
45
|
[
|
|
47
|
-
`Missing required argument
|
|
48
|
-
`astrale ${matched.path.join(' ')}`,
|
|
49
|
-
)}`,
|
|
46
|
+
`Missing required argument for ${chalk.bold(`astrale ${matched.path.join(' ')}`)}`,
|
|
50
47
|
'',
|
|
51
48
|
'Usage:',
|
|
52
49
|
` astrale ${usage}`,
|
|
53
50
|
].join('\n'),
|
|
51
|
+
machine,
|
|
54
52
|
)
|
|
55
53
|
}
|
|
56
54
|
|
|
@@ -66,6 +64,7 @@ export function renderCommanderError(
|
|
|
66
64
|
'Usage:',
|
|
67
65
|
` astrale ${usage}`,
|
|
68
66
|
].join('\n'),
|
|
67
|
+
machine,
|
|
69
68
|
)
|
|
70
69
|
}
|
|
71
70
|
|
|
@@ -77,6 +76,7 @@ export function renderCommanderError(
|
|
|
77
76
|
? ['', 'Did you mean:', ...suggestions.map((s) => ` astrale ${s}`)]
|
|
78
77
|
: []),
|
|
79
78
|
].join('\n'),
|
|
79
|
+
machine,
|
|
80
80
|
)
|
|
81
81
|
}
|
|
82
82
|
|
|
@@ -143,8 +143,8 @@ function usageFor(path: string[], command: Command): string {
|
|
|
143
143
|
|
|
144
144
|
const ANSI_RE = new RegExp(String.fromCharCode(27) + '\\[[0-9;]*m', 'g')
|
|
145
145
|
|
|
146
|
-
function maybeMachine(text: string): string {
|
|
147
|
-
if (!
|
|
146
|
+
function maybeMachine(text: string, machine: boolean): string {
|
|
147
|
+
if (!machine) return text
|
|
148
148
|
const plain = text.replace(ANSI_RE, '')
|
|
149
149
|
const first = plain.split('\n').find((line) => line.trim().length > 0) ?? plain
|
|
150
150
|
return JSON.stringify({ error: 'USAGE_ERROR', message: first, detail: plain })
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { publication } from '@astrale-os/sdk/publication'
|
|
2
|
+
|
|
3
|
+
const MAXIMUM_PUBLICATION_BYTES = 1024 * 1024
|
|
4
|
+
type FetchLike = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
|
|
5
|
+
|
|
6
|
+
/** Fetch and admit one canonical Domain Publication without following its bundle. */
|
|
7
|
+
export async function fetchDomainPublication(
|
|
8
|
+
input: string,
|
|
9
|
+
signal?: AbortSignal,
|
|
10
|
+
fetchImpl: FetchLike = globalThis.fetch,
|
|
11
|
+
): Promise<publication.Publication> {
|
|
12
|
+
const url = domainPublicationUrl(input)
|
|
13
|
+
const response = await fetchImpl(url, {
|
|
14
|
+
redirect: 'error',
|
|
15
|
+
...(signal === undefined ? {} : { signal }),
|
|
16
|
+
})
|
|
17
|
+
if (!response.ok) {
|
|
18
|
+
await cancel(response.body)
|
|
19
|
+
throw new Error(`GET ${url.href} → ${response.status}`)
|
|
20
|
+
}
|
|
21
|
+
const bytes = await readBounded(response, url)
|
|
22
|
+
try {
|
|
23
|
+
const source = new TextDecoder('utf-8', { fatal: true }).decode(bytes)
|
|
24
|
+
return publication.accept(JSON.parse(source) as unknown)
|
|
25
|
+
} catch (cause) {
|
|
26
|
+
throw new Error(`GET ${url.href} returned an invalid Domain Publication`, { cause })
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function readBounded(response: Response, url: URL): Promise<Uint8Array> {
|
|
31
|
+
const declared = response.headers.get('content-length')
|
|
32
|
+
if (
|
|
33
|
+
declared !== null &&
|
|
34
|
+
(!/^\d+$/u.test(declared) ||
|
|
35
|
+
!Number.isSafeInteger(Number(declared)) ||
|
|
36
|
+
Number(declared) > MAXIMUM_PUBLICATION_BYTES)
|
|
37
|
+
) {
|
|
38
|
+
await cancel(response.body)
|
|
39
|
+
throw sizeError(url)
|
|
40
|
+
}
|
|
41
|
+
if (response.body === null) return new Uint8Array()
|
|
42
|
+
|
|
43
|
+
const reader = response.body.getReader()
|
|
44
|
+
const chunks: Uint8Array[] = []
|
|
45
|
+
let size = 0
|
|
46
|
+
let complete = false
|
|
47
|
+
try {
|
|
48
|
+
for (;;) {
|
|
49
|
+
const next = await reader.read()
|
|
50
|
+
if (next.done) {
|
|
51
|
+
complete = true
|
|
52
|
+
break
|
|
53
|
+
}
|
|
54
|
+
if (next.value.byteLength === 0) continue
|
|
55
|
+
if (next.value.byteLength > MAXIMUM_PUBLICATION_BYTES - size) {
|
|
56
|
+
throw sizeError(url)
|
|
57
|
+
}
|
|
58
|
+
chunks.push(next.value)
|
|
59
|
+
size += next.value.byteLength
|
|
60
|
+
}
|
|
61
|
+
} finally {
|
|
62
|
+
if (!complete) await reader.cancel().catch(() => undefined)
|
|
63
|
+
reader.releaseLock()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const bytes = new Uint8Array(size)
|
|
67
|
+
let offset = 0
|
|
68
|
+
for (const chunk of chunks) {
|
|
69
|
+
bytes.set(chunk, offset)
|
|
70
|
+
offset += chunk.byteLength
|
|
71
|
+
}
|
|
72
|
+
return bytes
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function sizeError(url: URL): Error {
|
|
76
|
+
return new Error(`GET ${url.href} exceeded ${MAXIMUM_PUBLICATION_BYTES} bytes`)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function cancel(body: ReadableStream<Uint8Array> | null): Promise<void> {
|
|
80
|
+
if (body !== null) await body.cancel().catch(() => undefined)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Canonical discovery document for a Domain origin or exact Publication URL. */
|
|
84
|
+
export function domainPublicationUrl(input: string): URL {
|
|
85
|
+
let url: URL
|
|
86
|
+
try {
|
|
87
|
+
url = new URL(input)
|
|
88
|
+
} catch {
|
|
89
|
+
throw new TypeError(`Domain deployment URL is invalid: ${input}`)
|
|
90
|
+
}
|
|
91
|
+
if (
|
|
92
|
+
!['http:', 'https:'].includes(url.protocol) ||
|
|
93
|
+
url.username !== '' ||
|
|
94
|
+
url.password !== '' ||
|
|
95
|
+
url.search !== '' ||
|
|
96
|
+
url.hash !== '' ||
|
|
97
|
+
!['', '/', publication.PATH].includes(url.pathname)
|
|
98
|
+
) {
|
|
99
|
+
throw new TypeError(
|
|
100
|
+
`Domain deployment URL must be an HTTP(S) origin or canonical Publication URL: ${input}`,
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
return new URL(publication.PATH, url.origin)
|
|
104
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
|
|
3
|
+
const MAXIMUM_DEBUG_FAILURES = 32
|
|
4
|
+
|
|
5
|
+
export function printFailureDebug(error: unknown, url: string): void {
|
|
6
|
+
process.stderr.write(`\n${chalk.dim('── debug ─────────────────')}\n`)
|
|
7
|
+
if (url) process.stderr.write(`${chalk.dim(`url: ${url}`)}\n`)
|
|
8
|
+
const seen = new Set<unknown>()
|
|
9
|
+
const pending: Array<{ value: unknown; relation: string }> = [{ value: error, relation: 'error' }]
|
|
10
|
+
while (pending.length > 0 && seen.size < MAXIMUM_DEBUG_FAILURES) {
|
|
11
|
+
const { value, relation } = pending.shift()!
|
|
12
|
+
if (seen.has(value)) continue
|
|
13
|
+
seen.add(value)
|
|
14
|
+
if (!(value instanceof Error)) {
|
|
15
|
+
process.stderr.write(`${chalk.dim(`${relation}: ${String(value)}`)}\n`)
|
|
16
|
+
continue
|
|
17
|
+
}
|
|
18
|
+
process.stderr.write(
|
|
19
|
+
`${chalk.dim(`${relation}: ${value.constructor.name}: ${value.message}`)}\n`,
|
|
20
|
+
)
|
|
21
|
+
if (value.stack) process.stderr.write(`${chalk.dim(value.stack)}\n`)
|
|
22
|
+
if (value.cause !== undefined) pending.push({ value: value.cause, relation: 'caused by' })
|
|
23
|
+
if (value instanceof AggregateError)
|
|
24
|
+
for (const child of value.errors) pending.push({ value: child, relation: 'aggregate' })
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/lib/idp.ts
CHANGED
|
@@ -145,6 +145,8 @@ export const IdpSessionSchema = z
|
|
|
145
145
|
idp: z.string(),
|
|
146
146
|
issuer: z.string().url(),
|
|
147
147
|
subject: z.string(),
|
|
148
|
+
/** OAuth client that issued this session's refresh token. */
|
|
149
|
+
clientId: z.string().optional(),
|
|
148
150
|
audience: z.string().optional(),
|
|
149
151
|
organizationId: z.string().optional(),
|
|
150
152
|
access_token: z.string(),
|
|
@@ -212,6 +214,8 @@ export type IdpConfig = {
|
|
|
212
214
|
|
|
213
215
|
export const BUILTIN_WORKOS_IDP_NAME = 'workos'
|
|
214
216
|
const DEFAULT_WORKOS_API_HOST = 'https://api.workos.com'
|
|
217
|
+
const DEFAULT_WORKOS_CLIENT_ID = 'client_01KC29HET5F3QAQ8GNTPZ7F320'
|
|
218
|
+
const LEGACY_WORKOS_CLIENT_ID = 'client_01KC29HEGD7B40TV2C4QZ436BG'
|
|
215
219
|
const WORKOS_CLIENT_ID_ENV_NAMES = ['WORKOS_CLIENT_ID', 'VITE_WORKOS_CLIENT_ID'] as const
|
|
216
220
|
|
|
217
221
|
export function idpDir(name: string): string {
|
|
@@ -278,7 +282,17 @@ export async function readIdpConfigOrBuiltin(
|
|
|
278
282
|
validateName(name, 'IdP')
|
|
279
283
|
const store = await readIdpStore()
|
|
280
284
|
if (store.idps[name]) {
|
|
281
|
-
|
|
285
|
+
const existing = await readIdpConfig(name)
|
|
286
|
+
const replacement = opts.persist
|
|
287
|
+
? legacyBuiltinWorkosReplacement(existing, opts.clientId)
|
|
288
|
+
: undefined
|
|
289
|
+
if (!replacement) return existing
|
|
290
|
+
return upsertIdpConfig({
|
|
291
|
+
name: replacement.name,
|
|
292
|
+
metadata: replacement.metadata,
|
|
293
|
+
client: replacement.client,
|
|
294
|
+
builtIn: true,
|
|
295
|
+
})
|
|
282
296
|
}
|
|
283
297
|
|
|
284
298
|
const builtin = builtinIdpConfig(name, opts.clientId)
|
|
@@ -411,11 +425,30 @@ export function builtinIdpConfig(
|
|
|
411
425
|
}
|
|
412
426
|
|
|
413
427
|
export function workosClientIdFromEnv(env: NodeJS.ProcessEnv = process.env): string | undefined {
|
|
414
|
-
return
|
|
415
|
-
(
|
|
428
|
+
return (
|
|
429
|
+
WORKOS_CLIENT_ID_ENV_NAMES.map((name) => env[name]?.trim()).find(
|
|
430
|
+
(value): value is string => typeof value === 'string' && value.length > 0,
|
|
431
|
+
) ?? DEFAULT_WORKOS_CLIENT_ID
|
|
416
432
|
)
|
|
417
433
|
}
|
|
418
434
|
|
|
435
|
+
export function legacyBuiltinWorkosReplacement(
|
|
436
|
+
existing: IdpConfig,
|
|
437
|
+
clientIdOverride?: string,
|
|
438
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
439
|
+
): IdpConfig | undefined {
|
|
440
|
+
if (
|
|
441
|
+
existing.name !== BUILTIN_WORKOS_IDP_NAME ||
|
|
442
|
+
!existing.entry.builtIn ||
|
|
443
|
+
existing.client.client_id !== LEGACY_WORKOS_CLIENT_ID
|
|
444
|
+
) {
|
|
445
|
+
return undefined
|
|
446
|
+
}
|
|
447
|
+
const replacement = builtinIdpConfig(existing.name, clientIdOverride, env)
|
|
448
|
+
if (!replacement || replacement.client.client_id === LEGACY_WORKOS_CLIENT_ID) return undefined
|
|
449
|
+
return replacement
|
|
450
|
+
}
|
|
451
|
+
|
|
419
452
|
async function fetchOAuthAuthorizationServerMetadata(
|
|
420
453
|
issuer: string,
|
|
421
454
|
): Promise<Partial<OidcMetadata> | undefined> {
|
|
@@ -644,10 +677,11 @@ export async function refreshSession(
|
|
|
644
677
|
if (!session.refresh_token)
|
|
645
678
|
throw new Error(`IdP session for "${identityName}" has no refresh token`)
|
|
646
679
|
const idp = await readIdpConfig(session.idp)
|
|
680
|
+
const clientId = requireClientId(idp, session.clientId)
|
|
647
681
|
const params = new URLSearchParams({
|
|
648
682
|
grant_type: 'refresh_token',
|
|
649
683
|
refresh_token: session.refresh_token,
|
|
650
|
-
client_id:
|
|
684
|
+
client_id: clientId,
|
|
651
685
|
})
|
|
652
686
|
const audience = opts.audience ?? session.audience
|
|
653
687
|
if (audience) params.set('audience', audience)
|
|
@@ -674,6 +708,7 @@ export async function refreshSession(
|
|
|
674
708
|
const expiresAt = tokenExpiresAt(token)
|
|
675
709
|
const next: IdpSession = {
|
|
676
710
|
...session,
|
|
711
|
+
clientId,
|
|
677
712
|
access_token: token.access_token,
|
|
678
713
|
id_token: token.id_token ?? session.id_token,
|
|
679
714
|
refresh_token: token.refresh_token ?? session.refresh_token,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { InstanceEntry, InstanceStore } from './instance'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { formatInstanceState, type OwnedInstanceInfo } from './admin-instance'
|
|
4
4
|
import { normalizeInstanceKernelUrl, resolveInstanceKey } from './instance'
|
|
5
5
|
|
|
6
6
|
export type InstanceCandidate =
|
|
@@ -44,6 +44,6 @@ export function describeInstanceCandidate(candidate: InstanceCandidate): string
|
|
|
44
44
|
if (candidate.source === 'bookmark') {
|
|
45
45
|
return `${candidate.key} (bookmark) ${candidate.url}`
|
|
46
46
|
}
|
|
47
|
-
const extra =
|
|
47
|
+
const extra = formatInstanceState(candidate.info)
|
|
48
48
|
return `${candidate.key} (managed) ${candidate.url}${extra ? ` — ${extra}` : ''}`
|
|
49
49
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { InstanceInfo } from '../admin/instance/model'
|
|
2
1
|
import type { AstraleConfig } from './config'
|
|
3
2
|
|
|
3
|
+
import { AdminInstanceNotFoundError, type InstanceInfo } from '../admin/instance/model'
|
|
4
4
|
import { AstraleError } from '../errors'
|
|
5
5
|
import {
|
|
6
6
|
DEFAULT_ADMIN_TARGET_NAME,
|
|
@@ -190,19 +190,8 @@ export function isAdminDiscoveryFailure(error: unknown): boolean {
|
|
|
190
190
|
}
|
|
191
191
|
|
|
192
192
|
export function isManagedInstanceNotFound(error: unknown): boolean {
|
|
193
|
-
if (error instanceof AstraleError && error.code === 'INSTANCE_NOT_FOUND') return true
|
|
194
|
-
if (!(error instanceof Error)) return false
|
|
195
|
-
if (error.name === 'NotFoundError') return true
|
|
196
|
-
// The admin kernel reports a missing instance node as InternalKernelError
|
|
197
|
-
// with a NOT_FOUND-prefixed message. In this lookup that's an instance
|
|
198
|
-
// miss (config problem), not a kernel fault — map it so callers get the
|
|
199
|
-
// typed INSTANCE_NOT_FOUND with remediation instead of a raw kernel error.
|
|
200
|
-
if (error.name === 'InternalKernelError' && /^NOT_FOUND\b/.test(error.message)) return true
|
|
201
|
-
const data = (error as Error & { data?: unknown }).data
|
|
202
193
|
return (
|
|
203
|
-
error
|
|
204
|
-
|
|
205
|
-
typeof data === 'object' &&
|
|
206
|
-
(data as { type?: unknown }).type === 'NotFoundError'
|
|
194
|
+
error instanceof AdminInstanceNotFoundError ||
|
|
195
|
+
(error instanceof AstraleError && error.code === 'INSTANCE_NOT_FOUND')
|
|
207
196
|
)
|
|
208
197
|
}
|
package/src/lib/instance.ts
CHANGED
|
@@ -297,22 +297,26 @@ export async function upsertInstance(
|
|
|
297
297
|
* bookmark was repointed to a different kernel so callers can warn.
|
|
298
298
|
*/
|
|
299
299
|
export async function upsertManagedBookmark(
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
300
|
+
input: Readonly<{
|
|
301
|
+
key: string
|
|
302
|
+
slug: string
|
|
303
|
+
url: string
|
|
304
|
+
organizationId?: string
|
|
305
|
+
defaultIdentity?: string
|
|
306
|
+
}>,
|
|
304
307
|
): Promise<{ entry: InstanceEntry; repointedFrom?: string }> {
|
|
305
308
|
const store = await readInstances()
|
|
306
|
-
const previousUrl = store.instances[key]?.url
|
|
307
|
-
const url = normalizeInstanceKernelUrl(
|
|
308
|
-
const { entry } = await upsertInstance(key, {
|
|
309
|
+
const previousUrl = store.instances[input.key]?.url
|
|
310
|
+
const url = normalizeInstanceKernelUrl(input.url)
|
|
311
|
+
const { entry } = await upsertInstance(input.key, {
|
|
309
312
|
url,
|
|
310
313
|
issuer: url,
|
|
311
|
-
slug,
|
|
312
|
-
name: slug,
|
|
314
|
+
slug: input.slug,
|
|
315
|
+
name: input.slug,
|
|
313
316
|
kind: 'bookmark',
|
|
314
317
|
mode: 'remote',
|
|
315
|
-
...(organizationId ? { organizationId } : {}),
|
|
318
|
+
...(input.organizationId ? { organizationId: input.organizationId } : {}),
|
|
319
|
+
...(input.defaultIdentity ? { defaultIdentity: input.defaultIdentity } : {}),
|
|
316
320
|
})
|
|
317
321
|
return {
|
|
318
322
|
entry,
|
|
@@ -362,8 +366,9 @@ export async function getActive(config?: AstraleConfig): Promise<InstanceEntry &
|
|
|
362
366
|
export async function resolveInstance(
|
|
363
367
|
identifier: string,
|
|
364
368
|
config?: AstraleConfig,
|
|
369
|
+
opts: { persist?: boolean } = {},
|
|
365
370
|
): Promise<ResolvedInstance> {
|
|
366
|
-
const store = await readInstances(config)
|
|
371
|
+
const store = await readInstances(config, opts)
|
|
367
372
|
const key = resolveInstanceKey(store, identifier)
|
|
368
373
|
const entry = key ? store.instances[key] : undefined
|
|
369
374
|
if (!key || !entry?.url) {
|
|
@@ -448,7 +453,12 @@ function isRegionRoutedInstanceRoot(url: URL): boolean {
|
|
|
448
453
|
if (url.search || url.hash) return false
|
|
449
454
|
|
|
450
455
|
const labels = url.hostname.toLowerCase().split('.')
|
|
451
|
-
return
|
|
456
|
+
return (
|
|
457
|
+
(labels.length === 4 || labels.length === 5) &&
|
|
458
|
+
/^[a-z]{2}(?:-[a-z0-9]+)*$/u.test(labels[1] ?? '') &&
|
|
459
|
+
labels.at(-2) === 'astrale' &&
|
|
460
|
+
labels.at(-1) === 'ai'
|
|
461
|
+
)
|
|
452
462
|
}
|
|
453
463
|
|
|
454
464
|
function definedEntry(entry: AddInstanceOpts): AddInstanceOpts {
|
package/src/lib/log.ts
CHANGED
|
@@ -2,6 +2,7 @@ import chalk from 'chalk'
|
|
|
2
2
|
import ora, { type Ora } from 'ora'
|
|
3
3
|
|
|
4
4
|
import { AstraleError, NotImplementedError } from '../errors'
|
|
5
|
+
import { printFailureDebug } from './failure-debug'
|
|
5
6
|
import { formatElapsed } from './format'
|
|
6
7
|
import { isMachine, type MachineOpts } from './output'
|
|
7
8
|
|
|
@@ -18,28 +19,34 @@ export const log = {
|
|
|
18
19
|
|
|
19
20
|
/** Report an error with hint (when present) and exit. `--json` / `--ci` / a
|
|
20
21
|
* non-TTY stdout always get one structured JSON line on stderr. */
|
|
21
|
-
export function fatal(e: unknown, opts?: MachineOpts): never {
|
|
22
|
+
export function fatal(e: unknown, opts?: MachineOpts & { readonly debug?: boolean }): never {
|
|
22
23
|
// Ctrl-C at an interactive (@inquirer/prompts) prompt — exit quietly with the
|
|
23
24
|
// SIGINT convention, not a red error line.
|
|
24
25
|
if (e instanceof Error && e.name === 'ExitPromptError') process.exit(130)
|
|
25
|
-
const msg =
|
|
26
|
-
|
|
26
|
+
const msg =
|
|
27
|
+
e instanceof AstraleError ? e.message : 'The CLI encountered an unexpected internal failure.'
|
|
28
|
+
const code = e instanceof AstraleError ? e.code : 'UNEXPECTED_ERROR'
|
|
27
29
|
if (isMachine(opts)) {
|
|
28
30
|
const payload: Record<string, unknown> = { error: code, message: msg }
|
|
29
31
|
if (e instanceof AstraleError && e.hint) payload.hint = e.hint
|
|
30
32
|
process.stderr.write(JSON.stringify(payload) + '\n')
|
|
31
|
-
|
|
33
|
+
} else {
|
|
34
|
+
log.error(`${code}: ${msg}`)
|
|
35
|
+
if (e instanceof AstraleError && e.hint) log.dim(` hint: ${e.hint}`)
|
|
32
36
|
}
|
|
33
|
-
|
|
34
|
-
if (e instanceof AstraleError && e.hint) log.dim(` hint: ${e.hint}`)
|
|
37
|
+
if (opts?.debug) printFailureDebug(e, '')
|
|
35
38
|
process.exit(1)
|
|
36
39
|
}
|
|
37
40
|
|
|
38
|
-
/**
|
|
39
|
-
export function
|
|
41
|
+
/** Project only a caller-authored admission failure into the CLI input family. */
|
|
42
|
+
export function failInput(error: unknown, opts?: MachineOpts): never {
|
|
40
43
|
if (error instanceof AstraleError) fatal(error, opts)
|
|
44
|
+
if (!(error instanceof TypeError)) fatal(error, opts)
|
|
41
45
|
fatal(
|
|
42
|
-
new AstraleError(
|
|
46
|
+
new AstraleError(
|
|
47
|
+
'INVALID_INPUT',
|
|
48
|
+
error.message.trim() ? error.message : 'CLI input is invalid.',
|
|
49
|
+
),
|
|
43
50
|
opts,
|
|
44
51
|
)
|
|
45
52
|
}
|
package/src/lib/login-flow.ts
CHANGED
|
@@ -7,7 +7,9 @@ import {
|
|
|
7
7
|
normalizeTokenResponse,
|
|
8
8
|
pollDeviceToken,
|
|
9
9
|
readIdpConfigOrBuiltin,
|
|
10
|
+
readIdpSession,
|
|
10
11
|
readIdpStore,
|
|
12
|
+
requireClientId,
|
|
11
13
|
requestClientCredentials,
|
|
12
14
|
requestDeviceAuthorization,
|
|
13
15
|
saveIdpSession,
|
|
@@ -71,10 +73,17 @@ export type LoginResult = {
|
|
|
71
73
|
*/
|
|
72
74
|
export async function loginViaIdp(opts: LoginFlowOpts): Promise<LoginResult> {
|
|
73
75
|
const idpName = await resolveIdpName(opts.idp)
|
|
74
|
-
const
|
|
76
|
+
const previous = opts.name && !opts.clientId ? await readIdpSession(opts.name) : null
|
|
77
|
+
const savedClientId = previous?.idp === idpName ? previous.clientId : undefined
|
|
78
|
+
const requestedClientId = opts.clientId ?? savedClientId
|
|
79
|
+
const idp = await readIdpConfigOrBuiltin(idpName, {
|
|
80
|
+
clientId: requestedClientId,
|
|
81
|
+
persist: true,
|
|
82
|
+
})
|
|
83
|
+
const clientId = requireClientId(idp, requestedClientId)
|
|
75
84
|
const scope = opts.scope ?? idp.client.scope ?? 'openid profile email offline_access'
|
|
76
85
|
|
|
77
|
-
const token = normalizeTokenResponse(await obtainToken(idp, opts, scope))
|
|
86
|
+
const token = normalizeTokenResponse(await obtainToken(idp, { ...opts, clientId }, scope))
|
|
78
87
|
if (!token.access_token) throw new Error('IdP response did not include access_token')
|
|
79
88
|
if (opts.audience && !tokenAudienceMatches(token.access_token, opts.audience)) {
|
|
80
89
|
throw new Error(
|
|
@@ -83,7 +92,7 @@ export async function loginViaIdp(opts: LoginFlowOpts): Promise<LoginResult> {
|
|
|
83
92
|
}
|
|
84
93
|
|
|
85
94
|
const claims = decodeTokenClaims(token.id_token ?? token.access_token)
|
|
86
|
-
const subject = subjectFromToken(token,
|
|
95
|
+
const subject = subjectFromToken(token, clientId)
|
|
87
96
|
const issuer = issuerFromToken(token, idp.metadata.issuer)
|
|
88
97
|
const identityName = opts.name ?? identityNameFromClaims(claims, idpName)
|
|
89
98
|
const session: IdpSession = {
|
|
@@ -91,6 +100,7 @@ export async function loginViaIdp(opts: LoginFlowOpts): Promise<LoginResult> {
|
|
|
91
100
|
idp: idpName,
|
|
92
101
|
issuer,
|
|
93
102
|
subject,
|
|
103
|
+
clientId,
|
|
94
104
|
audience: opts.audience,
|
|
95
105
|
access_token: token.access_token,
|
|
96
106
|
id_token: token.id_token,
|
package/src/lib/meta.ts
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* `jwks_uri`) and JWKS at `/.well-known/jwks.json`. These helpers probe
|
|
7
7
|
* both to verify the issuer is alive and publishes at least one key.
|
|
8
8
|
*
|
|
9
|
-
* Domain
|
|
10
|
-
* outside this
|
|
9
|
+
* Domain deployment readiness uses the canonical Domain Publication; that is
|
|
10
|
+
* outside this issuer-connectivity helper.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { IssuerUnreachableError } from '../errors'
|
|
@@ -9,12 +9,10 @@ import { createOwnedInstance } from './admin-instance'
|
|
|
9
9
|
import { setActive, upsertManagedBookmark } from './instance'
|
|
10
10
|
import { withSpinner } from './log'
|
|
11
11
|
import { isMachine } from './output'
|
|
12
|
-
import { promptSelect } from './prompt'
|
|
13
12
|
import { validateSlug } from './validation'
|
|
14
13
|
|
|
15
14
|
export type ProvisionOpts = KernelCommandOpts &
|
|
16
15
|
AdminTargetCommandOpts & {
|
|
17
|
-
hostId?: string
|
|
18
16
|
// Global flags (program.ts) that force non-interactive — mirrors `instance use`.
|
|
19
17
|
ci?: boolean
|
|
20
18
|
noPrompt?: boolean
|
|
@@ -35,10 +33,10 @@ export type ProvisionResult = {
|
|
|
35
33
|
const SAGA_TIMEOUT_MS = '240000'
|
|
36
34
|
|
|
37
35
|
/**
|
|
38
|
-
* Provision an
|
|
36
|
+
* Provision an instance through the admin kernel, then bookmark it and
|
|
39
37
|
* make it the active target. Extracted from `instance create` so `astrale
|
|
40
|
-
* setup` provisions through the exact same saga (auth assertion →
|
|
41
|
-
* bookmark → set-active)
|
|
38
|
+
* setup` provisions through the exact same saga (auth assertion → create →
|
|
39
|
+
* bookmark → set-active).
|
|
42
40
|
*
|
|
43
41
|
* Presentation is deliberately minimal here (a spinner + a one-line success);
|
|
44
42
|
* the caller renders anything richer — `setup` follows this with a hero panel.
|
|
@@ -48,15 +46,10 @@ export async function provisionInstance(
|
|
|
48
46
|
opts: ProvisionOpts,
|
|
49
47
|
): Promise<ProvisionResult> {
|
|
50
48
|
validateSlug(slug)
|
|
51
|
-
await
|
|
52
|
-
|
|
53
|
-
// identity pinned on the admin bookmark (that mismatch silently made a fresh
|
|
54
|
-
// user's instance unusable). `--as` still wins.
|
|
55
|
-
if (!opts.as) opts = { ...opts, as: (await readIdentities()).default }
|
|
49
|
+
const authentication = await instanceCreateAuthentication(opts)
|
|
50
|
+
opts = authentication.opts
|
|
56
51
|
|
|
57
52
|
const machine = isMachine(opts)
|
|
58
|
-
const interactive = !!process.stdin.isTTY && !(opts.ci || opts.noPrompt || process.env.CI)
|
|
59
|
-
|
|
60
53
|
let repointedFrom: string | undefined
|
|
61
54
|
let selectionError: unknown = null
|
|
62
55
|
// The global 30s default doesn't just fail the CLIENT: the disconnect kills
|
|
@@ -65,21 +58,24 @@ export async function provisionInstance(
|
|
|
65
58
|
// timeout; an explicit --timeout still wins.
|
|
66
59
|
const createOpts = { ...opts, timeout: opts.timeout ?? SAGA_TIMEOUT_MS }
|
|
67
60
|
|
|
68
|
-
const runProvision = (
|
|
61
|
+
const runProvision = () =>
|
|
69
62
|
withSpinner(
|
|
70
63
|
`Provisioning instance ${slug}`,
|
|
71
64
|
!machine,
|
|
72
65
|
async () => {
|
|
73
|
-
const created = await createOwnedInstance(createOpts, slug
|
|
66
|
+
const created = await createOwnedInstance(createOpts, slug)
|
|
74
67
|
try {
|
|
75
68
|
// Org id from the create response — authoritative for token scoping
|
|
76
69
|
// (the router's /auth/org is eventually consistent).
|
|
77
|
-
const bookmarked = await upsertManagedBookmark(
|
|
78
|
-
slug,
|
|
70
|
+
const bookmarked = await upsertManagedBookmark({
|
|
71
|
+
key: slug,
|
|
79
72
|
slug,
|
|
80
|
-
created.url,
|
|
81
|
-
created.organizationId,
|
|
82
|
-
|
|
73
|
+
url: created.url,
|
|
74
|
+
...(created.organizationId ? { organizationId: created.organizationId } : {}),
|
|
75
|
+
...(authentication.defaultIdentity
|
|
76
|
+
? { defaultIdentity: authentication.defaultIdentity }
|
|
77
|
+
: {}),
|
|
78
|
+
})
|
|
83
79
|
repointedFrom = bookmarked.repointedFrom
|
|
84
80
|
await setActive(slug)
|
|
85
81
|
} catch (e) {
|
|
@@ -93,23 +89,7 @@ export async function provisionInstance(
|
|
|
93
89
|
},
|
|
94
90
|
)
|
|
95
91
|
|
|
96
|
-
|
|
97
|
-
// hosts). We recover ONLY from its multi-host ambiguity: pop a picker and
|
|
98
|
-
// retry with the chosen host_id — the ambiguity error throws before any
|
|
99
|
-
// provisioning side effect, so the retry is clean. No host / permission /
|
|
100
|
-
// capacity / a non-interactive run all reject through to the caller's `fatal`,
|
|
101
|
-
// surfacing the server's own message plus the listed ids. An explicit
|
|
102
|
-
// `--host-id` skips all of this.
|
|
103
|
-
const created = await runProvision(opts.hostId).catch(async (e: unknown) => {
|
|
104
|
-
const hostIds = !opts.hostId && interactive ? parseEligibleHostIds(e) : null
|
|
105
|
-
if (!hostIds) throw e
|
|
106
|
-
const chosen = await promptSelect(
|
|
107
|
-
`${hostIds.length} hosts available — pick one to provision on`,
|
|
108
|
-
hostIds.map((hid) => ({ name: hid, value: hid })),
|
|
109
|
-
)
|
|
110
|
-
if (!chosen) throw e
|
|
111
|
-
return runProvision(chosen)
|
|
112
|
-
})
|
|
92
|
+
const created = await runProvision()
|
|
113
93
|
|
|
114
94
|
// Warnings go to stderr so machine-readable stdout stays clean.
|
|
115
95
|
const warn = (msg: string) => console.error(chalk.yellow('⚠'), msg)
|
|
@@ -124,31 +104,28 @@ export async function provisionInstance(
|
|
|
124
104
|
return { created, slug, repointedFrom, ...(selectionError ? { selectionError } : {}) }
|
|
125
105
|
}
|
|
126
106
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (!match) return null
|
|
139
|
-
const ids = match[1]!
|
|
140
|
-
.split(',')
|
|
141
|
-
.map((s) => s.trim())
|
|
142
|
-
.filter(Boolean)
|
|
143
|
-
return ids.length > 1 ? ids : null
|
|
107
|
+
async function instanceCreateAuthentication(opts: ProvisionOpts): Promise<{
|
|
108
|
+
readonly opts: ProvisionOpts
|
|
109
|
+
readonly defaultIdentity?: string
|
|
110
|
+
}> {
|
|
111
|
+
if (opts.creds) return { opts }
|
|
112
|
+
const store = await readIdentities()
|
|
113
|
+
const defaultIdentity = selectInstanceCreateIdentity(store, opts)
|
|
114
|
+
return {
|
|
115
|
+
opts: opts.as ? opts : { ...opts, as: defaultIdentity },
|
|
116
|
+
defaultIdentity,
|
|
117
|
+
}
|
|
144
118
|
}
|
|
145
119
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
120
|
+
export function selectInstanceCreateIdentity(
|
|
121
|
+
store: IdentityStore,
|
|
122
|
+
opts: Pick<ProvisionOpts, 'as'> = {},
|
|
123
|
+
): string {
|
|
124
|
+
assertInstanceCreateIdentity(store, opts)
|
|
125
|
+
return opts.as ?? store.default
|
|
149
126
|
}
|
|
150
127
|
|
|
151
|
-
export function
|
|
128
|
+
export function assertInstanceCreateIdentity(
|
|
152
129
|
store: IdentityStore,
|
|
153
130
|
opts: Pick<ProvisionOpts, 'as'> = {},
|
|
154
131
|
): void {
|