@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
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exact Astrale CLI bridge for Studio.
|
|
3
|
+
*
|
|
4
|
+
* The launching `astrale studio` process passes its runtime + entrypoint through a versioned
|
|
5
|
+
* descriptor. Every Studio CLI delegation uses that exact pair; this module never falls back to
|
|
6
|
+
* resolving an unrelated `astrale` binary from PATH.
|
|
7
|
+
*/
|
|
8
|
+
import { isAbsolute } from 'node:path'
|
|
9
|
+
|
|
10
|
+
import { asJsonRecord, asStringArray, parseJson as parseUntrustedJson } from './json'
|
|
11
|
+
|
|
12
|
+
export const STUDIO_CLI_DESCRIPTOR_ENV = 'DOMAIN_STUDIO_CLI_DESCRIPTOR'
|
|
13
|
+
|
|
14
|
+
export interface StudioCliDescriptorV1 {
|
|
15
|
+
version: 1
|
|
16
|
+
executable: string
|
|
17
|
+
args: string[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type StudioCliDecoder<T> = (value: unknown) => T | null
|
|
21
|
+
|
|
22
|
+
export interface StudioCliMachineResult<T> {
|
|
23
|
+
version: 1
|
|
24
|
+
ok: boolean
|
|
25
|
+
data: T | null
|
|
26
|
+
value: unknown | null
|
|
27
|
+
detail: string
|
|
28
|
+
exitCode: number
|
|
29
|
+
stdout: string
|
|
30
|
+
stderr: string
|
|
31
|
+
timedOut: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface StudioCliTextResult {
|
|
35
|
+
version: 1
|
|
36
|
+
ok: boolean
|
|
37
|
+
detail: string
|
|
38
|
+
exitCode: number
|
|
39
|
+
stdout: string
|
|
40
|
+
stderr: string
|
|
41
|
+
timedOut: boolean
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface RunOptions {
|
|
45
|
+
cwd?: string
|
|
46
|
+
timeoutMs?: number
|
|
47
|
+
acceptedExitCodes?: readonly number[]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface CapturedProcess {
|
|
51
|
+
exitCode: number
|
|
52
|
+
stdout: string
|
|
53
|
+
stderr: string
|
|
54
|
+
timedOut: boolean
|
|
55
|
+
spawnError?: string
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function decodeStudioCliDescriptor(value: string | undefined): StudioCliDescriptorV1 | null {
|
|
59
|
+
if (!value) return null
|
|
60
|
+
const parsed = asJsonRecord(parseUntrustedJson(value))
|
|
61
|
+
const args = asStringArray(parsed?.args)
|
|
62
|
+
if (
|
|
63
|
+
parsed?.version !== 1 ||
|
|
64
|
+
typeof parsed.executable !== 'string' ||
|
|
65
|
+
!isAbsolute(parsed.executable) ||
|
|
66
|
+
!args ||
|
|
67
|
+
args.length > 1 ||
|
|
68
|
+
!args.every((arg) => isAbsolute(arg))
|
|
69
|
+
) {
|
|
70
|
+
return null
|
|
71
|
+
}
|
|
72
|
+
return { version: 1, executable: parsed.executable, args }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function studioCliCommand(
|
|
76
|
+
args: readonly string[],
|
|
77
|
+
encodedDescriptor = process.env[STUDIO_CLI_DESCRIPTOR_ENV],
|
|
78
|
+
): string[] {
|
|
79
|
+
const descriptor = decodeStudioCliDescriptor(encodedDescriptor)
|
|
80
|
+
if (!descriptor) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`${STUDIO_CLI_DESCRIPTOR_ENV} is missing or invalid; launch Studio through this Astrale CLI`,
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
return [descriptor.executable, ...descriptor.args, ...args]
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function decodeJsonObject(value: unknown): Record<string, unknown> | null {
|
|
89
|
+
return asJsonRecord(value) ?? null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function conciseCliFailure(raw: string): string | undefined {
|
|
93
|
+
const lines = raw
|
|
94
|
+
.split(/\r?\n/)
|
|
95
|
+
.map((line) => line.trim())
|
|
96
|
+
.filter(Boolean)
|
|
97
|
+
const error = lines.find((line) => /^[A-Za-z_$][\w$]*(?:Error|Exception):\s+\S/.test(line))
|
|
98
|
+
if (error) return error.slice(0, 600)
|
|
99
|
+
const explicit = lines.find((line) => /^(?:error|failed):\s+\S/i.test(line))
|
|
100
|
+
if (explicit) return explicit.slice(0, 600)
|
|
101
|
+
const useful = lines.find(
|
|
102
|
+
(line) =>
|
|
103
|
+
!/^\d+\s+\|/.test(line) &&
|
|
104
|
+
line !== '^' &&
|
|
105
|
+
!/^at\s/.test(line) &&
|
|
106
|
+
!/^Bun v\d/.test(line) &&
|
|
107
|
+
!/^details?:\s*[{[]?$/i.test(line),
|
|
108
|
+
)
|
|
109
|
+
return useful?.slice(0, 600)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function nonEmptyString(value: unknown): string | undefined {
|
|
113
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function resultDetail(value: unknown, stdout: string, stderr: string): string {
|
|
117
|
+
const object = decodeJsonObject(value)
|
|
118
|
+
return (
|
|
119
|
+
nonEmptyString(object?.message) ??
|
|
120
|
+
nonEmptyString(object?.error) ??
|
|
121
|
+
conciseCliFailure(stderr) ??
|
|
122
|
+
conciseCliFailure(stdout) ??
|
|
123
|
+
''
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function parseJson(text: string): unknown | null {
|
|
128
|
+
const normalized = text.replace(/^\uFEFF/, '').trim()
|
|
129
|
+
if (!normalized) return null
|
|
130
|
+
try {
|
|
131
|
+
return JSON.parse(normalized)
|
|
132
|
+
} catch {
|
|
133
|
+
return null
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function captureStudioCli(
|
|
138
|
+
args: readonly string[],
|
|
139
|
+
options: RunOptions,
|
|
140
|
+
): Promise<CapturedProcess> {
|
|
141
|
+
let command: string[]
|
|
142
|
+
try {
|
|
143
|
+
command = studioCliCommand(args)
|
|
144
|
+
} catch (error) {
|
|
145
|
+
return {
|
|
146
|
+
exitCode: -1,
|
|
147
|
+
stdout: '',
|
|
148
|
+
stderr: '',
|
|
149
|
+
timedOut: false,
|
|
150
|
+
spawnError: error instanceof Error ? error.message : String(error),
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const proc = Bun.spawn(command, {
|
|
156
|
+
...(options.cwd ? { cwd: options.cwd } : {}),
|
|
157
|
+
stdout: 'pipe',
|
|
158
|
+
stderr: 'pipe',
|
|
159
|
+
})
|
|
160
|
+
let timedOut = false
|
|
161
|
+
const timer = options.timeoutMs
|
|
162
|
+
? setTimeout(() => {
|
|
163
|
+
timedOut = true
|
|
164
|
+
try {
|
|
165
|
+
proc.kill()
|
|
166
|
+
} catch {
|
|
167
|
+
// Already exited.
|
|
168
|
+
}
|
|
169
|
+
}, options.timeoutMs)
|
|
170
|
+
: undefined
|
|
171
|
+
try {
|
|
172
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
173
|
+
new Response(proc.stdout).text(),
|
|
174
|
+
new Response(proc.stderr).text(),
|
|
175
|
+
proc.exited,
|
|
176
|
+
])
|
|
177
|
+
return { exitCode, stdout, stderr, timedOut }
|
|
178
|
+
} finally {
|
|
179
|
+
if (timer) clearTimeout(timer)
|
|
180
|
+
}
|
|
181
|
+
} catch (error) {
|
|
182
|
+
return {
|
|
183
|
+
exitCode: -1,
|
|
184
|
+
stdout: '',
|
|
185
|
+
stderr: '',
|
|
186
|
+
timedOut: false,
|
|
187
|
+
spawnError: error instanceof Error ? error.message : String(error),
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export async function runStudioCliJson<T>(
|
|
193
|
+
args: readonly string[],
|
|
194
|
+
decoder: StudioCliDecoder<T>,
|
|
195
|
+
options: RunOptions = {},
|
|
196
|
+
): Promise<StudioCliMachineResult<T>> {
|
|
197
|
+
const machineArgs = args.includes('--json') ? [...args] : [...args, '--json']
|
|
198
|
+
const captured = await captureStudioCli(machineArgs, options)
|
|
199
|
+
const value = parseJson(captured.stdout) ?? parseJson(captured.stderr)
|
|
200
|
+
const data = value === null ? null : decoder(value)
|
|
201
|
+
const accepted = options.acceptedExitCodes ?? [0]
|
|
202
|
+
const detail =
|
|
203
|
+
captured.spawnError ??
|
|
204
|
+
(captured.timedOut
|
|
205
|
+
? `Astrale CLI timed out after ${options.timeoutMs ?? 0}ms`
|
|
206
|
+
: resultDetail(value, captured.stdout, captured.stderr))
|
|
207
|
+
return {
|
|
208
|
+
version: 1,
|
|
209
|
+
ok: accepted.includes(captured.exitCode) && data !== null && !captured.timedOut,
|
|
210
|
+
data,
|
|
211
|
+
value,
|
|
212
|
+
detail,
|
|
213
|
+
exitCode: captured.exitCode,
|
|
214
|
+
stdout: captured.stdout,
|
|
215
|
+
stderr: captured.stderr,
|
|
216
|
+
timedOut: captured.timedOut,
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export async function runStudioCliText(
|
|
221
|
+
args: readonly string[],
|
|
222
|
+
options: RunOptions = {},
|
|
223
|
+
): Promise<StudioCliTextResult> {
|
|
224
|
+
const captured = await captureStudioCli(args, options)
|
|
225
|
+
const accepted = options.acceptedExitCodes ?? [0]
|
|
226
|
+
const detail =
|
|
227
|
+
captured.spawnError ??
|
|
228
|
+
(captured.timedOut
|
|
229
|
+
? `Astrale CLI timed out after ${options.timeoutMs ?? 0}ms`
|
|
230
|
+
: (conciseCliFailure(captured.stderr) ?? conciseCliFailure(captured.stdout) ?? ''))
|
|
231
|
+
return {
|
|
232
|
+
version: 1,
|
|
233
|
+
ok: accepted.includes(captured.exitCode) && !captured.timedOut,
|
|
234
|
+
detail,
|
|
235
|
+
exitCode: captured.exitCode,
|
|
236
|
+
stdout: captured.stdout,
|
|
237
|
+
stderr: captured.stderr,
|
|
238
|
+
timedOut: captured.timedOut,
|
|
239
|
+
}
|
|
240
|
+
}
|
|
@@ -2,6 +2,8 @@ import { existsSync, readFileSync, statSync } from 'node:fs'
|
|
|
2
2
|
import { isAbsolute, join, relative, resolve } from 'node:path'
|
|
3
3
|
import { parse as parseYaml } from 'yaml'
|
|
4
4
|
|
|
5
|
+
import { asJsonRecord, asStringArray, parseJson } from './json'
|
|
6
|
+
|
|
5
7
|
export type ClientPackageResolution =
|
|
6
8
|
| {
|
|
7
9
|
status: 'available'
|
|
@@ -203,7 +205,29 @@ function packageWorkspacePatterns(pkg?: PackageManifest): string[] {
|
|
|
203
205
|
}
|
|
204
206
|
|
|
205
207
|
function readPackage(packageFile: string): PackageManifest {
|
|
206
|
-
|
|
208
|
+
const record = asJsonRecord(parseJson(readFileSync(packageFile, 'utf8')))
|
|
209
|
+
if (!record) throw new TypeError('package.json must contain an object')
|
|
210
|
+
const scriptsRecord = asJsonRecord(record.scripts)
|
|
211
|
+
const scripts = scriptsRecord
|
|
212
|
+
? Object.fromEntries(
|
|
213
|
+
Object.entries(scriptsRecord).filter(
|
|
214
|
+
(entry): entry is [string, string] => typeof entry[1] === 'string',
|
|
215
|
+
),
|
|
216
|
+
)
|
|
217
|
+
: undefined
|
|
218
|
+
let workspaces: PackageManifest['workspaces']
|
|
219
|
+
const workspaceList = asStringArray(record.workspaces)
|
|
220
|
+
if (workspaceList) {
|
|
221
|
+
workspaces = workspaceList
|
|
222
|
+
} else {
|
|
223
|
+
const workspaceObject = asJsonRecord(record.workspaces)
|
|
224
|
+
const packages = asStringArray(workspaceObject?.packages)
|
|
225
|
+
if (workspaceObject) workspaces = packages ? { packages } : {}
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
...(scripts === undefined ? {} : { scripts }),
|
|
229
|
+
...(workspaces === undefined ? {} : { workspaces }),
|
|
230
|
+
}
|
|
207
231
|
}
|
|
208
232
|
|
|
209
233
|
function previewScript(pkg?: PackageManifest): string | undefined {
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { afterEach, expect, test } from 'bun:test'
|
|
1
|
+
import { afterEach, describe, expect, test } from 'bun:test'
|
|
2
2
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
3
3
|
import { tmpdir } from 'node:os'
|
|
4
4
|
import { basename, join } from 'node:path'
|
|
5
5
|
|
|
6
|
-
import { resolveTarget } from './detect'
|
|
7
6
|
import {
|
|
8
7
|
depsInstalled,
|
|
9
8
|
isDomainDir,
|
|
10
9
|
registerDomain,
|
|
11
|
-
|
|
10
|
+
resolveApplicationEntry,
|
|
11
|
+
resolveSchemaEntry,
|
|
12
12
|
unregisterDomain,
|
|
13
13
|
} from './domain'
|
|
14
14
|
|
|
@@ -20,48 +20,58 @@ afterEach(() => {
|
|
|
20
20
|
while (roots.length) rmSync(roots.pop()!, { recursive: true, force: true })
|
|
21
21
|
})
|
|
22
22
|
|
|
23
|
-
function fixture(
|
|
24
|
-
const root = mkdtempSync(join(tmpdir(), 'studio-
|
|
23
|
+
function fixture(nested = false): string {
|
|
24
|
+
const root = mkdtempSync(join(tmpdir(), 'studio-application-layout-'))
|
|
25
25
|
roots.push(root)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
writeFileSync(
|
|
29
|
-
|
|
26
|
+
const owner = nested ? join(root, 'domain') : root
|
|
27
|
+
mkdirSync(join(owner, 'schema'), { recursive: true })
|
|
28
|
+
writeFileSync(
|
|
29
|
+
join(root, 'astrale.config.ts'),
|
|
30
|
+
nested
|
|
31
|
+
? "import application from './domain/application.js'\nexport default { application }\n"
|
|
32
|
+
: 'export default {}\n',
|
|
33
|
+
)
|
|
34
|
+
writeFileSync(join(owner, 'application.ts'), 'export default {}\n')
|
|
35
|
+
writeFileSync(join(owner, 'schema/index.ts'), 'export const schema = {}\n')
|
|
30
36
|
return root
|
|
31
37
|
}
|
|
32
38
|
|
|
33
|
-
|
|
34
|
-
|
|
39
|
+
describe('SDK V1 project discovery', () => {
|
|
40
|
+
test('discovers conventional root Application and Schema entries', () => {
|
|
41
|
+
const root = fixture()
|
|
42
|
+
expect(isDomainDir(root)).toBe(true)
|
|
43
|
+
expect(basename(resolveApplicationEntry(root)!)).toBe('application.ts')
|
|
44
|
+
expect(basename(resolveSchemaEntry(root, resolveApplicationEntry(root)!)!)).toBe('index.ts')
|
|
45
|
+
const handle = registerDomain(root)!
|
|
46
|
+
domainIds.push(handle.id)
|
|
47
|
+
expect(basename(handle.applicationFile)).toBe('application.ts')
|
|
48
|
+
expect(handle.schemaDirName).toBe('schema')
|
|
49
|
+
})
|
|
35
50
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
51
|
+
test('follows a config-imported nested Application and adjacent Schema', () => {
|
|
52
|
+
const root = fixture(true)
|
|
53
|
+
const application = resolveApplicationEntry(root)!
|
|
54
|
+
expect(application).toBe(join(root, 'domain/application.ts'))
|
|
55
|
+
expect(resolveSchemaEntry(root, application)).toBe(join(root, 'domain/schema/index.ts'))
|
|
56
|
+
})
|
|
39
57
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
58
|
+
test('requires the semantic SDK dependency, not Kernel implementation packages', () => {
|
|
59
|
+
const root = fixture()
|
|
60
|
+
expect(depsInstalled(root)).toBe(false)
|
|
61
|
+
mkdirSync(join(root, 'node_modules', '@astrale-os', 'kernel-core'), { recursive: true })
|
|
62
|
+
expect(depsInstalled(root)).toBe(false)
|
|
63
|
+
mkdirSync(join(root, 'node_modules', '@astrale-os', 'sdk'), { recursive: true })
|
|
64
|
+
expect(depsInstalled(root)).toBe(true)
|
|
65
|
+
})
|
|
44
66
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
expect(isDomainDir(root)).toBe(true)
|
|
56
|
-
expect(depsInstalled(root)).toBe(true)
|
|
57
|
-
const handle = registerDomain(root)!
|
|
58
|
-
domainIds.push(handle.id)
|
|
59
|
-
expect(basename(handle.domainFile)).toBe('domain.ts')
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
test('prefers implementation.ts when both composition entries exist', () => {
|
|
63
|
-
const root = fixture('domain.ts')
|
|
64
|
-
writeFileSync(join(root, 'implementation.ts'), 'export const domain = {}\n')
|
|
65
|
-
|
|
66
|
-
expect(basename(resolveDomainEntry(root)!)).toBe('implementation.ts')
|
|
67
|
+
test('rejects implementation.ts and domain.ts compatibility layouts', () => {
|
|
68
|
+
const root = mkdtempSync(join(tmpdir(), 'studio-legacy-layout-'))
|
|
69
|
+
roots.push(root)
|
|
70
|
+
mkdirSync(join(root, 'schema'))
|
|
71
|
+
writeFileSync(join(root, 'astrale.config.ts'), 'export default {}\n')
|
|
72
|
+
writeFileSync(join(root, 'implementation.ts'), 'export default {}\n')
|
|
73
|
+
writeFileSync(join(root, 'domain.ts'), 'export default {}\n')
|
|
74
|
+
writeFileSync(join(root, 'schema/index.ts'), 'export const schema = {}\n')
|
|
75
|
+
expect(isDomainDir(root)).toBe(false)
|
|
76
|
+
})
|
|
67
77
|
})
|
package/studio/server/domain.ts
CHANGED
|
@@ -1,74 +1,95 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* Current SDK projects use implementation.ts; domain.ts remains the legacy
|
|
5
|
-
* fallback. The schema dir is configurable (default 'schema') and threaded
|
|
6
|
-
* everywhere.
|
|
7
|
-
*/
|
|
8
|
-
import { existsSync } from 'node:fs'
|
|
9
|
-
import { basename, join, resolve } from 'node:path'
|
|
1
|
+
/** SDK V1 project discovery and the in-process Studio registry. */
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
3
|
+
import { basename, dirname, extname, join, relative, resolve } from 'node:path'
|
|
10
4
|
|
|
11
5
|
export interface DomainHandle {
|
|
12
|
-
id: string
|
|
13
|
-
root: string
|
|
14
|
-
configFile: string
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
schemaIndex: string
|
|
6
|
+
readonly id: string
|
|
7
|
+
readonly root: string
|
|
8
|
+
readonly configFile: string
|
|
9
|
+
readonly applicationFile: string
|
|
10
|
+
readonly schemaDirName: string
|
|
11
|
+
readonly schemaDir: string
|
|
12
|
+
readonly schemaIndex: string
|
|
20
13
|
origin?: string
|
|
21
14
|
}
|
|
22
15
|
|
|
23
16
|
const registry = new Map<string, DomainHandle>()
|
|
24
17
|
|
|
25
|
-
export const DOMAIN_ENTRY_FILES = ['implementation.ts', 'domain.ts'] as const
|
|
26
|
-
|
|
27
18
|
export function makeId(root: string): string {
|
|
28
19
|
return basename(resolve(root)).replace(/[^a-zA-Z0-9_-]/g, '-') || 'domain'
|
|
29
20
|
}
|
|
30
21
|
|
|
31
|
-
/** Resolve the
|
|
32
|
-
export function
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
22
|
+
/** Resolve the Application imported by config, with root application.ts as convention. */
|
|
23
|
+
export function resolveApplicationEntry(root: string): string | null {
|
|
24
|
+
const project = resolve(root)
|
|
25
|
+
const conventional = join(project, 'application.ts')
|
|
26
|
+
if (existsSync(conventional)) return conventional
|
|
27
|
+
const config = join(project, 'astrale.config.ts')
|
|
28
|
+
if (!existsSync(config)) return null
|
|
29
|
+
let source: string
|
|
30
|
+
try {
|
|
31
|
+
source = readFileSync(config, 'utf8')
|
|
32
|
+
} catch {
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
for (const match of source.matchAll(/\bfrom\s+['"]([^'"]+)['"]/gu)) {
|
|
36
|
+
const specifier = match[1]
|
|
37
|
+
if (
|
|
38
|
+
!specifier?.startsWith('.') ||
|
|
39
|
+
basename(specifier).replace(/\.[^.]+$/u, '') !== 'application'
|
|
40
|
+
) {
|
|
41
|
+
continue
|
|
42
|
+
}
|
|
43
|
+
const selected = resolveSourceFile(project, specifier)
|
|
44
|
+
if (selected !== null) return selected
|
|
37
45
|
}
|
|
38
46
|
return null
|
|
39
47
|
}
|
|
40
48
|
|
|
41
|
-
/**
|
|
49
|
+
/** Resolve schema.ts or schema/index.ts beside the Application, then at project root. */
|
|
50
|
+
export function resolveSchemaEntry(
|
|
51
|
+
root: string,
|
|
52
|
+
applicationFile: string,
|
|
53
|
+
schemaDirName = 'schema',
|
|
54
|
+
): string | null {
|
|
55
|
+
const project = resolve(root)
|
|
56
|
+
const applicationDir = dirname(applicationFile)
|
|
57
|
+
const candidates = [
|
|
58
|
+
join(applicationDir, 'schema.ts'),
|
|
59
|
+
join(applicationDir, schemaDirName, 'index.ts'),
|
|
60
|
+
join(project, 'schema.ts'),
|
|
61
|
+
join(project, schemaDirName, 'index.ts'),
|
|
62
|
+
]
|
|
63
|
+
return [...new Set(candidates)].find(existsSync) ?? null
|
|
64
|
+
}
|
|
65
|
+
|
|
42
66
|
export function isDomainDir(root: string, schemaDirName = 'schema'): boolean {
|
|
43
|
-
const
|
|
44
|
-
return
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
existsSync(join(r, schemaDirName, 'index.ts'))
|
|
48
|
-
)
|
|
67
|
+
const project = resolve(root)
|
|
68
|
+
if (!existsSync(join(project, 'astrale.config.ts'))) return false
|
|
69
|
+
const application = resolveApplicationEntry(project)
|
|
70
|
+
return application !== null && resolveSchemaEntry(project, application, schemaDirName) !== null
|
|
49
71
|
}
|
|
50
72
|
|
|
51
|
-
/** Confirm + register a domain rooted at `root`. Returns null if the triple is incomplete. */
|
|
52
73
|
export function registerDomain(root: string, schemaDirName = 'schema'): DomainHandle | null {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
74
|
+
const project = resolve(root)
|
|
75
|
+
const applicationFile = resolveApplicationEntry(project)
|
|
76
|
+
if (applicationFile === null || !existsSync(join(project, 'astrale.config.ts'))) return null
|
|
77
|
+
const schemaIndex = resolveSchemaEntry(project, applicationFile, schemaDirName)
|
|
78
|
+
if (schemaIndex === null) return null
|
|
79
|
+
const schemaDir = dirname(schemaIndex)
|
|
58
80
|
const handle: DomainHandle = {
|
|
59
|
-
id: makeId(
|
|
60
|
-
root:
|
|
61
|
-
configFile: join(
|
|
62
|
-
|
|
63
|
-
schemaDirName,
|
|
81
|
+
id: makeId(project),
|
|
82
|
+
root: project,
|
|
83
|
+
configFile: join(project, 'astrale.config.ts'),
|
|
84
|
+
applicationFile,
|
|
85
|
+
schemaDirName: relative(project, schemaDir).replaceAll('\\', '/') || '.',
|
|
64
86
|
schemaDir,
|
|
65
|
-
schemaIndex
|
|
87
|
+
schemaIndex,
|
|
66
88
|
}
|
|
67
89
|
registry.set(handle.id, handle)
|
|
68
90
|
return handle
|
|
69
91
|
}
|
|
70
92
|
|
|
71
|
-
/** Drop a domain from the registry (its dir/triple is gone). */
|
|
72
93
|
export function unregisterDomain(id: string): void {
|
|
73
94
|
registry.delete(id)
|
|
74
95
|
}
|
|
@@ -81,13 +102,23 @@ export function allDomains(): DomainHandle[] {
|
|
|
81
102
|
return [...registry.values()]
|
|
82
103
|
}
|
|
83
104
|
|
|
84
|
-
/**
|
|
105
|
+
/** Studio's current project model always requires the semantic SDK Schema facade. */
|
|
85
106
|
export function depsInstalled(root: string): boolean {
|
|
86
|
-
|
|
87
|
-
|
|
107
|
+
if (existsSync(join(root, 'node_modules', '@astrale-os', 'sdk'))) return true
|
|
108
|
+
try {
|
|
109
|
+
Bun.resolveSync('@astrale-os/sdk/schema', root)
|
|
110
|
+
return true
|
|
111
|
+
} catch {
|
|
112
|
+
return false
|
|
113
|
+
}
|
|
114
|
+
}
|
|
88
115
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
116
|
+
function resolveSourceFile(root: string, specifier: string): string | null {
|
|
117
|
+
const absolute = resolve(root, specifier)
|
|
118
|
+
const extension = extname(absolute)
|
|
119
|
+
const candidates =
|
|
120
|
+
extension === ''
|
|
121
|
+
? [`${absolute}.ts`, join(absolute, 'index.ts')]
|
|
122
|
+
: [absolute, `${absolute.slice(0, -extension.length)}.ts`]
|
|
123
|
+
return candidates.find(existsSync) ?? null
|
|
93
124
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import { parseDotenvPreview } from './dotenv-preview'
|
|
4
|
+
|
|
5
|
+
test('environment preview mirrors the SDK declared-secrets subset without mutating process.env', () => {
|
|
6
|
+
const keys = ['BASE', 'EXPANDED', 'LITERAL', 'EMPTY'] as const
|
|
7
|
+
const before = Object.fromEntries(keys.map((key) => [key, process.env[key]]))
|
|
8
|
+
const seeded = {
|
|
9
|
+
BASE: 'process-base',
|
|
10
|
+
EXPANDED: 'process-expanded',
|
|
11
|
+
LITERAL: 'process-literal',
|
|
12
|
+
EMPTY: 'process-empty',
|
|
13
|
+
} as const
|
|
14
|
+
Object.assign(process.env, seeded)
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const values = parseDotenvPreview(`
|
|
18
|
+
BASE=alpha
|
|
19
|
+
EXPANDED="${'${BASE}'}/beta"
|
|
20
|
+
LITERAL='${'${BASE}'}/beta'
|
|
21
|
+
export EMPTY=
|
|
22
|
+
# IGNORED=value
|
|
23
|
+
`)
|
|
24
|
+
|
|
25
|
+
expect(values).toEqual({
|
|
26
|
+
BASE: 'alpha',
|
|
27
|
+
EXPANDED: 'alpha/beta',
|
|
28
|
+
LITERAL: '${BASE}/beta',
|
|
29
|
+
EMPTY: '',
|
|
30
|
+
})
|
|
31
|
+
expect(Object.fromEntries(keys.map((key) => [key, process.env[key]]))).toEqual(seeded)
|
|
32
|
+
} finally {
|
|
33
|
+
for (const key of keys) {
|
|
34
|
+
const value = before[key]
|
|
35
|
+
if (value === undefined) delete process.env[key]
|
|
36
|
+
else process.env[key] = value
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Isolated preview of an adapter secrets file for Studio's editor.
|
|
3
|
+
*
|
|
4
|
+
* The SDK adapter remains authoritative when deploying. It is not a runtime
|
|
5
|
+
* dependency of the raw Studio server shipped inside the CLI package, so this
|
|
6
|
+
* isolated parser intentionally mirrors only the SDK's documented dotenv
|
|
7
|
+
* subset and never mutates process.env.
|
|
8
|
+
*/
|
|
9
|
+
export function parseDotenvPreview(contents: string): Record<string, string> {
|
|
10
|
+
const values: Record<string, string> = {}
|
|
11
|
+
for (const raw of contents.split('\n')) {
|
|
12
|
+
const line = raw.trim()
|
|
13
|
+
if (!line || line.startsWith('#')) continue
|
|
14
|
+
const match = /^(?:export\s+)?([A-Za-z_]\w*)\s*=\s*(.*)$/.exec(line)
|
|
15
|
+
if (!match) continue
|
|
16
|
+
const key = match[1]!
|
|
17
|
+
let value = match[2]!.trim()
|
|
18
|
+
const singleQuoted = value.length >= 2 && value.startsWith("'") && value.endsWith("'")
|
|
19
|
+
if (singleQuoted || (value.length >= 2 && value.startsWith('"') && value.endsWith('"'))) {
|
|
20
|
+
value = value.slice(1, -1)
|
|
21
|
+
}
|
|
22
|
+
values[key] = singleQuoted
|
|
23
|
+
? value
|
|
24
|
+
: value.replace(/\$\{(\w+)\}/g, (_, name: string) => values[name] ?? '')
|
|
25
|
+
}
|
|
26
|
+
return values
|
|
27
|
+
}
|