@astrale-os/cli 1.0.0-beta.21 → 1.0.0-beta.23

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 (31) hide show
  1. package/dist/astrale.js +670 -622
  2. package/dist/public/connect-core.js +4 -4
  3. package/dist/types/admin/contract.d.ts +2 -2
  4. package/package.json +4 -4
  5. package/src/admin/contract.ts +3 -2
  6. package/src/commands/__tests__/read-commands.test.ts +56 -0
  7. package/src/commands/__tests__/view.test.ts +1 -2
  8. package/src/commands/introspect.ts +8 -9
  9. package/src/ui/.spec/architecture.md +8 -6
  10. package/src/ui/.spec/laws.ts +8 -1
  11. package/src/ui/__tests__/ui.test.ts +166 -14
  12. package/src/ui/operations.ts +117 -14
  13. package/src/ui/release.ts +9 -2
  14. package/studio/client/dist/assets/{index-7YHdeCxp.js → index-OeJK1TjG.js} +2 -2
  15. package/studio/client/dist/assets/{schema-studio-CABpTfvH.js → schema-studio-DRdfu_cQ.js} +2 -2
  16. package/studio/client/dist/index.html +1 -1
  17. package/studio/package.json +3 -3
  18. package/studio/server/cli-consumers.test.ts +0 -1
  19. package/studio/server/introspect/anatomy/views.ts +1 -2
  20. package/studio/server/introspect/anatomy-extras.test.ts +3 -14
  21. package/studio/server/introspect/canonical-schema.test.ts +1 -1
  22. package/studio/server/introspect/canonical-schema.ts +0 -4
  23. package/studio/server/introspect/diff.test.ts +10 -2
  24. package/studio/server/introspect/diff.ts +1 -9
  25. package/studio/server/introspect/runtime.test.ts +1 -1
  26. package/studio/server/introspect/schema-ir-json.test.ts +12 -1
  27. package/studio/server/introspect/schema-ir-json.ts +1 -1
  28. package/studio/server/views/target.test.ts +1 -2
  29. package/studio/shared/contracts/schema.ts +1 -3
  30. package/studio/tsconfig.json +2 -1
  31. package/viewer/dist/main.js +13 -13
@@ -134,8 +134,45 @@ function activateTheme(current: string, statement: string): string {
134
134
  .replace(/^\n/u, '')
135
135
  }
136
136
 
