@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.
Files changed (322) hide show
  1. package/README.md +23 -12
  2. package/dist/astrale.js +51494 -56307
  3. package/dist/public/connect-core.js +2028 -26846
  4. package/dist/public/keys/index.js +1755 -40694
  5. package/dist/public/paths/index.js +859 -39518
  6. package/dist/types/admin/binding.d.ts +19 -0
  7. package/dist/types/admin/instance/client.d.ts +22 -0
  8. package/dist/types/admin/instance/index.d.ts +2 -0
  9. package/dist/types/admin/instance/model.d.ts +5 -12
  10. package/dist/types/connection/auth.d.ts +3 -0
  11. package/dist/types/connection/call.d.ts +3 -0
  12. package/dist/types/connection/command.d.ts +27 -0
  13. package/dist/types/connection/credential.d.ts +14 -0
  14. package/dist/types/connection/errors.d.ts +1 -0
  15. package/dist/types/connection/exchange.d.ts +9 -0
  16. package/dist/types/connection/failure/classify.d.ts +2 -0
  17. package/dist/types/connection/failure/index.d.ts +5 -0
  18. package/dist/types/connection/failure/model.d.ts +25 -0
  19. package/dist/types/connection/failure/render.d.ts +3 -0
  20. package/dist/types/connection/index.d.ts +8 -0
  21. package/dist/types/connection/reasons.d.ts +37 -0
  22. package/dist/types/connection/self.d.ts +37 -0
  23. package/dist/types/connection/session.d.ts +31 -0
  24. package/dist/types/connection/target.d.ts +38 -0
  25. package/dist/types/errors.d.ts +1 -1
  26. package/dist/types/lib/admin-instance.d.ts +17 -0
  27. package/dist/types/lib/admin-target.d.ts +6 -2
  28. package/dist/types/lib/failure-debug.d.ts +1 -0
  29. package/dist/types/lib/idp.d.ts +7 -0
  30. package/dist/types/lib/instance-target.d.ts +1 -1
  31. package/dist/types/lib/instance.d.ts +10 -2
  32. package/dist/types/lib/log.d.ts +5 -3
  33. package/dist/types/lib/meta.d.ts +2 -2
  34. package/dist/types/lib/proc.d.ts +43 -0
  35. package/dist/types/lib/self.d.ts +9 -0
  36. package/dist/types/lib/update.d.ts +131 -0
  37. package/package.json +8 -10
  38. package/src/__tests__/fixtures/publication.ts +20 -0
  39. package/src/admin/__tests__/binding.test.ts +31 -0
  40. package/src/admin/__tests__/fixture.ts +101 -0
  41. package/src/admin/binding.ts +98 -0
  42. package/src/admin/catalog/.spec/api.d.ts +2 -2
  43. package/src/admin/catalog/__tests__/client.test.ts +20 -49
  44. package/src/admin/catalog/client.ts +36 -32
  45. package/src/admin/catalog/model.ts +3 -4
  46. package/src/admin/graph/.spec/api.d.ts +4 -10
  47. package/src/admin/graph/nodes.ts +1 -1
  48. package/src/admin/instance/.spec/api.d.ts +2 -2
  49. package/src/admin/instance/__tests__/client.test.ts +40 -163
  50. package/src/admin/instance/client.ts +35 -137
  51. package/src/admin/instance/index.ts +1 -2
  52. package/src/admin/instance/model.ts +11 -20
  53. package/src/commands/__tests__/admin-instance.test.ts +11 -8
  54. package/src/commands/__tests__/auth-login.test.ts +54 -1
  55. package/src/commands/__tests__/call-describe.test.ts +46 -33
  56. package/src/commands/__tests__/call.test.ts +5 -1
  57. package/src/commands/__tests__/domain-install-operation.test.ts +12 -7
  58. package/src/commands/__tests__/domain-install-owned.test.ts +6 -2
  59. package/src/commands/__tests__/domain-list.test.ts +63 -4
  60. package/src/commands/__tests__/install-identity-override.test.ts +19 -19
  61. package/src/commands/__tests__/instance-list-rows.test.ts +1 -1
  62. package/src/commands/__tests__/instance-status.test.ts +139 -0
  63. package/src/commands/__tests__/read-commands.test.ts +35 -10
  64. package/src/commands/__tests__/studio-descriptor.test.ts +22 -0
  65. package/src/commands/__tests__/update.test.ts +95 -0
  66. package/src/commands/call-describe.ts +60 -68
  67. package/src/commands/call.ts +43 -56
  68. package/src/commands/domain/install.ts +13 -17
  69. package/src/commands/domain/list.ts +18 -13
  70. package/src/commands/domain/publish.ts +14 -11
  71. package/src/commands/domain/uninstall.ts +2 -2
  72. package/src/commands/get.ts +14 -15
  73. package/src/commands/identity/__tests__/register.test.ts +3 -3
  74. package/src/commands/identity/register.ts +5 -4
  75. package/src/commands/instance/create.ts +8 -16
  76. package/src/commands/instance/delete.ts +12 -9
  77. package/src/commands/instance/list.ts +2 -2
  78. package/src/commands/instance/status.ts +66 -20
  79. package/src/commands/instance/use.ts +16 -12
  80. package/src/commands/introspect.ts +9 -16
  81. package/src/commands/logs.ts +24 -25
  82. package/src/commands/mutate.ts +6 -5
  83. package/src/commands/query.ts +27 -30
  84. package/src/commands/studio.ts +20 -0
  85. package/src/commands/token.ts +8 -7
  86. package/src/commands/update.ts +68 -22
  87. package/src/commands/view.ts +7 -6
  88. package/src/connection/.spec/api.d.ts +7 -8
  89. package/src/connection/.spec/architecture.md +5 -4
  90. package/src/connection/.spec/flows/session.ts +2 -2
  91. package/src/connection/.spec/laws/connection.ts +6 -2
  92. package/src/connection/.spec/layout.ts +1 -0
  93. package/src/connection/__tests__/credential.test.ts +20 -1
  94. package/src/connection/__tests__/errors.test.ts +43 -32
  95. package/src/connection/__tests__/exchange.test.ts +36 -6
  96. package/src/connection/__tests__/failure-fixtures.ts +39 -0
  97. package/src/connection/__tests__/failure-safety.test.ts +75 -0
  98. package/src/connection/__tests__/self.test.ts +31 -11
  99. package/src/connection/__tests__/session.test.ts +3 -3
  100. package/src/connection/__tests__/target.test.ts +1 -0
  101. package/src/connection/auth.ts +13 -0
  102. package/src/connection/call.ts +1 -1
  103. package/src/connection/credential.ts +2 -2
  104. package/src/connection/errors.ts +1 -355
  105. package/src/connection/exchange.ts +52 -27
  106. package/src/connection/failure/classify.ts +110 -0
  107. package/src/connection/failure/index.ts +18 -0
  108. package/src/connection/failure/model.ts +28 -0
  109. package/src/connection/failure/render.ts +116 -0
  110. package/src/connection/reasons.ts +16 -0
  111. package/src/connection/self.ts +39 -21
  112. package/src/connection/session.ts +19 -8
  113. package/src/errors.ts +3 -2
  114. package/src/graph/.spec/api.d.ts +12 -5
  115. package/src/graph/.spec/layout.ts +9 -1
  116. package/src/graph/__tests__/mutation.test.ts +2 -2
  117. package/src/graph/__tests__/query.test.ts +25 -6
  118. package/src/graph/class.ts +30 -0
  119. package/src/graph/index.ts +1 -0
  120. package/src/graph/query.ts +21 -32
  121. package/src/identity/.spec/api.d.ts +2 -2
  122. package/src/identity/__tests__/registration.test.ts +1 -1
  123. package/src/identity/registration.ts +3 -3
  124. package/src/keys/__tests__/keys.test.ts +4 -0
  125. package/src/keys/credential.ts +1 -0
  126. package/src/lib/__tests__/admin-target.test.ts +12 -0
  127. package/src/lib/__tests__/command-dx.test.ts +1 -1
  128. package/src/lib/__tests__/domain-publication.test.ts +138 -0
  129. package/src/lib/__tests__/idp-session.test.ts +22 -0
  130. package/src/lib/__tests__/idp.test.ts +36 -4
  131. package/src/lib/__tests__/instance-candidates.test.ts +1 -3
  132. package/src/lib/__tests__/instance-target.test.ts +7 -39
  133. package/src/lib/__tests__/instance.test.ts +6 -0
  134. package/src/lib/__tests__/log-errors.test.ts +64 -0
  135. package/src/lib/__tests__/studio-server-deps.test.ts +53 -16
  136. package/src/lib/__tests__/update.test.ts +113 -8
  137. package/src/lib/__tests__/view-open-intent.test.ts +0 -2
  138. package/src/lib/__tests__/view-server.test.ts +1 -1
  139. package/src/lib/__tests__/view-session.test.ts +1 -1
  140. package/src/lib/admin-instance.ts +14 -8
  141. package/src/lib/admin-target.ts +21 -2
  142. package/src/lib/command-dx.ts +9 -9
  143. package/src/lib/domain-publication.ts +104 -0
  144. package/src/lib/failure-debug.ts +26 -0
  145. package/src/lib/idp.ts +39 -4
  146. package/src/lib/instance-candidates.ts +2 -2
  147. package/src/lib/instance-target.ts +3 -14
  148. package/src/lib/instance.ts +22 -12
  149. package/src/lib/log.ts +16 -9
  150. package/src/lib/login-flow.ts +13 -3
  151. package/src/lib/meta.ts +2 -2
  152. package/src/lib/provision-instance.ts +34 -57
  153. package/src/lib/update.ts +124 -30
  154. package/src/lib/validation.ts +2 -2
  155. package/src/lib/view/resolve.ts +1 -1
  156. package/src/program/__tests__/program.test.ts +20 -4
  157. package/src/program/build.ts +2 -2
  158. package/src/setup/__tests__/instance-step.test.ts +27 -10
  159. package/src/setup/steps/instance.ts +22 -15
  160. package/studio/client/dist/assets/{elk-api-D2xgMJvi.js → elk-api-ByzoDZWH.js} +1 -1
  161. package/studio/client/dist/assets/index-B0RCgUOT.js +8 -0
  162. package/studio/client/dist/assets/index-BiGR8Oo9.css +1 -0
  163. package/studio/client/dist/assets/index-N8J8EKlB.js +81 -0
  164. package/studio/client/dist/index.html +2 -2
  165. package/studio/package.json +5 -10
  166. package/studio/server/agent/ask.test.ts +1 -1
  167. package/studio/server/agent/bridge/grant.test.ts +1 -1
  168. package/studio/server/agent/bridge/routes.test.ts +1 -1
  169. package/studio/server/agent/bridge/stdio.ts +8 -1
  170. package/studio/server/agent/conversation.test.ts +10 -5
  171. package/studio/server/agent/conversation.ts +56 -8
  172. package/studio/server/agent/harness/claude/adapter.test.ts +15 -5
  173. package/studio/server/agent/harness/codex/loadout.ts +18 -21
  174. package/studio/server/agent/harness/gateway/config.ts +20 -18
  175. package/studio/server/agent/harness/selection.ts +9 -1
  176. package/studio/server/agent/prompts/anchors.test.ts +32 -36
  177. package/studio/server/agent/prompts/anchors.ts +20 -49
  178. package/studio/server/agent/prompts/system.test.ts +3 -2
  179. package/studio/server/agent/prompts/system.ts +6 -5
  180. package/studio/server/agent/prompts/turn.ts +13 -4
  181. package/studio/server/agent/routes.test.ts +1 -1
  182. package/studio/server/agent/run/completion.ts +2 -2
  183. package/studio/server/agent/run/coordinator.test.ts +1 -1
  184. package/studio/server/agent/run/preparation.ts +6 -5
  185. package/studio/server/agent/run/transcript.test.ts +35 -1
  186. package/studio/server/agent/run/transcript.ts +169 -3
  187. package/studio/server/agent/run/usage.test.ts +24 -0
  188. package/studio/server/agent/run/usage.ts +26 -1
  189. package/studio/server/api/canvas.ts +43 -0
  190. package/studio/server/api/comments.ts +85 -0
  191. package/studio/server/api/context.ts +61 -0
  192. package/studio/server/api/deployment.ts +39 -0
  193. package/studio/server/api/documents.ts +55 -0
  194. package/studio/server/api/domain.ts +59 -0
  195. package/studio/server/api/http.ts +45 -0
  196. package/studio/server/api/project.ts +57 -0
  197. package/studio/server/api/schema.ts +12 -0
  198. package/studio/server/api/views.ts +57 -0
  199. package/studio/server/api/workspace.ts +60 -0
  200. package/studio/server/api-agent-loadout.test.ts +1 -1
  201. package/studio/server/api.test.ts +147 -0
  202. package/studio/server/api.ts +20 -422
  203. package/studio/server/cache.test.ts +122 -0
  204. package/studio/server/cache.ts +162 -11
  205. package/studio/server/cli-consumers.test.ts +494 -0
  206. package/studio/server/cli.test.ts +88 -0
  207. package/studio/server/cli.ts +240 -0
  208. package/studio/server/client-package.ts +25 -1
  209. package/studio/server/domain.test.ts +50 -40
  210. package/studio/server/domain.ts +83 -52
  211. package/studio/server/environment/dotenv-preview.test.ts +39 -0
  212. package/studio/server/environment/dotenv-preview.ts +27 -0
  213. package/studio/server/environment/files.test.ts +53 -0
  214. package/studio/server/{state/env.ts → environment/files.ts} +7 -48
  215. package/studio/server/{state → handoff}/copy.ts +21 -6
  216. package/studio/server/handoff/service.test.ts +22 -0
  217. package/studio/server/{state/handoff.ts → handoff/service.ts} +21 -14
  218. package/studio/server/index.ts +1 -1
  219. package/studio/server/instances/active.ts +72 -0
  220. package/studio/server/instances/deploy-record.test.ts +31 -0
  221. package/studio/server/instances/deploy-record.ts +37 -0
  222. package/studio/server/instances/deploy.ts +56 -0
  223. package/studio/server/{state/instance.test.ts → instances/probe.test.ts} +25 -21
  224. package/studio/server/instances/probe.ts +67 -0
  225. package/studio/server/instances/status.test.ts +12 -0
  226. package/studio/server/instances/status.ts +49 -0
  227. package/studio/server/introspect/anatomy/client-tree.ts +57 -0
  228. package/studio/server/introspect/anatomy/env-fields.ts +66 -0
  229. package/studio/server/introspect/anatomy/schema-definition.ts +53 -0
  230. package/studio/server/introspect/anatomy/source.ts +181 -0
  231. package/studio/server/introspect/anatomy/views/routes.ts +114 -0
  232. package/studio/server/introspect/anatomy/views.ts +53 -0
  233. package/studio/server/introspect/anatomy-extras.test.ts +106 -35
  234. package/studio/server/introspect/anatomy-extras.ts +7 -726
  235. package/studio/server/introspect/anatomy.test.ts +22 -14
  236. package/studio/server/introspect/anatomy.ts +52 -28
  237. package/studio/server/introspect/bundle.ts +15 -9
  238. package/studio/server/introspect/canonical-schema.test.ts +93 -309
  239. package/studio/server/introspect/canonical-schema.ts +257 -425
  240. package/studio/server/introspect/config-preview.test.ts +32 -0
  241. package/studio/server/introspect/config-preview.ts +119 -0
  242. package/studio/server/introspect/core-extractor.ts +18 -127
  243. package/studio/server/introspect/core.ts +1 -2
  244. package/studio/server/introspect/diff.test.ts +18 -18
  245. package/studio/server/introspect/diff.ts +44 -90
  246. package/studio/server/introspect/extractor.ts +23 -116
  247. package/studio/server/introspect/hash.ts +5 -2
  248. package/studio/server/introspect/overlay-tsmorph.test.ts +119 -240
  249. package/studio/server/introspect/overlay-tsmorph.ts +7 -1305
  250. package/studio/server/introspect/overlay.test.ts +26 -54
  251. package/studio/server/introspect/overlay.ts +10 -37
  252. package/studio/server/introspect/revision.test.ts +54 -0
  253. package/studio/server/introspect/revision.ts +49 -0
  254. package/studio/server/introspect/runtime.test.ts +83 -143
  255. package/studio/server/introspect/runtime.ts +40 -11
  256. package/studio/server/introspect/schema-ir-json.test.ts +70 -0
  257. package/studio/server/introspect/schema-ir-json.ts +151 -0
  258. package/studio/server/introspect/schema-refs.test.ts +43 -88
  259. package/studio/server/introspect/schema-refs.ts +14 -49
  260. package/studio/server/introspect/source-overlay/annotations.ts +14 -0
  261. package/studio/server/introspect/source-overlay/handlers.ts +161 -0
  262. package/studio/server/introspect/source-overlay/kernel-calls.ts +46 -0
  263. package/studio/server/introspect/source-overlay/project.ts +248 -0
  264. package/studio/server/introspect/source-overlay/spans.ts +330 -0
  265. package/studio/server/json.ts +46 -0
  266. package/studio/server/lifecycle.ts +5 -1
  267. package/studio/server/sse.test.ts +26 -0
  268. package/studio/server/sse.ts +4 -1
  269. package/studio/server/state/baseline.test.ts +223 -7
  270. package/studio/server/state/baseline.ts +148 -34
  271. package/studio/server/state/comments.test.ts +66 -1
  272. package/studio/server/state/comments.ts +173 -10
  273. package/studio/server/state/context.ts +34 -2
  274. package/studio/server/state/documents.ts +42 -1
  275. package/studio/server/state/integrations.ts +27 -1
  276. package/studio/server/state/layout.test.ts +34 -0
  277. package/studio/server/state/layout.ts +44 -17
  278. package/studio/server/state/settings.ts +29 -22
  279. package/studio/server/state/store.test.ts +122 -0
  280. package/studio/server/state/store.ts +66 -17
  281. package/studio/server/state/visibility.ts +22 -11
  282. package/studio/server/views/model.test.ts +57 -0
  283. package/studio/server/views/model.ts +66 -0
  284. package/studio/server/views/runtime.ts +40 -0
  285. package/studio/server/views/selection-repository.test.ts +31 -0
  286. package/studio/server/views/selection-repository.ts +60 -0
  287. package/studio/server/views/session.test.ts +73 -0
  288. package/studio/server/views/session.ts +166 -0
  289. package/studio/server/views/target.test.ts +78 -0
  290. package/studio/server/views/target.ts +222 -0
  291. package/studio/server/watch.test.ts +11 -11
  292. package/studio/server/watch.ts +9 -5
  293. package/studio/server/workspace/catalog.test.ts +17 -0
  294. package/studio/server/workspace/catalog.ts +43 -0
  295. package/studio/server/{state → workspace}/create.test.ts +1 -1
  296. package/studio/server/{state → workspace}/create.ts +1 -1
  297. package/studio/server/{state → workspace}/git.ts +1 -1
  298. package/studio/server/workspace/updates.ts +83 -0
  299. package/studio/server/workspace-state.ts +1 -1
  300. package/studio/server/workspace-watch.ts +2 -2
  301. package/studio/shared/contracts/README.md +16 -0
  302. package/studio/shared/contracts/agent.ts +270 -0
  303. package/studio/shared/contracts/runtime.ts +50 -0
  304. package/studio/shared/contracts/schema.ts +393 -0
  305. package/studio/shared/contracts/surface.test.ts +317 -0
  306. package/studio/shared/contracts/workspace.ts +265 -0
  307. package/studio/shared/layout.test.ts +116 -0
  308. package/studio/shared/schema/identity.test.ts +34 -0
  309. package/studio/shared/schema/identity.ts +68 -0
  310. package/studio/shared/types.ts +11 -1099
  311. package/studio/tsconfig.json +1 -1
  312. package/viewer/dist/main.js +40 -61
  313. package/.check-workspace.cjs +0 -40
  314. package/src/commands/__tests__/instance-create-hosts.test.ts +0 -29
  315. package/studio/client/dist/assets/index-BMdnsIJA.css +0 -1
  316. package/studio/client/dist/assets/index-D-vRV8w7.js +0 -8
  317. package/studio/client/dist/assets/index-LGSWRrk8.js +0 -81
  318. package/studio/server/state/catalog.ts +0 -117
  319. package/studio/server/state/instance.ts +0 -280
  320. package/studio/server/state/updates.ts +0 -63
  321. package/studio/server/state/views.test.ts +0 -286
  322. 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
