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

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/dist/astrale.js CHANGED
@@ -2578,7 +2578,7 @@ var package_default;
2578
2578
  var init_package = __esm(() => {
2579
2579
  package_default = {
2580
2580
  name: "@astrale-os/cli",
2581
- version: "1.0.0-beta.21",
2581
+ version: "1.0.0-beta.22",
2582
2582
  description: "Astrale CLI — connect to existing Astrale kernels",
2583
2583
  keywords: [
2584
2584
  "astrale",
@@ -85157,6 +85157,9 @@ async function addLocalTheme(address, project2, options) {
85157
85157
  }
85158
85158
  const source2 = await readFile18(sourcePath, "utf8");
85159
85159
  admitLocalThemeCss(source2, slug);
85160
+ return installThemeCss(slug, source2, digest5(source2), address, project2, options);
85161
+ }
85162
+ async function installThemeCss(slug, source2, sourceDigest, sourceLabel, project2, options) {
85160
85163
  const lock = await readUiLock(project2.uiLockPath);
85161
85164
  const canonicalAddress = "theme/" + slug;
85162
85165
  const relativeTarget = "components/astrale/theme/" + slug + ".css";
@@ -85178,7 +85181,7 @@ async function addLocalTheme(address, project2, options) {
85178
85181
  items: [canonicalAddress],
85179
85182
  files: { [canonicalAddress]: { [relativeTarget]: digest5(source2) } },
85180
85183
  activation: { file: projectRelative(project2, project2.cssPath), import: statement },
85181
- source: address
85184
+ source: sourceLabel
85182
85185
  };
85183
85186
  if (options.dryRun)
85184
85187
  return result;
@@ -85193,7 +85196,7 @@ async function addLocalTheme(address, project2, options) {
85193
85196
  await writeFile6(project2.cssPath, activateTheme(css, statement), "utf8");
85194
85197
  lock.items[canonicalAddress] = {
85195
85198
  address: canonicalAddress,
85196
- sourceDigest: digest5(source2),
85199
+ sourceDigest,
85197
85200
  files: { [relativeTarget]: digest5(source2) }
85198
85201
  };
85199
85202
  await writeJson(project2.uiLockPath, lock);
@@ -85208,6 +85211,15 @@ async function addLocalTheme(address, project2, options) {
85208
85211
  throw error52;
85209
85212
  }
85210
85213
  }
85214
+ async function addReleasedTheme(item, document, project2, options) {
85215
+ const slug = item.meta.canonicalAddress.slice("theme/".length);
85216
+ const source2 = document.files[0]?.content;
85217
+ if (typeof source2 !== "string") {
85218
+ throw new UiError("UI_REGISTRY_UNAVAILABLE", "The released theme has no admitted CSS source.");
85219
+ }
85220
+ admitLocalThemeCss(source2, slug);
85221
+ return installThemeCss(slug, source2, digest5(JSON.stringify(document)), item.meta.canonicalAddress, project2, options);
85222
+ }
85211
85223
  async function addUi(addresses, options, dependencies = {}) {
85212
85224
  if (addresses.length === 0) {
85213
85225
  throw new UiError("UI_ITEM_NOT_FOUND", "No UI item was provided.");
@@ -85231,6 +85243,12 @@ async function addUi(addresses, options, dependencies = {}) {
85231
85243
  throw new UiError("UI_ITEM_CONFLICT", "Only one theme can be activated per add operation.");
85232
85244
  }
85233
85245
  const itemDocuments = await Promise.all(items.map((item) => readUiRegistryItem(release, item, dependencies.fetcher)));
85246
+ if (themes.length === 1) {
85247
+ if (items.length !== 1) {
85248
+ throw new UiError("UI_ITEM_CONFLICT", "Install a released theme separately from patterns and blocks.");
85249
+ }
85250
+ return addReleasedTheme(themes[0], itemDocuments[0], project2, options);
85251
+ }
85234
85252
  await rejectLocalChanges(project2, lock, items, options.overwrite === true);
85235
85253
  const targets = (await Promise.all(items.flatMap((item) => item.files.filter((file2) => file2.target).map((file2) => assertSafePlannedTarget(project2, file2.target))))).filter((target2, index3, all) => all.indexOf(target2) === index3);
85236
85254
  const invocation3 = shadcnInvocation(project2.manager, release.compatibility.shadcn, [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrale-os/cli",
3
- "version": "1.0.0-beta.21",
3
+ "version": "1.0.0-beta.22",
4
4
  "description": "Astrale CLI — connect to existing Astrale kernels",
5
5
  "keywords": [
6
6
  "astrale",
@@ -5,8 +5,9 @@ does not add React, Base UI, Tailwind, shadcn, or the UI runtime to the installe
5
5
  graph.
6
6
 
7
7
  One registry operation resolves one immutable UI commit. Package compatibility and registry reads
8
- use that commit, and the detected package manager invokes the release-qualified shadcn CLI on
9
- demand. A local playground-exported theme is instead admitted and copied without network or
10
- shadcn. Both paths install consumer-owned source, activate themes through one project-relative CSS
8
+ use that commit. Patterns and blocks invoke the release-qualified shadcn CLI on demand through the
9
+ detected package manager. Released themes copy the exact admitted embedded CSS directly, while a
10
+ local playground export is admitted and copied without registry or shadcn. Both theme paths install
11
+ consumer-owned source and activate it through one project-relative CSS
11
12
  import, and complete application writes before the lock advances. Dry-run, list, and doctor do not
12
13
  claim a file mutation.
@@ -24,7 +24,7 @@ export const CLI_UI_BOUNDED_REMOTE_DOCUMENTS = defineLaw({
24
24
  export const CLI_UI_EXACT_ITEM_SOURCE = defineLaw({
25
25
  id: 'CLI-UI-EXACT-ITEM-SOURCE',
26
26
  statement:
27
- 'An add operation admits the exact built item document against the release index and records a content-bearing source digest before invoking project tooling.',
27
+ 'An add operation admits the exact built item document against the release index and records its content-bearing source digest before any consumer mutation; only patterns and blocks invoke project tooling.',
28
28
  tests: [{ file: '../__tests__/ui.test.ts', id: 'TEST-CLI-UI-EXACT-ITEM-SOURCE' }],
29
29
  })
30
30
 
@@ -183,21 +183,25 @@ function themeFetch(seen: string[] = []): typeof fetch {
183
183
  }
184
184
  if (url.endsWith('/registry/themes/registry.json')) return Response.json(themeRegistry)
185
185
  if (url.endsWith('/registry/public/r/theme-observatory.json')) {
186
- return Response.json({
187
- ...themeRegistry.items[0],
188
- files: [
189
- {
190
- ...themeRegistry.items[0]!.files[0],
191
- path: 'registry/themes/observatory.css',
192
- content: themeCss,
193
- },
194
- ],
195
- })
186
+ return Response.json(builtThemeItem())
196
187
  }
197
188
  return new Response('not found', { status: 404 })
198
189
  }) as typeof fetch
199
190
  }
200
191
 
192
+ function builtThemeItem() {
193
+ return {
194
+ ...themeRegistry.items[0],
195
+ files: [
196
+ {
197
+ ...themeRegistry.items[0]!.files[0],
198
+ path: 'registry/themes/observatory.css',
199
+ content: themeCss,
200
+ },
201
+ ],
202
+ }
203
+ }
204
+
201
205
  function builtItem(item: UiRegistry['items'][number]) {
202
206
  return {
203
207
  ...item,
@@ -887,26 +891,29 @@ describe('UI source operations', () => {
887
891
  "@import '@astrale-os/ui/theme.css';\n@import '@astrale-os/ui/presets/astrale.css';\n",
888
892
  )
889
893
  const target = path.join(root, 'components/astrale/theme/observatory.css')
894
+ let invoked = false
890
895
  const result = await addUi(
891
896
  ['theme/observatory'],
892
897
  { project: root, yes: true },
893
898
  {
894
899
  fetcher: themeFetch(),
895
900
  runner: async () => {
896
- await mkdir(path.dirname(target), { recursive: true })
897
- await writeFile(target, themeCss)
901
+ invoked = true
898
902
  return { code: 0, stdout: '', stderr: '' }
899
903
  },
900
904
  },
901
905
  )
902
906
 
907
+ expect(invoked).toBe(false)
903
908
  expect(result).toMatchObject({ status: 'installed', items: ['theme/observatory'] })
909
+ expect(await readFile(target, 'utf8')).toBe(themeCss)
904
910
  expect(await readFile(path.join(root, 'src/index.css'), 'utf8')).toContain(
905
911
  "@import '../components/astrale/theme/observatory.css';",
906
912
  )
907
913
  const written = JSON.parse(await readFile(path.join(root, 'astrale-ui.lock.json'), 'utf8'))
908
914
  expect(written.items['theme/observatory']).toMatchObject({
909
915
  address: 'theme/observatory',
916
+ sourceDigest: digest(JSON.stringify(builtThemeItem())),
910
917
  files: { 'components/astrale/theme/observatory.css': digest(themeCss) },
911
918
  })
912
919
  expect((await doctorUi(root)).healthy).toBe(true)
@@ -544,6 +544,17 @@ async function addLocalTheme(
544
544
  const source = await readFile(sourcePath, 'utf8')
545
545
  admitLocalThemeCss(source, slug)
546
546
 
547
+ return installThemeCss(slug, source, digest(source), address, project, options)
548
+ }
549
+
550
+ async function installThemeCss(
551
+ slug: string,
552
+ source: string,
553
+ sourceDigest: string,
554
+ sourceLabel: string,
555
+ project: UiProject,
556
+ options: { dryRun?: boolean; overwrite?: boolean },
557
+ ): Promise<Record<string, unknown>> {
547
558
  const lock = await readUiLock(project.uiLockPath)
548
559
  const canonicalAddress = 'theme/' + slug
549
560
  const relativeTarget = 'components/astrale/theme/' + slug + '.css'
@@ -573,7 +584,7 @@ async function addLocalTheme(
573
584
  items: [canonicalAddress],
574
585
  files: { [canonicalAddress]: { [relativeTarget]: digest(source) } },
575
586
  activation: { file: projectRelative(project, project.cssPath), import: statement },
576
- source: address,
587
+ source: sourceLabel,
577
588
  }
578
589
  if (options.dryRun) return result
579
590
 
@@ -588,7 +599,7 @@ async function addLocalTheme(
588
599
  await writeFile(project.cssPath, activateTheme(css, statement), 'utf8')
589
600
  lock.items[canonicalAddress] = {
590
601
  address: canonicalAddress,
591
- sourceDigest: digest(source),
602
+ sourceDigest,
592
603
  files: { [relativeTarget]: digest(source) },
593
604
  }
594
605
  await writeJson(project.uiLockPath, lock)
@@ -602,6 +613,28 @@ async function addLocalTheme(
602
613
  }
603
614
  }
604
615
 
616
+ async function addReleasedTheme(
617
+ item: UiRegistryItem,
618
+ document: UiRegistryItem,
619
+ project: UiProject,
620
+ options: { dryRun?: boolean; overwrite?: boolean },
621
+ ): Promise<Record<string, unknown>> {
622
+ const slug = item.meta.canonicalAddress.slice('theme/'.length)
623
+ const source = document.files[0]?.content
624
+ if (typeof source !== 'string') {
625
+ throw new UiError('UI_REGISTRY_UNAVAILABLE', 'The released theme has no admitted CSS source.')
626
+ }
627
+ admitLocalThemeCss(source, slug)
628
+ return installThemeCss(
629
+ slug,
630
+ source,
631
+ digest(JSON.stringify(document)),
632
+ item.meta.canonicalAddress,
633
+ project,
634
+ options,
635
+ )
636
+ }
637
+
605
638
  export async function addUi(
606
639
  addresses: string[],
607
640
  options: { project?: string; dryRun?: boolean; overwrite?: boolean; yes?: boolean },
@@ -638,6 +671,15 @@ export async function addUi(
638
671
  const itemDocuments = await Promise.all(
639
672
  items.map((item) => readUiRegistryItem(release, item, dependencies.fetcher)),
640
673
  )
674
+ if (themes.length === 1) {
675
+ if (items.length !== 1) {
676
+ throw new UiError(
677
+ 'UI_ITEM_CONFLICT',
678
+ 'Install a released theme separately from patterns and blocks.',
679
+ )
680
+ }
681
+ return addReleasedTheme(themes[0]!, itemDocuments[0]!, project, options)
682
+ }
641
683
  await rejectLocalChanges(project, lock, items, options.overwrite === true)
642
684
  const targets = (
643
685
  await Promise.all(