@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,32 @@
|
|
|
1
|
+
import type { CommandDefinition } from '../../command'
|
|
2
|
+
|
|
3
|
+
import { getIdentity, setIdentityMode } from '../../lib/identity'
|
|
4
|
+
import { fatal, fatalNotImplemented, log } from '../../lib/log'
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
name: 'sync',
|
|
8
|
+
description: 'Migrate an identity local → remote (astrale cloud)',
|
|
9
|
+
arguments: [{ name: 'name', description: 'Identity name', required: true }],
|
|
10
|
+
options: [
|
|
11
|
+
{
|
|
12
|
+
flags: '--force',
|
|
13
|
+
description: 'Tag as remote in the local registry even without cloud login',
|
|
14
|
+
},
|
|
15
|
+
],
|
|
16
|
+
action: async (name: string, opts: { force?: boolean }) => {
|
|
17
|
+
try {
|
|
18
|
+
const identity = await getIdentity(name)
|
|
19
|
+
if (!opts.force) {
|
|
20
|
+
fatalNotImplemented(
|
|
21
|
+
'identity sync (remote)',
|
|
22
|
+
'Cloud adapter is stubbed in v1. Use --force to pre-tag the identity locally.',
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
await setIdentityMode(name, 'remote')
|
|
26
|
+
log.warn(`Tagged "${name}" as remote locally (cloud sync stubbed in v1)`)
|
|
27
|
+
log.dim(` subject=${identity.subject}`)
|
|
28
|
+
} catch (e) {
|
|
29
|
+
fatal(e)
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
} satisfies CommandDefinition
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { CommandDefinition } from '../../command'
|
|
2
|
+
|
|
3
|
+
import { getIdentity, setIdentityMode } from '../../lib/identity'
|
|
4
|
+
import { fatal, log } from '../../lib/log'
|
|
5
|
+
|
|
6
|
+
/** Metadata-only flip remote → local until cloud sync ships (§2.7). */
|
|
7
|
+
export default {
|
|
8
|
+
name: 'unsync',
|
|
9
|
+
description: 'Migrate an identity remote → local',
|
|
10
|
+
arguments: [{ name: 'name', description: 'Identity name', required: true }],
|
|
11
|
+
action: async (name: string) => {
|
|
12
|
+
try {
|
|
13
|
+
const identity = await getIdentity(name)
|
|
14
|
+
if (identity.mode !== 'remote') {
|
|
15
|
+
log.warn(`Identity "${name}" is already ${identity.mode ?? 'local'}`)
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
await setIdentityMode(name, 'local')
|
|
19
|
+
log.success(`Identity "${name}" → local`)
|
|
20
|
+
} catch (e) {
|
|
21
|
+
fatal(e)
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
} satisfies CommandDefinition
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { CommandDefinition } from '../../command'
|
|
2
|
+
|
|
3
|
+
import { setDefault } from '../../lib/identity'
|
|
4
|
+
import { fatal, log } from '../../lib/log'
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
name: 'use',
|
|
8
|
+
description: 'Set the active CLI identity',
|
|
9
|
+
arguments: [{ name: 'name', description: 'Identity name', required: true }],
|
|
10
|
+
action: async (name: string) => {
|
|
11
|
+
try {
|
|
12
|
+
await setDefault(name)
|
|
13
|
+
log.success(`Active identity set to "${name}"`)
|
|
14
|
+
} catch (e) {
|
|
15
|
+
fatal(e)
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
} satisfies CommandDefinition
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
|
|
3
|
+
import type { CommandDefinition } from '../../command'
|
|
4
|
+
|
|
5
|
+
import { getDefault } from '../../lib/identity'
|
|
6
|
+
import { fatal } from '../../lib/log'
|
|
7
|
+
import { isMachine, output, RAW_OUTPUT_OPTIONS, type RawOutputOpts } from '../../lib/output'
|
|
8
|
+
|
|
9
|
+
type WhoamiResult = {
|
|
10
|
+
name: string
|
|
11
|
+
subject: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default {
|
|
15
|
+
name: 'whoami',
|
|
16
|
+
description: 'Show the current default identity',
|
|
17
|
+
options: [...RAW_OUTPUT_OPTIONS],
|
|
18
|
+
action: async (opts: RawOutputOpts) => {
|
|
19
|
+
try {
|
|
20
|
+
const identity = await getDefault()
|
|
21
|
+
const result: WhoamiResult = {
|
|
22
|
+
name: identity.name,
|
|
23
|
+
subject: identity.subject,
|
|
24
|
+
}
|
|
25
|
+
if (isMachine(opts)) {
|
|
26
|
+
output(result, opts)
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
console.log(`${chalk.bold(result.name)} (subject: ${result.subject})`)
|
|
30
|
+
} catch (e) {
|
|
31
|
+
fatal(e)
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
} satisfies CommandDefinition
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
|
|
3
|
+
import type { CommandDefinition } from '../../command'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
fetchOidcMetadata,
|
|
7
|
+
fetchWorkosApplication,
|
|
8
|
+
OidcMetadataSchema,
|
|
9
|
+
type IdpClientConfig,
|
|
10
|
+
upsertIdpConfig,
|
|
11
|
+
workosAuthKitMetadata,
|
|
12
|
+
} from '../../lib/idp'
|
|
13
|
+
import { log } from '../../lib/log'
|
|
14
|
+
import { isMachine, output, RAW_OUTPUT_OPTIONS } from '../../lib/output'
|
|
15
|
+
import { validateName, validateUrl } from '../../lib/validation'
|
|
16
|
+
|
|
17
|
+
type AddOpts = {
|
|
18
|
+
issuer?: string
|
|
19
|
+
metadata?: string
|
|
20
|
+
clientId?: string
|
|
21
|
+
clientSecretEnv?: string
|
|
22
|
+
redirectUri?: string
|
|
23
|
+
scope?: string
|
|
24
|
+
public?: boolean
|
|
25
|
+
workosApiKeyEnv?: string
|
|
26
|
+
workosApp?: string
|
|
27
|
+
workosAuthkit?: boolean
|
|
28
|
+
workosApiHostname?: string
|
|
29
|
+
raw?: boolean
|
|
30
|
+
json?: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default {
|
|
34
|
+
name: 'add',
|
|
35
|
+
description: 'Add or update an OpenID Connect identity provider',
|
|
36
|
+
arguments: [{ name: 'name', description: 'Local IdP registry name' }],
|
|
37
|
+
options: [
|
|
38
|
+
{ flags: '--issuer <url>', description: 'OIDC issuer URL' },
|
|
39
|
+
{ flags: '--metadata <path>', description: 'Read discovery metadata from a local JSON file' },
|
|
40
|
+
{ flags: '--client-id <id>', description: 'OAuth/OIDC client ID' },
|
|
41
|
+
{ flags: '--client-secret-env <name>', description: 'Env var containing the client secret' },
|
|
42
|
+
{ flags: '--redirect-uri <url>', description: 'Redirect URI for authorization-code login' },
|
|
43
|
+
{ flags: '--scope <scope>', description: 'Default OAuth scope string' },
|
|
44
|
+
{ flags: '--public', description: 'Mark the client as public/PKCE-capable' },
|
|
45
|
+
{
|
|
46
|
+
flags: '--workos-api-key-env <name>',
|
|
47
|
+
description: 'Env var containing the WorkOS API key',
|
|
48
|
+
default: 'WORKOS_API_KEY',
|
|
49
|
+
},
|
|
50
|
+
{ flags: '--workos-app <id>', description: 'Fetch WorkOS Connect application metadata' },
|
|
51
|
+
{
|
|
52
|
+
flags: '--workos-authkit',
|
|
53
|
+
description: 'Configure WorkOS AuthKit CLI Auth using client_id only',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
flags: '--workos-api-hostname <url>',
|
|
57
|
+
description: 'WorkOS API host for AuthKit CLI Auth',
|
|
58
|
+
default: 'https://api.workos.com',
|
|
59
|
+
},
|
|
60
|
+
...RAW_OUTPUT_OPTIONS,
|
|
61
|
+
],
|
|
62
|
+
afterHelpText: `
|
|
63
|
+
Examples:
|
|
64
|
+
$ astrale idp add workos --workos-authkit --client-id client_...
|
|
65
|
+
$ astrale idp add workos-connect --issuer https://example.authkit.app \\
|
|
66
|
+
--client-id client_... --scope "openid profile email offline_access"
|
|
67
|
+
$ astrale idp add workos-connect --issuer https://example.authkit.app \\
|
|
68
|
+
--workos-app app_... --workos-api-key-env WORKOS_API_KEY
|
|
69
|
+
|
|
70
|
+
Security:
|
|
71
|
+
Client secrets and WorkOS API keys are never stored by the CLI. Store only
|
|
72
|
+
the env var name with --client-secret-env / --workos-api-key-env.
|
|
73
|
+
`,
|
|
74
|
+
action: async (name: string, opts: AddOpts) => {
|
|
75
|
+
validateName(name, 'IdP')
|
|
76
|
+
if (!opts.issuer && !opts.metadata && !opts.workosAuthkit) {
|
|
77
|
+
throw new Error('Either --issuer, --metadata, or --workos-authkit is required')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let metadata = opts.workosAuthkit
|
|
81
|
+
? workosAuthKitMetadata(opts.workosApiHostname, opts.clientId)
|
|
82
|
+
: opts.metadata
|
|
83
|
+
? OidcMetadataSchema.parse(JSON.parse(await readFile(opts.metadata, 'utf-8')))
|
|
84
|
+
: await fetchOidcMetadata(opts.issuer!)
|
|
85
|
+
|
|
86
|
+
if (opts.issuer) {
|
|
87
|
+
validateUrl(opts.issuer)
|
|
88
|
+
if (normalizeIssuer(metadata.issuer) !== normalizeIssuer(opts.issuer)) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`OIDC issuer mismatch: metadata has ${metadata.issuer}, expected ${opts.issuer}`,
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let client: IdpClientConfig = {
|
|
96
|
+
client_id: opts.clientId,
|
|
97
|
+
client_secret_env: opts.clientSecretEnv,
|
|
98
|
+
redirect_uris: opts.redirectUri ? [opts.redirectUri] : undefined,
|
|
99
|
+
scope: opts.scope,
|
|
100
|
+
public: opts.public,
|
|
101
|
+
token_response: opts.workosAuthkit ? 'workos-authkit' : undefined,
|
|
102
|
+
token_request_format: opts.workosAuthkit ? 'json' : undefined,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (opts.workosApp) {
|
|
106
|
+
const app = await fetchWorkosApplication({
|
|
107
|
+
app: opts.workosApp,
|
|
108
|
+
apiKeyEnv: opts.workosApiKeyEnv ?? 'WORKOS_API_KEY',
|
|
109
|
+
})
|
|
110
|
+
client = {
|
|
111
|
+
...client,
|
|
112
|
+
client_id: client.client_id ?? app.client_id,
|
|
113
|
+
redirect_uris:
|
|
114
|
+
client.redirect_uris ?? app.redirect_uris?.map((uri) => uri.uri).filter(isString),
|
|
115
|
+
scope: client.scope ?? app.scopes?.join(' '),
|
|
116
|
+
public: client.public ?? app.uses_pkce,
|
|
117
|
+
workos_application_id: app.id,
|
|
118
|
+
workos_application_type: app.application_type,
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (opts.workosAuthkit && client.client_id) {
|
|
123
|
+
metadata = workosAuthKitMetadata(opts.workosApiHostname, client.client_id)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const idp = await upsertIdpConfig({ name, metadata, client })
|
|
127
|
+
if (isMachine(opts)) {
|
|
128
|
+
output(redact(idp), opts)
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
log.success(`IdP "${name}" saved`)
|
|
133
|
+
log.dim(` issuer: ${idp.metadata.issuer}`)
|
|
134
|
+
if (idp.client.client_id) log.dim(` client_id: ${idp.client.client_id}`)
|
|
135
|
+
if (idp.client.client_secret_env)
|
|
136
|
+
log.dim(` client_secret_env: ${idp.client.client_secret_env}`)
|
|
137
|
+
},
|
|
138
|
+
} satisfies CommandDefinition
|
|
139
|
+
|
|
140
|
+
function isString(value: unknown): value is string {
|
|
141
|
+
return typeof value === 'string'
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function redact<T extends { client: IdpClientConfig }>(value: T): T {
|
|
145
|
+
return value
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function normalizeIssuer(value: string): string {
|
|
149
|
+
return value.replace(/\/+$/, '')
|
|
150
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
|
|
3
|
+
import type { CommandDefinition } from '../../command'
|
|
4
|
+
import type { ListOpts, ListProjection } from '../../lib/output'
|
|
5
|
+
|
|
6
|
+
import { listIdpConfigs } from '../../lib/idp'
|
|
7
|
+
import { log } from '../../lib/log'
|
|
8
|
+
import { isMachine, presentList, RAW_OUTPUT_OPTIONS } from '../../lib/output'
|
|
9
|
+
|
|
10
|
+
type IdpRow = {
|
|
11
|
+
name: string
|
|
12
|
+
issuer: string
|
|
13
|
+
clientId?: string
|
|
14
|
+
scope?: string
|
|
15
|
+
builtIn: boolean
|
|
16
|
+
updatedAt?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function projection(items: IdpRow[]): ListProjection {
|
|
20
|
+
return {
|
|
21
|
+
columns: [
|
|
22
|
+
{ key: 'name', header: 'NAME', color: chalk.bold },
|
|
23
|
+
{ key: 'type', header: 'TYPE', color: chalk.dim },
|
|
24
|
+
{ key: 'issuer', header: 'ISSUER', color: chalk.dim },
|
|
25
|
+
{ key: 'clientId', header: 'CLIENT_ID', color: chalk.dim },
|
|
26
|
+
],
|
|
27
|
+
rows: items.map((i) => ({
|
|
28
|
+
name: i.name,
|
|
29
|
+
type: i.builtIn ? 'built-in' : 'custom',
|
|
30
|
+
issuer: i.issuer ?? '',
|
|
31
|
+
clientId: i.clientId ?? '',
|
|
32
|
+
})),
|
|
33
|
+
paths: items.map((i) => i.name),
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default {
|
|
38
|
+
name: 'list',
|
|
39
|
+
description: 'List configured identity providers',
|
|
40
|
+
options: [...RAW_OUTPUT_OPTIONS],
|
|
41
|
+
action: async (opts: ListOpts) => {
|
|
42
|
+
const items: IdpRow[] = (await listIdpConfigs()).map((config) => ({
|
|
43
|
+
name: config.name,
|
|
44
|
+
issuer: config.entry.issuer,
|
|
45
|
+
clientId: config.client.client_id,
|
|
46
|
+
scope: config.client.scope,
|
|
47
|
+
builtIn: !!config.entry.builtIn,
|
|
48
|
+
updatedAt: config.entry.updatedAt,
|
|
49
|
+
}))
|
|
50
|
+
|
|
51
|
+
if (items.length === 0 && !isMachine(opts)) {
|
|
52
|
+
log.dim(' No IdPs. Run: astrale idp add <name> --issuer <url>')
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
presentList(items, opts, projection)
|
|
56
|
+
},
|
|
57
|
+
} satisfies CommandDefinition
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { CommandDefinition } from '../../command'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
fetchOidcMetadata,
|
|
5
|
+
readIdpConfigOrBuiltin,
|
|
6
|
+
upsertIdpConfig,
|
|
7
|
+
workosAuthKitMetadata,
|
|
8
|
+
} from '../../lib/idp'
|
|
9
|
+
import { log } from '../../lib/log'
|
|
10
|
+
import { isMachine, output, RAW_OUTPUT_OPTIONS } from '../../lib/output'
|
|
11
|
+
|
|
12
|
+
export default {
|
|
13
|
+
name: 'refresh',
|
|
14
|
+
description: 'Refresh discovery metadata for an identity provider',
|
|
15
|
+
arguments: [{ name: 'name', description: 'Local IdP registry name' }],
|
|
16
|
+
options: [...RAW_OUTPUT_OPTIONS],
|
|
17
|
+
action: async (name: string, opts: { raw?: boolean; json?: boolean }) => {
|
|
18
|
+
const existing = await readIdpConfigOrBuiltin(name)
|
|
19
|
+
const metadata =
|
|
20
|
+
existing.client.token_response === 'workos-authkit'
|
|
21
|
+
? workosAuthKitMetadata(existing.entry.issuer)
|
|
22
|
+
: await fetchOidcMetadata(existing.entry.issuer)
|
|
23
|
+
const idp = await upsertIdpConfig({
|
|
24
|
+
name,
|
|
25
|
+
metadata,
|
|
26
|
+
client: existing.client,
|
|
27
|
+
builtIn: existing.entry.builtIn,
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
if (isMachine(opts)) {
|
|
31
|
+
output(idp, opts)
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
log.success(`IdP "${name}" discovery metadata refreshed`)
|
|
36
|
+
log.dim(` issuer: ${idp.metadata.issuer}`)
|
|
37
|
+
},
|
|
38
|
+
} satisfies CommandDefinition
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { CommandDefinition } from '../../command'
|
|
2
|
+
|
|
3
|
+
import { readIdentities } from '../../lib/identity'
|
|
4
|
+
import { removeIdpConfig } from '../../lib/idp'
|
|
5
|
+
import { log } from '../../lib/log'
|
|
6
|
+
import { output, RAW_OUTPUT_OPTIONS } from '../../lib/output'
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
name: 'remove',
|
|
10
|
+
aliases: ['rm'],
|
|
11
|
+
description: 'Remove an identity provider',
|
|
12
|
+
arguments: [{ name: 'name', description: 'Local IdP registry name' }],
|
|
13
|
+
options: [
|
|
14
|
+
{ flags: '--force', description: 'Remove even when IdP-backed identities exist' },
|
|
15
|
+
...RAW_OUTPUT_OPTIONS,
|
|
16
|
+
],
|
|
17
|
+
action: async (name: string, opts: { force?: boolean; raw?: boolean; json?: boolean }) => {
|
|
18
|
+
const store = await readIdentities()
|
|
19
|
+
const dependents = Object.entries(store.identities)
|
|
20
|
+
.filter(([, identity]) => identity.source === 'idp' && identity.idp === name)
|
|
21
|
+
.map(([identityName]) => identityName)
|
|
22
|
+
|
|
23
|
+
if (dependents.length > 0 && !opts.force) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`IdP "${name}" is used by identities: ${dependents.join(', ')}. Re-run with --force or delete those identities first.`,
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
await removeIdpConfig(name)
|
|
30
|
+
if (opts.raw || opts.json) {
|
|
31
|
+
output({ removed: name, dependentIdentities: dependents }, opts)
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
log.success(`IdP "${name}" removed`)
|
|
35
|
+
},
|
|
36
|
+
} satisfies CommandDefinition
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { CommandDefinition } from '../../command'
|
|
2
|
+
|
|
3
|
+
import { readIdpConfigOrBuiltin } from '../../lib/idp'
|
|
4
|
+
import { output, RAW_OUTPUT_OPTIONS } from '../../lib/output'
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
name: 'show',
|
|
8
|
+
description: 'Show an identity provider configuration',
|
|
9
|
+
arguments: [{ name: 'name', description: 'Local IdP registry name' }],
|
|
10
|
+
options: [
|
|
11
|
+
{ flags: '--format <type>', description: 'Output format', choices: ['yaml', 'json'] },
|
|
12
|
+
...RAW_OUTPUT_OPTIONS,
|
|
13
|
+
],
|
|
14
|
+
action: async (
|
|
15
|
+
name: string,
|
|
16
|
+
opts: { raw?: boolean; json?: boolean; format?: 'yaml' | 'json' },
|
|
17
|
+
) => {
|
|
18
|
+
const idp = await readIdpConfigOrBuiltin(name)
|
|
19
|
+
output(
|
|
20
|
+
{
|
|
21
|
+
name: idp.name,
|
|
22
|
+
entry: idp.entry,
|
|
23
|
+
metadata: idp.metadata,
|
|
24
|
+
client: idp.client,
|
|
25
|
+
},
|
|
26
|
+
opts,
|
|
27
|
+
)
|
|
28
|
+
},
|
|
29
|
+
} satisfies CommandDefinition
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
|
|
3
|
+
import type { CommandDefinition } from '../../command'
|
|
4
|
+
|
|
5
|
+
import { withAdminKernelClient } from '../../kernel/client'
|
|
6
|
+
import { ADMIN_INSTANCE, type InstanceInfo } from '../../lib/admin-instance'
|
|
7
|
+
import { getActive, normalizeInstanceKernelUrl } from '../../lib/instance'
|
|
8
|
+
import { log } from '../../lib/log'
|
|
9
|
+
import { RAW_OUTPUT_OPTIONS, isMachine, output, type RawOutputOpts } from '../../lib/output'
|
|
10
|
+
|
|
11
|
+
export default {
|
|
12
|
+
name: 'active',
|
|
13
|
+
description: 'Show the currently active instance',
|
|
14
|
+
options: [...RAW_OUTPUT_OPTIONS],
|
|
15
|
+
action: async (opts: RawOutputOpts) => {
|
|
16
|
+
try {
|
|
17
|
+
const isRaw = isMachine(opts)
|
|
18
|
+
const active = await resolveActiveForDisplay()
|
|
19
|
+
const { name } = active
|
|
20
|
+
const url = active.url ?? null
|
|
21
|
+
const createdAt = active.createdAt ?? null
|
|
22
|
+
|
|
23
|
+
if (isRaw) {
|
|
24
|
+
output({ name, url, createdAt }, opts)
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
console.log(`${chalk.bold(name)} (${url ?? 'local'})`)
|
|
29
|
+
} catch (e) {
|
|
30
|
+
log.error(e instanceof Error ? e.message : String(e))
|
|
31
|
+
process.exit(1)
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
} satisfies CommandDefinition
|
|
35
|
+
|
|
36
|
+
async function resolveActiveForDisplay(): Promise<{
|
|
37
|
+
name: string
|
|
38
|
+
url?: string
|
|
39
|
+
createdAt?: string
|
|
40
|
+
}> {
|
|
41
|
+
const active = await getActive()
|
|
42
|
+
if (active.url) {
|
|
43
|
+
return {
|
|
44
|
+
name: active.name,
|
|
45
|
+
url: active.url,
|
|
46
|
+
createdAt: active.createdAt,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const managed = await withAdminKernelClient(
|
|
52
|
+
{},
|
|
53
|
+
async (ctx) =>
|
|
54
|
+
(await ctx.client.call(`${ADMIN_INSTANCE}/info`, { id: active.name })) as InstanceInfo,
|
|
55
|
+
)
|
|
56
|
+
return {
|
|
57
|
+
name: managed.slug,
|
|
58
|
+
url: normalizeInstanceKernelUrl(managed.url),
|
|
59
|
+
createdAt: managed.createdAt,
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
return { name: active.name }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { CommandDefinition } from '../../command'
|
|
2
|
+
|
|
3
|
+
import { fetchWithCaFile } from '../../kernel/ca-fetch'
|
|
4
|
+
import { normalizeInstanceKernelUrl, setActive, upsertInstance } from '../../lib/instance'
|
|
5
|
+
import { fatal, log } from '../../lib/log'
|
|
6
|
+
import { checkIssuerReachability } from '../../lib/meta'
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
name: 'bookmark',
|
|
10
|
+
description: 'Register a reference to an existing remote instance',
|
|
11
|
+
arguments: [{ name: 'name', description: 'Bookmark name (free, required)', required: true }],
|
|
12
|
+
options: [
|
|
13
|
+
{ flags: '--url <url>', description: 'URL of the remote instance (required)' },
|
|
14
|
+
{ flags: '--issuer <url>', description: 'Kernel issuer/audience if different from URL' },
|
|
15
|
+
{ flags: '--ca <path>', description: 'CA certificate file to trust for this bookmark' },
|
|
16
|
+
{
|
|
17
|
+
flags: '--as <identity>',
|
|
18
|
+
description: 'Default identity for this bookmark',
|
|
19
|
+
},
|
|
20
|
+
{ flags: '--use', description: 'Set as active instance after bookmarking' },
|
|
21
|
+
{ flags: '--skip-probe', description: 'Skip the OIDC + JWKS liveness probe (still recorded)' },
|
|
22
|
+
],
|
|
23
|
+
action: async (
|
|
24
|
+
name: string,
|
|
25
|
+
opts: {
|
|
26
|
+
url?: string
|
|
27
|
+
issuer?: string
|
|
28
|
+
ca?: string
|
|
29
|
+
as?: string
|
|
30
|
+
use?: boolean
|
|
31
|
+
skipProbe?: boolean
|
|
32
|
+
},
|
|
33
|
+
) => {
|
|
34
|
+
try {
|
|
35
|
+
if (!opts.url) fatal(new Error('Missing required flag: --url <url>'))
|
|
36
|
+
const url = normalizeInstanceKernelUrl(opts.url)
|
|
37
|
+
const expectedIssuer = opts.issuer ? normalizeInstanceKernelUrl(opts.issuer) : undefined
|
|
38
|
+
|
|
39
|
+
if (!opts.skipProbe) {
|
|
40
|
+
try {
|
|
41
|
+
const { issuer, keys } = await checkIssuerReachability(
|
|
42
|
+
url,
|
|
43
|
+
expectedIssuer,
|
|
44
|
+
opts.ca ? fetchWithCaFile(opts.ca) : undefined,
|
|
45
|
+
)
|
|
46
|
+
log.dim(` iss=${issuer} keys=${keys.length}`)
|
|
47
|
+
} catch (e) {
|
|
48
|
+
log.dim(' (re-run with --skip-probe to bookmark anyway.)')
|
|
49
|
+
fatal(e)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const { entry, created } = await upsertInstance(name, {
|
|
54
|
+
url,
|
|
55
|
+
issuer: expectedIssuer,
|
|
56
|
+
caFile: opts.ca,
|
|
57
|
+
name,
|
|
58
|
+
kind: 'bookmark',
|
|
59
|
+
mode: 'remote',
|
|
60
|
+
defaultIdentity: opts.as,
|
|
61
|
+
})
|
|
62
|
+
log.success(`${created ? 'Bookmarked' : 'Updated bookmark'} "${name}" → ${entry.url}`)
|
|
63
|
+
if (opts.as) log.dim(` default identity: ${opts.as}`)
|
|
64
|
+
if (opts.use) {
|
|
65
|
+
await setActive(name)
|
|
66
|
+
log.success(`Active instance: ${name}`)
|
|
67
|
+
}
|
|
68
|
+
} catch (e) {
|
|
69
|
+
fatal(e)
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
} satisfies CommandDefinition
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { CommandDefinition } from '../../command'
|
|
2
|
+
|
|
3
|
+
import { ADMIN_TARGET_OPTIONS } from '../../lib/admin-target'
|
|
4
|
+
import { fatal } from '../../lib/log'
|
|
5
|
+
import { isMachine, output } from '../../lib/output'
|
|
6
|
+
import { promptText } from '../../lib/prompt'
|
|
7
|
+
import { provisionInstance, type ProvisionOpts } from '../../lib/provision-instance'
|
|
8
|
+
import { validateSlug } from '../../lib/validation'
|
|
9
|
+
|
|
10
|
+
// Re-exported for tests + `astrale setup`, which share the provisioning saga.
|
|
11
|
+
export { assertAlphaCreateIdentity, parseEligibleHostIds } from '../../lib/provision-instance'
|
|
12
|
+
|
|
13
|
+
/** inquirer `validate` for a slug: true when valid, else the human message. */
|
|
14
|
+
function slugError(value: string): true | string {
|
|
15
|
+
try {
|
|
16
|
+
validateSlug(value)
|
|
17
|
+
return true
|
|
18
|
+
} catch (e) {
|
|
19
|
+
return e instanceof Error ? e.message : 'Invalid slug'
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default {
|
|
24
|
+
name: 'create',
|
|
25
|
+
description: 'Provision an alpha instance through the admin kernel (Instance.alphaCreate)',
|
|
26
|
+
afterHelpText: `
|
|
27
|
+
Behavior:
|
|
28
|
+
Calls Instance.alphaCreate on the configured admin kernel. The caller must be
|
|
29
|
+
logged in with WorkOS. When --host-id is omitted, the admin kernel chooses the
|
|
30
|
+
caller's single eligible host. The new instance becomes the active instance.
|
|
31
|
+
|
|
32
|
+
Run with no slug in a terminal and it prompts for one (validated live). With
|
|
33
|
+
no TTY — or --ci / --no-prompt — the slug argument is required up front, so
|
|
34
|
+
piped / CI / agent runs fail fast instead of waiting on input.
|
|
35
|
+
|
|
36
|
+
Examples:
|
|
37
|
+
$ astrale auth login
|
|
38
|
+
$ astrale instance create demo
|
|
39
|
+
`,
|
|
40
|
+
arguments: [{ name: 'id', description: 'Instance slug', required: false }],
|
|
41
|
+
options: [
|
|
42
|
+
...ADMIN_TARGET_OPTIONS,
|
|
43
|
+
{
|
|
44
|
+
flags: '--host-id <id>',
|
|
45
|
+
description: 'Advanced: host node id to provision on when multiple hosts are available',
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
action: async (id: string | undefined, opts: ProvisionOpts) => {
|
|
49
|
+
try {
|
|
50
|
+
// Interactive (TTY only): prompt for the slug when omitted, with live
|
|
51
|
+
// validation. No TTY / --ci / --no-prompt / CI → the slug arg is required
|
|
52
|
+
// (fail fast, never hang a piped / agent / LLM run).
|
|
53
|
+
const interactive = !!process.stdin.isTTY && !(opts.ci || opts.noPrompt || process.env.CI)
|
|
54
|
+
if (!id && interactive) id = await promptText('Instance slug', { validate: slugError })
|
|
55
|
+
if (!id) {
|
|
56
|
+
throw new Error('`instance create` needs a slug, e.g. `astrale instance create demo`.')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const { created } = await provisionInstance(id, opts)
|
|
60
|
+
|
|
61
|
+
if (isMachine(opts)) {
|
|
62
|
+
output(created, opts)
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
} catch (e) {
|
|
66
|
+
fatal(e)
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
} satisfies CommandDefinition
|