137
- function manifestDependencies(manifest: Record<string, unknown>): Record<string, string> {
138
- return { ...(manifest.dependencies as Record<string, string> | undefined) }
137
+ type ManifestDependencySection = 'dependencies' | 'devDependencies'
138
+
139
+ function manifestDependencies(
140
+ manifest: Record<string, unknown>,
141
+ section: ManifestDependencySection = 'dependencies',
142
+ ): Record<string, string> {
143
+ return { ...(manifest[section] as Record<string, string> | undefined) }
144
+ }
145
+
146
+ function uiDependencySection(
147
+ manifest: Record<string, unknown>,
148
+ fallback: ManifestDependencySection = 'dependencies',
149
+ ): ManifestDependencySection {
150
+ const runtime = Object.hasOwn(manifestDependencies(manifest), UI_PACKAGE)
151
+ const development = Object.hasOwn(manifestDependencies(manifest, 'devDependencies'), UI_PACKAGE)
152
+ if (runtime && development) {
153
+ throw new UiError(
154
+ 'UI_PROJECT_UNSUPPORTED',
155
+ UI_PACKAGE + ' must be declared in exactly one dependency section.',
156
+ 'Remove it from either dependencies or devDependencies, then retry.',
157
+ )
158
+ }
159
+ if (development) return 'devDependencies'
160
+ if (runtime) return 'dependencies'
161
+ return fallback
162
+ }
163
+
164
+ function setUiDependency(
165
+ manifest: Record<string, unknown>,
166
+ section: ManifestDependencySection,
167
+ version: string,
168
+ ): void {
169
+ const other = section === 'dependencies' ? 'devDependencies' : 'dependencies'
170
+ manifest[section] = { ...manifestDependencies(manifest, section), [UI_PACKAGE]: version }
171
+ const otherDependencies = manifestDependencies(manifest, other)
172
+ if (Object.hasOwn(otherDependencies, UI_PACKAGE)) {
173
+ delete otherDependencies[UI_PACKAGE]
174
+ manifest[other] = otherDependencies
175
+ }
139
176
  }
140
177
 
141
178
  function domainRegistryPackagePath(project: UiProject): string {
@@ -294,6 +331,10 @@ export async function initUi(
294
331
  }
295
332
  if ((await exists(project.uiLockPath)) && !options.force) {
296
333
  const lock = await readUiLock(project.uiLockPath)
334
+ const dependencySection = uiDependencySection(project.packageJson)
335
+ const dependencyVersion = manifestDependencies(project.packageJson, dependencySection)[
336
+ UI_PACKAGE
337
+ ]
297
338
  const css = (await readOptional(project.cssPath)) ?? ''
298
339
  const components = await readOptional(project.componentsPath)
299
340
  .then((value) => (value ? (JSON.parse(value) as Record<string, unknown>) : undefined))
@@ -301,6 +342,7 @@ export async function initUi(
301
342
  const requestedVersion = options.version?.replace(/^v/u, '')
302
343
  const desired =
303
344
  (!requestedVersion || requestedVersion === lock.package.version) &&
345
+ dependencyVersion === lock.package.version &&
304
346
  (!options.preset || options.preset === lock.preset) &&
305
347
  css.includes(UI_PACKAGE + '/theme.css') &&
306
348
  css.includes(UI_PACKAGE + '/presets/' + lock.preset + '.css') &&
@@ -362,10 +404,7 @@ export async function initUi(
362
404
 
363
405
  try {
364
406
  const manifest = { ...project.packageJson }
365
- manifest.dependencies = {
366
- ...manifestDependencies(manifest),
367
- [UI_PACKAGE]: release.version,
368
- }
407
+ setUiDependency(manifest, uiDependencySection(manifest), release.version)
369
408
  await writeDomainRegistryWorkspace(project, manifest)
370
409
  await writeJson(project.packageJsonPath, manifest)
371
410
 
@@ -461,14 +500,17 @@ export async function initUi(
461
500
  async function pinUiDependency(
462
501
  project: UiProject,
463
502
  version: string,
503
+ section: ManifestDependencySection,
464
504
  runner: UiRunner,
465
505
  ): Promise<void> {
466
506
  const manifest = JSON.parse(await readFile(project.packageJsonPath, 'utf8')) as Record<
467
507
  string,
468
508
  unknown
469
509
  >
470
- if (manifestDependencies(manifest)[UI_PACKAGE] === version) return
471
- manifest.dependencies = { ...manifestDependencies(manifest), [UI_PACKAGE]: version }
510
+ const current = manifestDependencies(manifest, section)[UI_PACKAGE]
511
+ const other = section === 'dependencies' ? 'devDependencies' : 'dependencies'
512
+ if (current === version && manifestDependencies(manifest, other)[UI_PACKAGE] === undefined) return
513
+ setUiDependency(manifest, section, version)
472
514
  await writeJson(project.packageJsonPath, manifest)
473
515
  const result = await runner(project.manager, ['install'], project.root)
474
516
  if (result.code !== 0) {
@@ -544,6 +586,17 @@ async function addLocalTheme(
544
586
  const source = await readFile(sourcePath, 'utf8')
545
587
  admitLocalThemeCss(source, slug)
546
588
 
589
+ return installThemeCss(slug, source, digest(source), address, project, options)
590
+ }
591
+
592
+ async function installThemeCss(
593
+ slug: string,
594
+ source: string,
595
+ sourceDigest: string,
596
+ sourceLabel: string,
597
+ project: UiProject,
598
+ options: { dryRun?: boolean; overwrite?: boolean },
599
+ ): Promise<Record<string, unknown>> {
547
600
  const lock = await readUiLock(project.uiLockPath)
548
601
  const canonicalAddress = 'theme/' + slug
549
602
  const relativeTarget = 'components/astrale/theme/' + slug + '.css'
@@ -573,7 +626,7 @@ async function addLocalTheme(
573
626
  items: [canonicalAddress],
574
627
  files: { [canonicalAddress]: { [relativeTarget]: digest(source) } },
575
628
  activation: { file: projectRelative(project, project.cssPath), import: statement },
576
- source: address,
629
+ source: sourceLabel,
577
630
  }
578
631
  if (options.dryRun) return result
579
632
 
@@ -588,7 +641,7 @@ async function addLocalTheme(
588
641
  await writeFile(project.cssPath, activateTheme(css, statement), 'utf8')
589
642
  lock.items[canonicalAddress] = {
590
643
  address: canonicalAddress,
591
- sourceDigest: digest(source),
644
+ sourceDigest,
592
645
  files: { [relativeTarget]: digest(source) },
593
646
  }
594
647
  await writeJson(project.uiLockPath, lock)
@@ -602,6 +655,28 @@ async function addLocalTheme(
602
655
  }
603
656
  }
604
657
 
658
+ async function addReleasedTheme(
659
+ item: UiRegistryItem,
660
+ document: UiRegistryItem,
661
+ project: UiProject,
662
+ options: { dryRun?: boolean; overwrite?: boolean },
663
+ ): Promise<Record<string, unknown>> {
664
+ const slug = item.meta.canonicalAddress.slice('theme/'.length)
665
+ const source = document.files[0]?.content
666
+ if (typeof source !== 'string') {
667
+ throw new UiError('UI_REGISTRY_UNAVAILABLE', 'The released theme has no admitted CSS source.')
668
+ }
669
+ admitLocalThemeCss(source, slug)
670
+ return installThemeCss(
671
+ slug,
672
+ source,
673
+ digest(JSON.stringify(document)),
674
+ item.meta.canonicalAddress,
675
+ project,
676
+ options,
677
+ )
678
+ }
679
+
605
680
  export async function addUi(
606
681
  addresses: string[],
607
682
  options: { project?: string; dryRun?: boolean; overwrite?: boolean; yes?: boolean },
@@ -630,6 +705,7 @@ export async function addUi(
630
705
  return addLocalTheme(localThemes[0]!, project, options)
631
706
  }
632
707
  const { lock, release } = await lockedRelease(project, dependencies.fetcher)
708
+ const dependencySection = uiDependencySection(project.packageJson)
633
709
  const items = addresses.map((address) => findItem(release, address))
634
710
  const themes = items.filter((item) => item.type === 'registry:theme')
635
711
  if (themes.length > 1) {
@@ -638,6 +714,15 @@ export async function addUi(
638
714
  const itemDocuments = await Promise.all(
639
715
  items.map((item) => readUiRegistryItem(release, item, dependencies.fetcher)),
640
716
  )
717
+ if (themes.length === 1) {
718
+ if (items.length !== 1) {
719
+ throw new UiError(
720
+ 'UI_ITEM_CONFLICT',
721
+ 'Install a released theme separately from patterns and blocks.',
722
+ )
723
+ }
724
+ return addReleasedTheme(themes[0]!, itemDocuments[0]!, project, options)
725
+ }
641
726
  await rejectLocalChanges(project, lock, items, options.overwrite === true)
642
727
  const targets = (
643
728
  await Promise.all(
@@ -689,7 +774,12 @@ export async function addUi(
689
774
  throw new UiError('UI_TOOL_FAILED', 'The pinned shadcn operation omitted an item file.')
690
775
  }
691
776
  }
692
- await pinUiDependency(project, lock.package.version, dependencies.runner ?? defaultUiRunner)
777
+ await pinUiDependency(
778
+ project,
779
+ lock.package.version,
780
+ dependencySection,
781
+ dependencies.runner ?? defaultUiRunner,
782
+ )
693
783
  const theme = themes[0]
694
784
  const themeTarget = theme?.files[0]?.target
695
785
  if (themeTarget) {
@@ -779,11 +869,24 @@ export async function doctorUi(
779
869
  detail: error instanceof Error ? error.message : String(error),
780
870
  })
781
871
  }
782
- const dependencies = manifestDependencies(project.packageJson)
872
+ const runtimeVersion = manifestDependencies(project.packageJson)[UI_PACKAGE]
873
+ const developmentVersion = manifestDependencies(project.packageJson, 'devDependencies')[
874
+ UI_PACKAGE
875
+ ]
876
+ const declarations = [runtimeVersion, developmentVersion].filter(
877
+ (version): version is string => typeof version === 'string',
878
+ )
879
+ const packageVersion = declarations.length === 1 ? declarations[0] : undefined
783
880
  checks.push({
784
881
  check: 'package',
785
- ok: typeof dependencies[UI_PACKAGE] === 'string',
786
- detail: dependencies[UI_PACKAGE],
882
+ ok: declarations.length === 1 && (!lock || packageVersion === lock.package.version),
883
+ detail:
884
+ declarations.length > 1
885
+ ? 'declared in dependencies and devDependencies'
886
+ : packageVersion === undefined
887
+ ? 'missing'
888
+ : (runtimeVersion === packageVersion ? 'dependencies:' : 'devDependencies:') +
889
+ packageVersion,
787
890
  })
788
891
  const css = (await readOptional(project.cssPath)) ?? ''
789
892
  checks.push({ check: 'theme', ok: css.includes(UI_PACKAGE + '/theme.css') })
package/src/ui/release.ts CHANGED
@@ -250,8 +250,14 @@ function isInstallableItem(item: unknown): item is UiRegistry['items'][number] {
250
250
  /^(?:pattern|block)-[a-z0-9-]+$/u.test(candidate.name) &&
251
251
  typeof address === 'string' &&
252
252
  /^(?:pattern|block)\/[a-z0-9-]+\/[a-z0-9-/]+$/u.test(address)
253
+ const isComponent =
254
+ candidate.type === 'registry:component' &&
255
+ typeof candidate.name === 'string' &&
256
+ /^component-[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u.test(candidate.name) &&
257
+ typeof address === 'string' &&
258
+ /^component\/[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u.test(address)
253
259
  return (
254
- (isTheme || isComposition) &&
260
+ (isTheme || isComposition || isComponent) &&
255
261
  (candidate.dependencies === undefined ||
256
262
  (Array.isArray(candidate.dependencies) &&
257
263
  candidate.dependencies.every(
@@ -274,7 +280,8 @@ function isInstallableItem(item: unknown): item is UiRegistry['items'][number] {
274
280
  (candidate.files?.length === 1 &&
275
281
  file.type === 'registry:file' &&
276
282
  file.target ===
277
- 'components/astrale/theme/' + address!.slice('theme/'.length) + '.css')),
283
+ 'components/astrale/theme/' + address!.slice('theme/'.length) + '.css')) &&
284
+ (!isComponent || file.target.startsWith('components/astrale/component/')),
278
285
  ) &&
279
286
  typeof address === 'string'
280
287
  )