@astrale-os/sdk 0.1.10 → 0.3.0

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 (186) hide show
  1. package/LICENSE +202 -0
  2. package/dist/auth/check.d.ts +1 -2
  3. package/dist/auth/check.d.ts.map +1 -1
  4. package/dist/auth/check.js +2 -6
  5. package/dist/auth/check.js.map +1 -1
  6. package/dist/auth/index.d.ts +3 -1
  7. package/dist/auth/index.d.ts.map +1 -1
  8. package/dist/auth/index.js +2 -1
  9. package/dist/auth/index.js.map +1 -1
  10. package/dist/auth/issuer-mint.d.ts +53 -0
  11. package/dist/auth/issuer-mint.d.ts.map +1 -0
  12. package/dist/auth/issuer-mint.js +35 -0
  13. package/dist/auth/issuer-mint.js.map +1 -0
  14. package/dist/auth/kernel-client.d.ts +8 -2
  15. package/dist/auth/kernel-client.d.ts.map +1 -1
  16. package/dist/auth/kernel-client.js +22 -46
  17. package/dist/auth/kernel-client.js.map +1 -1
  18. package/dist/cli/dotenv.d.ts +8 -0
  19. package/dist/cli/dotenv.d.ts.map +1 -1
  20. package/dist/cli/dotenv.js +18 -0
  21. package/dist/cli/dotenv.js.map +1 -1
  22. package/dist/cli/index.d.ts +1 -1
  23. package/dist/cli/index.d.ts.map +1 -1
  24. package/dist/cli/index.js +1 -1
  25. package/dist/cli/index.js.map +1 -1
  26. package/dist/cli/log.d.ts +11 -0
  27. package/dist/cli/log.d.ts.map +1 -0
  28. package/dist/cli/log.js +17 -0
  29. package/dist/cli/log.js.map +1 -0
  30. package/dist/cli/publish.d.ts +35 -0
  31. package/dist/cli/publish.d.ts.map +1 -0
  32. package/dist/cli/publish.js +271 -0
  33. package/dist/cli/publish.js.map +1 -0
  34. package/dist/cli/run.d.ts +17 -2
  35. package/dist/cli/run.d.ts.map +1 -1
  36. package/dist/cli/run.js +102 -111
  37. package/dist/cli/run.js.map +1 -1
  38. package/dist/cli/spec.d.ts.map +1 -1
  39. package/dist/cli/spec.js +1 -0
  40. package/dist/cli/spec.js.map +1 -1
  41. package/dist/config/adapter.d.ts +12 -11
  42. package/dist/config/adapter.d.ts.map +1 -1
  43. package/dist/config/adapter.js.map +1 -1
  44. package/dist/config/define-domain.d.ts +88 -27
  45. package/dist/config/define-domain.d.ts.map +1 -1
  46. package/dist/config/define-domain.js +79 -33
  47. package/dist/config/define-domain.js.map +1 -1
  48. package/dist/config/deploy.d.ts +1 -1
  49. package/dist/config/deploy.js +1 -1
  50. package/dist/config/index.d.ts +1 -1
  51. package/dist/config/index.d.ts.map +1 -1
  52. package/dist/define/remote-function.d.ts +32 -17
  53. package/dist/define/remote-function.d.ts.map +1 -1
  54. package/dist/define/remote-function.js +10 -9
  55. package/dist/define/remote-function.js.map +1 -1
  56. package/dist/define/view.d.ts +8 -6
  57. package/dist/define/view.d.ts.map +1 -1
  58. package/dist/define/view.js +8 -6
  59. package/dist/define/view.js.map +1 -1
  60. package/dist/deploy/check.d.ts +6 -6
  61. package/dist/deploy/check.d.ts.map +1 -1
  62. package/dist/deploy/check.js +1 -1
  63. package/dist/deploy/check.js.map +1 -1
  64. package/dist/deploy/index.d.ts +0 -1
  65. package/dist/deploy/index.d.ts.map +1 -1
  66. package/dist/deploy/index.js +0 -1
  67. package/dist/deploy/index.js.map +1 -1
  68. package/dist/deploy/meta.d.ts +11 -0
  69. package/dist/deploy/meta.d.ts.map +1 -1
  70. package/dist/deploy/meta.js +23 -0
  71. package/dist/deploy/meta.js.map +1 -1
  72. package/dist/dispatch/dispatcher.d.ts +17 -4
  73. package/dist/dispatch/dispatcher.d.ts.map +1 -1
  74. package/dist/dispatch/dispatcher.js +15 -3
  75. package/dist/dispatch/dispatcher.js.map +1 -1
  76. package/dist/dispatch/execute.d.ts +6 -2
  77. package/dist/dispatch/execute.d.ts.map +1 -1
  78. package/dist/dispatch/execute.js +1 -0
  79. package/dist/dispatch/execute.js.map +1 -1
  80. package/dist/dispatch/identity.d.ts +12 -10
  81. package/dist/dispatch/identity.d.ts.map +1 -1
  82. package/dist/dispatch/identity.js +10 -15
  83. package/dist/dispatch/identity.js.map +1 -1
  84. package/dist/dispatch/index.d.ts +1 -1
  85. package/dist/dispatch/index.d.ts.map +1 -1
  86. package/dist/dispatch/index.js +1 -1
  87. package/dist/dispatch/index.js.map +1 -1
  88. package/dist/dispatch/self.d.ts +25 -37
  89. package/dist/dispatch/self.d.ts.map +1 -1
  90. package/dist/dispatch/self.js +60 -30
  91. package/dist/dispatch/self.js.map +1 -1
  92. package/dist/domain/binding.d.ts +18 -0
  93. package/dist/domain/binding.d.ts.map +1 -0
  94. package/dist/domain/binding.js +29 -0
  95. package/dist/domain/binding.js.map +1 -0
  96. package/dist/domain/build-spec.d.ts.map +1 -1
  97. package/dist/domain/build-spec.js +22 -7
  98. package/dist/domain/build-spec.js.map +1 -1
  99. package/dist/domain/define.d.ts +28 -43
  100. package/dist/domain/define.d.ts.map +1 -1
  101. package/dist/domain/define.js +23 -60
  102. package/dist/domain/define.js.map +1 -1
  103. package/dist/domain/extend-functions.d.ts +48 -0
  104. package/dist/domain/extend-functions.d.ts.map +1 -0
  105. package/dist/domain/extend-functions.js +84 -0
  106. package/dist/domain/extend-functions.js.map +1 -0
  107. package/dist/domain/extend-views.d.ts +45 -0
  108. package/dist/domain/extend-views.d.ts.map +1 -0
  109. package/dist/domain/extend-views.js +116 -0
  110. package/dist/domain/extend-views.js.map +1 -0
  111. package/dist/index.d.ts +8 -4
  112. package/dist/index.d.ts.map +1 -1
  113. package/dist/index.js +13 -5
  114. package/dist/index.js.map +1 -1
  115. package/dist/method/class.d.ts +2 -2
  116. package/dist/method/class.d.ts.map +1 -1
  117. package/dist/method/class.js.map +1 -1
  118. package/dist/method/context.d.ts +18 -3
  119. package/dist/method/context.d.ts.map +1 -1
  120. package/dist/method/index.d.ts +2 -2
  121. package/dist/method/index.d.ts.map +1 -1
  122. package/dist/method/index.js.map +1 -1
  123. package/dist/method/single.d.ts +25 -38
  124. package/dist/method/single.d.ts.map +1 -1
  125. package/dist/method/single.js +1 -11
  126. package/dist/method/single.js.map +1 -1
  127. package/dist/server/auxiliary-routes.d.ts.map +1 -1
  128. package/dist/server/auxiliary-routes.js +14 -3
  129. package/dist/server/auxiliary-routes.js.map +1 -1
  130. package/dist/server/create.d.ts +2 -15
  131. package/dist/server/create.d.ts.map +1 -1
  132. package/dist/server/create.js +15 -51
  133. package/dist/server/create.js.map +1 -1
  134. package/dist/server/domain-entry.d.ts +10 -0
  135. package/dist/server/domain-entry.d.ts.map +1 -1
  136. package/dist/server/domain-entry.js +2 -0
  137. package/dist/server/domain-entry.js.map +1 -1
  138. package/dist/server/index.d.ts +0 -1
  139. package/dist/server/index.d.ts.map +1 -1
  140. package/dist/server/index.js +0 -1
  141. package/dist/server/index.js.map +1 -1
  142. package/dist/server/worker-entry.d.ts +3 -3
  143. package/dist/server/worker-entry.js +3 -3
  144. package/package.json +10 -11
  145. package/src/auth/check.ts +2 -6
  146. package/src/auth/index.ts +3 -1
  147. package/src/auth/issuer-mint.ts +75 -0
  148. package/src/auth/kernel-client.ts +31 -63
  149. package/src/cli/dotenv.ts +20 -0
  150. package/src/cli/index.ts +1 -1
  151. package/src/cli/log.ts +18 -0
  152. package/src/cli/publish.ts +341 -0
  153. package/src/cli/run.ts +109 -125
  154. package/src/cli/spec.ts +1 -0
  155. package/src/config/adapter.ts +9 -11
  156. package/src/config/define-domain.ts +184 -53
  157. package/src/config/deploy.ts +1 -1
  158. package/src/config/index.ts +1 -1
  159. package/src/define/remote-function.ts +56 -21
  160. package/src/define/view.ts +8 -6
  161. package/src/deploy/check.ts +7 -7
  162. package/src/deploy/index.ts +0 -1
  163. package/src/deploy/meta.ts +25 -0
  164. package/src/dispatch/dispatcher.ts +32 -9
  165. package/src/dispatch/execute.ts +7 -2
  166. package/src/dispatch/identity.ts +17 -22
  167. package/src/dispatch/index.ts +1 -1
  168. package/src/dispatch/self.ts +117 -41
  169. package/src/domain/binding.ts +37 -0
  170. package/src/domain/build-spec.ts +32 -7
  171. package/src/domain/define.ts +71 -78
  172. package/src/domain/extend-functions.ts +106 -0
  173. package/src/domain/extend-views.ts +151 -0
  174. package/src/index.ts +36 -7
  175. package/src/method/class.ts +11 -7
  176. package/src/method/context.ts +19 -3
  177. package/src/method/index.ts +8 -2
  178. package/src/method/single.ts +35 -42
  179. package/src/server/auxiliary-routes.ts +14 -3
  180. package/src/server/create.ts +15 -51
  181. package/src/server/domain-entry.ts +12 -0
  182. package/src/server/index.ts +0 -1
  183. package/src/server/worker-entry.ts +3 -3
  184. package/src/deploy/hash-spec.ts +0 -31
  185. package/src/domain/extend-core.ts +0 -301
  186. package/src/server/worker-meta.ts +0 -25