- return JSON.parse(readFileSync(packageFile, 'utf8')) as PackageManifest
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
- resolveDomainEntry,
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(entry: 'implementation.ts' | 'domain.ts'): string {
24
- const root = mkdtempSync(join(tmpdir(), 'studio-domain-layout-'))
23
+ function fixture(nested = false): string {
24
+ const root = mkdtempSync(join(tmpdir(), 'studio-application-layout-'))
25
25
  roots.push(root)
26
- mkdirSync(join(root, 'schema'), { recursive: true })
27
- writeFileSync(join(root, 'astrale.config.ts'), 'export default {}\n')
28
- writeFileSync(join(root, entry), 'export const domain = {}\n')
29
- writeFileSync(join(root, 'schema/index.ts'), 'export const schema = {}\n')
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
- test('detects the current implementation.ts layout and requires its SDK dependency', () => {
34
- const root = fixture('implementation.ts')
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
- expect(isDomainDir(root)).toBe(true)
37
- expect(basename(resolveDomainEntry(root)!)).toBe('implementation.ts')
38
- expect(depsInstalled(root)).toBe(false)
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
- mkdirSync(join(root, 'node_modules', '@astrale-os', 'kernel-core'), { recursive: true })
41
- expect(depsInstalled(root)).toBe(false)
42
- mkdirSync(join(root, 'node_modules', '@astrale-os', 'sdk'), { recursive: true })
43
- expect(depsInstalled(root)).toBe(true)
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
- const handle = registerDomain(root)!
46
- domainIds.push(handle.id)
47
- expect(basename(handle.domainFile)).toBe('implementation.ts')
48
- expect(resolveTarget(root).map((domain) => domain.root)).toEqual([root])
49
- })
50
-
51
- test('preserves domain.ts and kernel-core as the legacy fallback', () => {
52
- const root = fixture('domain.ts')
53
- mkdirSync(join(root, 'node_modules', '@astrale-os', 'kernel-core'), { recursive: true })
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
  })
@@ -1,74 +1,95 @@
1
- /**
2
- * domain.ts DomainHandle resolution + the in-process registry. A "domain" is
3
- * confirmed by astrale.config.ts + a composition entry + <schemaDir>/index.ts.
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
- /** Active composition entry (implementation.ts when present, otherwise legacy domain.ts). */
16
- domainFile: string
17
- schemaDirName: string
18
- schemaDir: string
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 current composition entry, preferring the SDK layout. */
32
- export function resolveDomainEntry(root: string): string | null {
33
- const r = resolve(root)
34
- for (const file of DOMAIN_ENTRY_FILES) {
35
- const candidate = join(r, file)
36
- if (existsSync(candidate)) return candidate
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
- /** The single definition of "is this dir an Astrale domain": the triple must all exist. */
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 r = resolve(root)
44
- return (
45
- existsSync(join(r, 'astrale.config.ts')) &&
46
- resolveDomainEntry(r) !== null &&
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 r = resolve(root)
54
- if (!isDomainDir(r, schemaDirName)) return null
55
- const domainFile = resolveDomainEntry(r)
56
- if (!domainFile) return null
57
- const schemaDir = join(r, schemaDirName)
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(r),
60
- root: r,
61
- configFile: join(r, 'astrale.config.ts'),
62
- domainFile,
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: join(schemaDir, 'index.ts'),
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
- /** Does the domain have the dependency cohort required by its project layout installed? */
105
+ /** Studio's current project model always requires the semantic SDK Schema facade. */
85
106
  export function depsInstalled(root: string): boolean {
86
- const sdk = existsSync(join(root, 'node_modules', '@astrale-os', 'sdk'))
87
- const entry = resolveDomainEntry(root)
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
- // implementation.ts is an SDK boundary by contract. Legacy domain.ts
90
- // projects may predate the SDK facade and depend on kernel-core directly.
91
- if (entry?.endsWith('implementation.ts')) return sdk
92
- return sdk || existsSync(join(root, 'node_modules', '@astrale-os', 'kernel-core'))
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
+ }