package/src/cli/run.ts CHANGED
@@ -18,7 +18,15 @@
18
18
  * astrale-domain prod # = deploy prod
19
19
  * astrale-domain deploy <env> # any env key
20
20
  * astrale-domain build # rebuild spec only (placeholder URL)
21
- * astrale-domain publish [env] # register the deployed URL in the admin catalog (NO deploy)
21
+ * astrale-domain publish [env] # RELEASE: publish ./schema to npm (if any) + register the deployed URL
22
+ *
23
+ * A release has two halves that must stay in sync: the CONTRACT (`./schema` on
24
+ * npm) and the DEPLOYMENT (worker URL in the catalog). `publish` does both,
25
+ * npm-FIRST — `npm publish` is irreversible, so the catalog is registered only on
26
+ * schema-publish success, never pointing at an unpublished contract. The schema
27
+ * publish is idempotent (skips an unbumped version). Scope it with `--schema`
28
+ * (contract only — config-free) or `--skip-schema` (catalog only). All publish
29
+ * logic lives in `./publish.ts`; this file only parses args and dispatches.
22
30
  *
23
31
  * `publish` registers a domain's ALREADY-deployed URL in the admin catalog by
24
32
  * SHELLING OUT to the operator CLI (`astrale domain publish --origin --name
@@ -31,8 +39,7 @@
31
39
  * already points you at for `domain install`).
32
40
  */
33
41
 
34
- import { spawn } from 'node:child_process'
35
- import { existsSync, readFileSync, watch as watchFs } from 'node:fs'
42
+ import { existsSync, watch as watchFs } from 'node:fs'
36
43
  import { mkdir, writeFile } from 'node:fs/promises'
37
44
  import { createRequire } from 'node:module'
38
45
  import { basename, dirname, isAbsolute, join } from 'node:path'
@@ -41,7 +48,9 @@ import { pathToFileURL } from 'node:url'
41
48
  import type { DeployResult, DomainAdapter, DomainInfo, WatchHandle } from '../config/adapter'
42
49
  import type { DeployConfig } from '../config/deploy'
43
50
 
44
- import { loadDotenvFile } from './dotenv'
51
+ import { loadDeclaredSecrets } from './dotenv'
52
+ import { BOLD, DIM, error, GREEN, info, RESET, warn } from './log'
53
+ import { publishSchema, runRelease, runReleaseAfterDeploy } from './publish'
45
54
  import { buildProjectSpec } from './spec'
46
55
 
47
56
  const CONFIG_NAMES = ['astrale.config.ts', 'astrale.config.js', 'astrale.config.mjs']
@@ -50,6 +59,14 @@ type ParsedArgs = {
50
59
  command: 'dev' | 'build' | 'deploy' | 'publish'
51
60
  env: string
52
61
  watch: boolean
62
+ /** `--dry-run` (publish) — pack + `npm publish --dry-run` and skip the catalog register. */
63
+ dryRun?: boolean
64
+ /** `--schema` (publish) — publish ONLY the `./schema` package; no catalog, no config. */
65
+ schemaOnly?: boolean
66
+ /** `--otp <code>` (publish / deploy --publish) — 2FA OTP forwarded to `npm publish`. */
67
+ otp?: string
68
+ /** `--skip-schema` (publish / deploy --publish) — register the catalog only; don't publish `./schema`. */
69
+ skipSchema?: boolean
53
70
  /** `--port <n>` (dev only) — overrides the env's local dev port. */
54
71
  port?: number
55
72
  /** `--host <url>` (dev only) — public URL of a tunnel/proxy front: binds 0.0.0.0 + pins WORKER_URL. */
@@ -79,6 +96,17 @@ export async function run(argv: readonly string[]): Promise<number> {
79
96
  return 2
80
97
  }
81
98
 
99
+ // `publish --schema` (schema only) operates on the schema PACKAGE, not
100
+ // astrale.config.ts — handle it before config discovery so it works in a folder
101
+ // that is purely a schema package and needs no adapter/Bun runtime.
102
+ if (parsed.command === 'publish' && parsed.schemaOnly) {
103
+ return await publishSchema({
104
+ projectDir: process.cwd(),
105
+ dryRun: parsed.dryRun ?? false,
106
+ ...(parsed.otp !== undefined ? { otp: parsed.otp } : {}),
107
+ })
108
+ }
109
+
82
110
  // The CLI imports the project's TypeScript modules (astrale.config.ts, the ★
83
111
  // files) directly — that requires a TS-native runtime. Fail with the remedy
84
112
  // instead of a cryptic ERR_UNKNOWN_FILE_EXTENSION deep in `import()`.
@@ -137,20 +165,16 @@ export async function run(argv: readonly string[]): Promise<number> {
137
165
  }
138
166
 
139
167
  if (parsed.command === 'publish') {
140
- // Register the already-deployed URL in the admin catalog NO deploy. The
141
- // public address defaults to `https://<origin>` (the canonical custom-domain
142
- // prod URL — what every fleet domain serves at); `--public-url` overrides it
143
- // for workers.dev / split-host deploys. No adapter, secrets, or params: this
144
- // only points the registry at an address the author already deployed.
145
- const pkg = readPackageMeta(projectDir)
146
- const name = parsed.name ?? def.origin.split('.')[0] ?? def.origin
147
- const url = parsed.publicUrl ?? `https://${def.origin}`
148
- info(`registering ${def.origin} → ${url} in the admin catalog (no deploy)`)
149
- return await publishToAdmin({
168
+ // `--schema` (schema only) was handled before config discovery; this is the
169
+ // full release (schema + catalog) or `--skip-schema` (catalog only).
170
+ return await runRelease({
171
+ projectDir,
150
172
  origin: def.origin,
151
- name,
152
- url,
153
- ...(pkg.description ? { description: pkg.description } : {}),
173
+ skipSchema: parsed.skipSchema ?? false,
174
+ dryRun: parsed.dryRun ?? false,
175
+ ...(parsed.name !== undefined ? { name: parsed.name } : {}),
176
+ ...(parsed.publicUrl !== undefined ? { publicUrl: parsed.publicUrl } : {}),
177
+ ...(parsed.otp !== undefined ? { otp: parsed.otp } : {}),
154
178
  ...(parsed.installByDefault ? { installByDefault: true } : {}),
155
179
  })
156
180
  }
@@ -172,9 +196,6 @@ export async function run(argv: readonly string[]): Promise<number> {
172
196
 
173
197
  const domain = toDomainInfo(def)
174
198
 
175
- // The client SPA is a DECLARED binding (`defineDomain({ client })`), not a
176
- // folder we sniff for: its presence + source dir come from the definition.
177
- const clientDir = def.client ? join(projectDir, def.client.dir) : undefined
178
199
  const secrets = loadSecrets(adapter, params, projectDir)
179
200
 
180
201
  if (parsed.command === 'dev' || (parsed.command === 'deploy' && parsed.watch)) {
@@ -184,7 +205,6 @@ export async function run(argv: readonly string[]): Promise<number> {
184
205
  paramOverrides,
185
206
  projectDir,
186
207
  specPath,
187
- clientDir,
188
208
  secrets,
189
209
  domain,
190
210
  env: parsed.env,
@@ -198,7 +218,6 @@ export async function run(argv: readonly string[]): Promise<number> {
198
218
  projectDir,
199
219
  specPath,
200
220
  secrets,
201
- ...(clientDir ? { clientDir } : {}),
202
221
  domain,
203
222
  env: parsed.env,
204
223
  schemaHashForUrl: async (url) => {
@@ -210,98 +229,34 @@ export async function run(argv: readonly string[]): Promise<number> {
210
229
  printDeployed(result, def)
211
230
 
212
231
  if (parsed.publish) {
213
- const pkg = readPackageMeta(projectDir)
214
- // The registry `name` is a short catalog slug, distinct from the FQDN-like
215
- // `origin` AND from the npm package name (`@scope/…` for a published domain —
216
- // illegal as a catalog slug). Default to the origin's first label (the fleet
217
- // convention, e.g. `ai-gateway`); `--name` overrides.
218
- const name = parsed.name ?? def.origin.split('.')[0] ?? def.origin
219
- return await publishToAdmin({
232
+ // `--publish` = release after deploy: ship the schema (if any) THEN register
233
+ // the just-deployed URL same npm-first ordering as `publish`.
234
+ return await runReleaseAfterDeploy({
235
+ projectDir,
220
236
  origin: def.origin,
221
- name,
222
- url: result.url,
223
- ...(pkg.description ? { description: pkg.description } : {}),
237
+ deployedUrl: result.url,
238
+ skipSchema: parsed.skipSchema ?? false,
239
+ ...(parsed.name !== undefined ? { name: parsed.name } : {}),
240
+ ...(parsed.otp !== undefined ? { otp: parsed.otp } : {}),
224
241
  ...(parsed.installByDefault ? { installByDefault: true } : {}),
225
242
  })
226
243
  }
227
244
  return 0
228
245
  }
229
246
 
230
- /**
231
- * Register the just-deployed URL in the admin catalog by shelling out to the
232
- * operator CLI (`astrale domain publish`). Deliberately a subprocess, not a
233
- * library call: all credential/admin-target resolution lives in `@astrale-os/
234
- * astrale` and we don't want it (or a kernel client) pulled into this build
235
- * tool — we only own the fresh URL. A missing `astrale` is reported with the
236
- * exact line to run by hand, so a deploy that already succeeded isn't a dead end.
237
- */
238
- async function publishToAdmin(args: {
239
- origin: string
240
- name: string
241
- url: string
242
- description?: string
243
- installByDefault?: boolean
244
- }): Promise<number> {
245
- // `--public-url`, not `--url`: the operator CLI reserves `--url` for kernel
246
- // targeting, so the domain's public address rides its own flag.
247
- const cliArgs = [
248
- 'domain',
249
- 'publish',
250
- '--origin',
251
- args.origin,
252
- '--name',
253
- args.name,
254
- '--public-url',
255
- args.url,
256
- ]
257
- if (args.description) cliArgs.push('--description', args.description)
258
- if (args.installByDefault) cliArgs.push('--install-by-default')
259
-
260
- info(`publishing to the admin catalog → astrale ${cliArgs.join(' ')}`)
261
- return await new Promise<number>((resolveCode) => {
262
- const child = spawn('astrale', cliArgs, { stdio: 'inherit' })
263
- child.on('error', (err) => {
264
- if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
265
- error(
266
- `\`astrale\` CLI not found on PATH. The deploy succeeded — publish it by hand:\n` +
267
- ` astrale ${cliArgs.join(' ')}`,
268
- )
269
- } else {
270
- error(`failed to run \`astrale domain publish\`: ${err.message}`)
271
- }
272
- resolveCode(1)
273
- })
274
- child.on('exit', (code) => resolveCode(code ?? 1))
275
- })
276
- }
277
-
278
- /** Best-effort read of the project's package.json `name`/`description` (publish defaults). */
279
- function readPackageMeta(projectDir: string): { name?: string; description?: string } {
280
- try {
281
- const pkg = JSON.parse(readFileSync(join(projectDir, 'package.json'), 'utf8')) as {
282
- name?: string
283
- description?: string
284
- }
285
- return { name: pkg.name, description: pkg.description }
286
- } catch {
287
- return {}
288
- }
289
- }
290
-
291
247
  async function runWatch(args: {
292
248
  adapter: DomainAdapter<unknown>
293
249
  params: unknown
294
250
  paramOverrides: Record<string, unknown>
295
251
  projectDir: string
296
252
  specPath: string
297
- clientDir?: string
298
253
  secrets: Record<string, string>
299
254
  domain: DomainInfo
300
255
  env: string
301
256
  def: DeployConfig
302
257
  configPath: string
303
258
  }): Promise<number> {
304
- const { adapter, params, projectDir, specPath, clientDir, secrets, domain, env, def } = args
259
+ const { adapter, params, projectDir, specPath, secrets, domain, env, def } = args
305
260
  let reloads = 0
306
261
  const onReload = () => {
307
262
  reloads += 1
@@ -310,7 +265,6 @@ async function runWatch(args: {
310
265
  const handle: WatchHandle = await adapter.watch(params, {
311
266
  projectDir,
312
267
  specPath,
313
- ...(clientDir ? { clientDir } : {}),
314
268
  secrets,
315
269
  domain,
316
270
  env,
@@ -326,7 +280,6 @@ async function runWatch(args: {
326
280
  paramOverrides: args.paramOverrides,
327
281
  projectDir,
328
282
  specPath,
329
- ...(clientDir ? { clientDir } : {}),
330
283
  url: handle.url,
331
284
  onReload,
332
285
  })
@@ -368,7 +321,6 @@ export function watchConfigForRegen(args: {
368
321
  paramOverrides: Record<string, unknown>
369
322
  projectDir: string
370
323
  specPath: string
371
- clientDir?: string
372
324
  url: string
373
325
  onReload: () => void
374
326
  }): () => void {
@@ -409,7 +361,6 @@ export function watchConfigForRegen(args: {
409
361
  await adapter.regenerate(params, {
410
362
  projectDir: args.projectDir,
411
363
  specPath: args.specPath,
412
- ...(args.clientDir ? { clientDir: args.clientDir } : {}),
413
364
  secrets: loadSecrets(adapter, params, args.projectDir),
414
365
  domain,
415
366
  env: args.env,
@@ -502,7 +453,7 @@ function loadSecrets(
502
453
  const file = adapter.secretsFile?.(params)
503
454
  if (!file) return {}
504
455
  const abs = isAbsolute(file) ? file : join(projectDir, file)
505
- return loadDotenvFile(abs)
456
+ return loadDeclaredSecrets(abs, file)
506
457
  }
507
458
 
508
459
  function findConfig(dir: string): string | undefined {
@@ -521,11 +472,26 @@ function toDomainInfo(def: DeployConfig): DomainInfo {
521
472
  // Presence comes from the definition the author wired — never a folder probe.
522
473
  hasViews: def.views !== undefined,
523
474
  hasFunctions: def.functions !== undefined,
524
- hasClient: def.client !== undefined,
475
+ mountedViews: collectMountedViews(def.views),
525
476
  hasDeps: def.deps !== undefined,
526
477
  }
527
478
  }
528
479
 
480
+ function collectMountedViews(views: DeployConfig['views']): DomainInfo['mountedViews'] {
481
+ if (!views) return []
482
+ const mounted: DomainInfo['mountedViews'] = []
483
+ for (const [slug, def] of Object.entries(views)) {
484
+ const mount = (def as { mount?: unknown }).mount
485
+ if (typeof mount !== 'string' || mount.length === 0) continue
486
+ mounted.push({ slug, mount: normalizeMount(mount) })
487
+ }
488
+ return mounted
489
+ }
490
+
491
+ function normalizeMount(mount: string): string {
492
+ return `/${mount.replace(/^\/+/, '')}`
493
+ }
494
+
529
495
  async function loadConfig(path: string, opts?: { fresh: boolean }): Promise<DeployConfig> {
530
496
  // Re-importing an EDITED file: Bun's ESM registry is keyed by path and never
531
497
  // re-reads the source — a `?reload=N` query re-EVALUATES but serves the
@@ -563,12 +529,19 @@ export function parseArgs(argv: readonly string[]): ParsedArgs {
563
529
  // catalog after a successful deploy. The `publish` command implies it.
564
530
  const publish = rest.includes('--publish')
565
531
  const installByDefault = rest.includes('--install-by-default')
532
+ // `--dry-run` (publish): pack + `npm publish --dry-run` and skip the catalog
533
+ // register. A boolean flag, filtered out of `positionals` below.
534
+ const dryRun = rest.includes('--dry-run')
535
+ // Publish modes: `--schema` = schema only · `--skip-schema` = catalog only.
536
+ const schemaOnly = rest.includes('--schema')
537
+ const skipSchema = rest.includes('--skip-schema')
566
538
  // `--port <n>` / `--port=<n>`: consume the flag AND its value so neither
567
539
  // leaks into the positionals (a bare `8788` would be read as the env name).
568
540
  let port: number | undefined
569
541
  let host: string | undefined
570
542
  let name: string | undefined
571
543
  let publicUrl: string | undefined
544
+ let otp: string | undefined
572
545
  const cleaned: string[] = []
573
546
  for (let i = 0; i < rest.length; i++) {
574
547
  const a = rest[i]!
@@ -596,17 +569,38 @@ export function parseArgs(argv: readonly string[]): ParsedArgs {
596
569
  throw new Error('--public-url needs a value (the address the catalog points at)')
597
570
  } else if (a.startsWith('--public-url=')) {
598
571
  publicUrl = a.slice('--public-url='.length)
572
+ } else if (a === '--otp') {
573
+ otp = rest[++i]
574
+ if (!otp) throw new Error('--otp needs a value (your 2FA one-time password)')
575
+ } else if (a.startsWith('--otp=')) {
576
+ otp = a.slice('--otp='.length)
599
577
  } else {
600
578
  cleaned.push(a)
601
579
  }
602
580
  }
603
581
  const positionals = cleaned.filter((a) => !a.startsWith('-'))
604
582
 
583
+ // `--dry-run` belongs ONLY to `publish` — never let it silently no-op a deploy.
584
+ if (dryRun && cmd !== 'publish') throw new Error('`--dry-run` is only valid for `publish`.')
585
+ // `--otp` feeds the schema publish (publish, or deploy/prod `--publish`).
586
+ if (otp !== undefined && !['publish', 'deploy', 'prod'].includes(cmd ?? ''))
587
+ throw new Error('`--otp` is only valid for the publish / deploy commands.')
588
+ // `--schema` (schema only) / `--skip-schema` (catalog only): publish-only, opposite.
589
+ if (schemaOnly && cmd !== 'publish') throw new Error('`--schema` is only valid for `publish`.')
590
+ if (schemaOnly && skipSchema)
591
+ throw new Error(
592
+ '`--schema` (schema only) and `--skip-schema` (catalog only) are mutually exclusive.',
593
+ )
594
+
605
595
  // Publish-related fields shared by deploy/prod/publish (publish itself is
606
- // gated per-command: opt-in via `--publish`, always-on for `publish`).
596
+ // gated per-command: opt-in via `--publish`, always-on for `publish`). `otp` /
597
+ // `skipSchema` ride along because the schema-publish step runs on the
598
+ // `publish` command AND the `deploy --publish` tail.
607
599
  const pub = {
608
600
  ...(name !== undefined ? { name } : {}),
609
601
  ...(installByDefault ? { installByDefault } : {}),
602
+ ...(otp !== undefined ? { otp } : {}),
603
+ ...(skipSchema ? { skipSchema } : {}),
610
604
  }
611
605
 
612
606
  switch (cmd) {
@@ -626,15 +620,17 @@ export function parseArgs(argv: readonly string[]): ParsedArgs {
626
620
  return { command: 'deploy', env, watch, ...(publish ? { publish } : {}), ...pub }
627
621
  }
628
622
  case 'publish':
629
- // Register the ALREADY-deployed URL in the admin catalog NO deploy.
630
- // (Use `deploy [env] --publish` to deploy AND register in one step.) The
631
- // public address defaults to `https://<origin>`; `--public-url` overrides.
623
+ // Release: publish ./schema + register the deployed URL (no deploy).
624
+ // `--schema` = schema only · `--skip-schema` = catalog only · `--dry-run`
625
+ // packs the schema and skips the catalog register.
632
626
  return {
633
627
  command: 'publish',
634
628
  env: positionals[0] ?? 'prod',
635
629
  watch: false,
636
630
  ...pub,
637
631
  ...(publicUrl !== undefined ? { publicUrl } : {}),
632
+ ...(dryRun ? { dryRun } : {}),
633
+ ...(schemaOnly ? { schemaOnly } : {}),
638
634
  }
639
635
  case 'build':
640
636
  // Spec-only — no env resolution happens on this path.
@@ -650,23 +646,6 @@ function rel(from: string, to: string): string {
650
646
 
651
647
  // ── output ───────────────────────────────────────────────────────────────
652
648
 
653
- const GREEN = '\x1b[32m'
654
- const DIM = '\x1b[2m'
655
- const BOLD = '\x1b[1m'
656
- const YELLOW = '\x1b[33m'
657
- const RED = '\x1b[31m'
658
- const RESET = '\x1b[0m'
659
-
660
- function info(msg: string): void {
661
- process.stdout.write(`${DIM}›${RESET} ${msg}\n`)
662
- }
663
- function warn(msg: string): void {
664
- process.stderr.write(`${YELLOW}!${RESET} ${msg}\n`)
665
- }
666
- function error(msg: string): void {
667
- process.stderr.write(`${RED}✗${RESET} ${msg}\n`)
668
- }
669
-
670
649
  function printDevReady(url: string, domain: DomainInfo): void {
671
650
  process.stdout.write(
672
651
  `\n${GREEN}✓${RESET} ${BOLD}${domain.origin}${RESET} running (dev, hot-reload)\n` +
@@ -702,13 +681,18 @@ function printUsage(msg?: string): void {
702
681
  ` astrale-domain dev # deploy dev --watch (hot-reload, prints URL)\n` +
703
682
  ` astrale-domain prod # deploy prod\n` +
704
683
  ` astrale-domain deploy <env> # deploy any env key (--watch optional)\n` +
705
- ` astrale-domain publish [env] # register the already-deployed URL in the admin catalog (NO deploy)\n` +
684
+ ` astrale-domain publish [env] # RELEASE: publish ./schema to npm (if any) + register the deployed URL\n` +
706
685
  ` astrale-domain build # rebuild the diagnostic spec only\n` +
707
686
  `\nFlags:\n` +
708
- ` --publish # on deploy/prod: ALSO register the deployed URL (deploy + register)\n` +
687
+ ` --publish # on deploy/prod: ALSO release (publish ./schema + register the deployed URL)\n` +
688
+ ` --schema # (publish) publish ONLY the ./schema package — no catalog, no config\n` +
689
+ ` --skip-schema # (publish / deploy --publish) register the catalog only; don't publish ./schema\n` +
709
690
  ` --name <slug> # registry name to publish under (default: the origin's first label)\n` +
710
691
  ` --public-url <url> # (publish) address the catalog points at (default: https://<origin>)\n` +
711
692
  ` --install-by-default # mark the published domain for install on every new instance\n` +
712
- `\n publish shells out to \`astrale domain publish\` (needs \`astrale\` on PATH).\n\n`,
693
+ ` --dry-run # (publish) pack + npm publish --dry-run; never ships, skips catalog\n` +
694
+ ` --otp <code> # (publish / deploy --publish) 2FA code for npm publish\n` +
695
+ `\n catalog register shells out to \`astrale domain publish\` (needs \`astrale\` on PATH).\n` +
696
+ ` schema publish uses \`pnpm pack\` then \`npm publish\` (needs \`pnpm\` + \`npm\`; npm handles 2FA).\n\n`,
713
697
  )
714
698
  }
package/src/cli/spec.ts CHANGED
@@ -33,6 +33,7 @@ export async function buildProjectSpec(
33
33
  methods: def.methods,
34
34
  ...(def.views ? { views: def.views } : {}),
35
35
  ...(def.functions ? { remoteFunctions: def.functions } : {}),
36
+ ...(def.path ? { path: def.path } : {}),
36
37
  } as RemoteDomainConfig<Schema, unknown>
37
38
 
38
39
  const domain = defineRemoteDomain<unknown>()(config)
@@ -32,17 +32,18 @@ export interface DomainInfo {
32
32
  /** Optional Astrale Path called by the kernel after install (as __SYSTEM__). */
33
33
  postInstall?: string
34
34
  /**
35
- * Whether the domain declares any Views / standalone Functions / a client SPA
36
- * — read from the `defineDomain` definition, NOT probed from the filesystem.
37
- * The adapter codegen imports the corresponding module / mounts the SPA hook
38
- * only when true. `hasClient` governs the WORKER's `/ui` hook specifically
39
- * (present whenever the domain has a client, even on managed deploys that ship
40
- * the built assets separately and pass no `clientDir`); the wrangler
41
- * `assets.directory` binding tracks `clientDir` instead.
35
+ * Whether the domain declares Views / standalone Functions read from the
36
+ * `defineDomain` definition, NOT probed from the filesystem. The adapter
37
+ * codegen imports the corresponding module only when true.
42
38
  */
43
39
  hasViews: boolean
44
40
  hasFunctions: boolean
45
- hasClient: boolean
41
+ /**
42
+ * Views whose binding is backed by a worker-relative SPA mount. This is domain
43
+ * contract metadata (`defineView({ mount })`), not a frontend source folder:
44
+ * adapters decide, per env, which client assets serve these mounts.
45
+ */
46
+ mountedViews: Array<{ slug: string; mount: string }>
46
47
  /**
47
48
  * Whether the domain declares a `deps` mapper (`defineDomain({ deps })`).
48
49
  * When true the adapter codegen imports it from the domain's fixed `deps`
@@ -58,8 +59,6 @@ export interface WatchCtx {
58
59
  projectDir: string
59
60
  /** Absolute path where the CLI writes the diagnostic `spec.json`. */
60
61
  specPath: string
61
- /** Absolute path to the client SPA dir, when the domain declares a `client`. */
62
- clientDir?: string
63
62
  /** Flat secret map loaded from the env's `secrets` file — injected locally in dev. */
64
63
  secrets: Record<string, string>
65
64
  /** Domain metadata for codegen. */
@@ -75,7 +74,6 @@ export interface DeployCtx {
75
74
  specPath: string
76
75
  /** Flat secret map loaded from the env's `secrets` file (gitignored). */
77
76
  secrets: Record<string, string>
78
- clientDir?: string
79
77
  domain: DomainInfo
80
78
  /** The env key being deployed (e.g. 'dev' | 'prod' | 'canary'). */
81
79
  env: string