@astrale-os/cli 1.0.0-beta.15 → 1.0.0-beta.16
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 +130 -3
- package/package.json +1 -1
- package/src/ui/__tests__/ui.test.ts +251 -14
- package/src/ui/operations.ts +172 -1
- package/src/ui/project.ts +5 -0
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.
|
|
2581
|
+
version: "1.0.0-beta.16",
|
|
2582
2582
|
description: "Astrale CLI — connect to existing Astrale kernels",
|
|
2583
2583
|
keywords: [
|
|
2584
2584
|
"astrale",
|
|
@@ -83525,7 +83525,8 @@ async function discoverUiProject(input = process.cwd()) {
|
|
|
83525
83525
|
lockPath,
|
|
83526
83526
|
cssPath: path5.join(root, cssRelative),
|
|
83527
83527
|
componentsPath,
|
|
83528
|
-
uiLockPath: path5.join(root, "astrale-ui.lock.json")
|
|
83528
|
+
uiLockPath: path5.join(root, "astrale-ui.lock.json"),
|
|
83529
|
+
isAstraleDomain: await exists(path5.join(root, "astrale.config.ts")) && await exists(path5.join(root, "ui/index.ts")) && await exists(path5.join(root, "frontend/package.json"))
|
|
83529
83530
|
};
|
|
83530
83531
|
}
|
|
83531
83532
|
function assertSupportedUiProject(project2) {
|
|
@@ -83768,6 +83769,108 @@ async function readOptional(target2) {
|
|
|
83768
83769
|
function manifestDependencies(manifest) {
|
|
83769
83770
|
return { ...manifest.dependencies };
|
|
83770
83771
|
}
|
|
83772
|
+
function domainRegistryPackagePath(project2) {
|
|
83773
|
+
return path6.join(project2.root, "components/package.json");
|
|
83774
|
+
}
|
|
83775
|
+
function pnpmWorkspacePath(project2) {
|
|
83776
|
+
return path6.join(project2.root, "pnpm-workspace.yaml");
|
|
83777
|
+
}
|
|
83778
|
+
function domainRegistryPackageName(manifest) {
|
|
83779
|
+
const name = typeof manifest.name === "string" ? manifest.name : "astrale-domain";
|
|
83780
|
+
return name + "-ui-registry";
|
|
83781
|
+
}
|
|
83782
|
+
function appendWorkspace(manifest, workspace) {
|
|
83783
|
+
const configured = manifest.workspaces;
|
|
83784
|
+
if (configured === undefined) {
|
|
83785
|
+
manifest.workspaces = [workspace];
|
|
83786
|
+
return;
|
|
83787
|
+
}
|
|
83788
|
+
if (Array.isArray(configured) && configured.every((value3) => typeof value3 === "string")) {
|
|
83789
|
+
if (!configured.includes(workspace))
|
|
83790
|
+
manifest.workspaces = [...configured, workspace];
|
|
83791
|
+
return;
|
|
83792
|
+
}
|
|
83793
|
+
if (configured && typeof configured === "object") {
|
|
83794
|
+
const candidate2 = configured;
|
|
83795
|
+
if (Array.isArray(candidate2.packages) && candidate2.packages.every((value3) => typeof value3 === "string")) {
|
|
83796
|
+
if (!candidate2.packages.includes(workspace)) {
|
|
83797
|
+
manifest.workspaces = { ...candidate2, packages: [...candidate2.packages, workspace] };
|
|
83798
|
+
}
|
|
83799
|
+
return;
|
|
83800
|
+
}
|
|
83801
|
+
}
|
|
83802
|
+
throw new UiError("UI_PROJECT_UNSUPPORTED", "package.json has an invalid workspaces field.");
|
|
83803
|
+
}
|
|
83804
|
+
async function appendPnpmWorkspace(project2, workspace) {
|
|
83805
|
+
const target2 = pnpmWorkspacePath(project2);
|
|
83806
|
+
const source2 = await readOptional(target2);
|
|
83807
|
+
if (source2 === undefined) {
|
|
83808
|
+
await writeFile6(target2, `packages:
|
|
83809
|
+
- '` + workspace + `'
|
|
83810
|
+
`, "utf8");
|
|
83811
|
+
return;
|
|
83812
|
+
}
|
|
83813
|
+
const document = $parseDocument(source2);
|
|
83814
|
+
if (document.errors.length > 0) {
|
|
83815
|
+
throw new UiError("UI_PROJECT_UNSUPPORTED", "pnpm-workspace.yaml is not valid YAML.");
|
|
83816
|
+
}
|
|
83817
|
+
const packages = document.get("packages", true);
|
|
83818
|
+
if (!$isSeq(packages)) {
|
|
83819
|
+
throw new UiError("UI_PROJECT_UNSUPPORTED", "pnpm-workspace.yaml must declare a packages sequence.");
|
|
83820
|
+
}
|
|
83821
|
+
const values = packages.toJSON();
|
|
83822
|
+
if (!Array.isArray(values) || !values.every((value3) => typeof value3 === "string")) {
|
|
83823
|
+
throw new UiError("UI_PROJECT_UNSUPPORTED", "pnpm-workspace.yaml must declare a packages sequence.");
|
|
83824
|
+
}
|
|
83825
|
+
if (!values.includes(workspace))
|
|
83826
|
+
packages.add(workspace);
|
|
83827
|
+
await writeFile6(target2, String(document), "utf8");
|
|
83828
|
+
}
|
|
83829
|
+
async function hasDomainRegistryWorkspace(project2) {
|
|
83830
|
+
if (!project2.isAstraleDomain || !await exists2(domainRegistryPackagePath(project2)))
|
|
83831
|
+
return false;
|
|
83832
|
+
const registryManifest = JSON.parse(await readFile18(domainRegistryPackagePath(project2), "utf8"));
|
|
83833
|
+
if (registryManifest.private !== true || typeof registryManifest.name !== "string" || registryManifest.name === UI_PACKAGE || registryManifest.name === project2.packageJson.name) {
|
|
83834
|
+
return false;
|
|
83835
|
+
}
|
|
83836
|
+
if (project2.manager === "pnpm") {
|
|
83837
|
+
const source2 = await readOptional(pnpmWorkspacePath(project2));
|
|
83838
|
+
if (source2 === undefined)
|
|
83839
|
+
return false;
|
|
83840
|
+
const document = $parseDocument(source2);
|
|
83841
|
+
if (document.errors.length > 0)
|
|
83842
|
+
return false;
|
|
83843
|
+
const packages = document.get("packages", true);
|
|
83844
|
+
const values = $isSeq(packages) ? packages.toJSON() : undefined;
|
|
83845
|
+
return Array.isArray(values) && values.includes("components");
|
|
83846
|
+
}
|
|
83847
|
+
const configured = project2.packageJson.workspaces;
|
|
83848
|
+
const workspaces = Array.isArray(configured) ? configured : configured && typeof configured === "object" ? configured.packages : undefined;
|
|
83849
|
+
return Array.isArray(workspaces) && workspaces.includes("components");
|
|
83850
|
+
}
|
|
83851
|
+
async function writeDomainRegistryWorkspace(project2, manifest) {
|
|
83852
|
+
if (!project2.isAstraleDomain)
|
|
83853
|
+
return;
|
|
83854
|
+
if (project2.manager === "pnpm")
|
|
83855
|
+
await appendPnpmWorkspace(project2, "components");
|
|
83856
|
+
else
|
|
83857
|
+
appendWorkspace(manifest, "components");
|
|
83858
|
+
const target2 = domainRegistryPackagePath(project2);
|
|
83859
|
+
const existing = await readOptional(target2);
|
|
83860
|
+
const registryManifest = existing ? JSON.parse(existing) : { name: domainRegistryPackageName(manifest) };
|
|
83861
|
+
if (registryManifest.private === false) {
|
|
83862
|
+
throw new UiError("UI_PROJECT_UNSUPPORTED", "The Astrale registry source workspace must remain private.");
|
|
83863
|
+
}
|
|
83864
|
+
if (registryManifest.name === UI_PACKAGE || registryManifest.name === manifest.name || registryManifest.name !== undefined && typeof registryManifest.name !== "string") {
|
|
83865
|
+
throw new UiError("UI_PROJECT_UNSUPPORTED", "The Astrale registry source workspace must have a distinct package name.");
|
|
83866
|
+
}
|
|
83867
|
+
await writeJson(target2, {
|
|
83868
|
+
...registryManifest,
|
|
83869
|
+
name: registryManifest.name ?? domainRegistryPackageName(manifest),
|
|
83870
|
+
private: true,
|
|
83871
|
+
type: registryManifest.type ?? "module"
|
|
83872
|
+
});
|
|
83873
|
+
}
|
|
83771
83874
|
async function initUi(options, dependencies = {}) {
|
|
83772
83875
|
const project2 = await discoverUiProject(options.path);
|
|
83773
83876
|
assertSupportedUiProject(project2);
|
|
@@ -83782,7 +83885,7 @@ async function initUi(options, dependencies = {}) {
|
|
|
83782
83885
|
return;
|
|
83783
83886
|
});
|
|
83784
83887
|
const requestedVersion = options.version?.replace(/^v/u, "");
|
|
83785
|
-
const desired = (!requestedVersion || requestedVersion === lock.package.version) && (!options.preset || options.preset === lock.preset) && css.includes(UI_PACKAGE + "/theme.css") && css.includes(UI_PACKAGE + "/presets/" + lock.preset + ".css") && components?.style === "base-nova";
|
|
83888
|
+
const desired = (!requestedVersion || requestedVersion === lock.package.version) && (!options.preset || options.preset === lock.preset) && css.includes(UI_PACKAGE + "/theme.css") && css.includes(UI_PACKAGE + "/presets/" + lock.preset + ".css") && components?.style === "base-nova" && (!project2.isAstraleDomain || await hasDomainRegistryWorkspace(project2));
|
|
83786
83889
|
if (!desired) {
|
|
83787
83890
|
throw new UiError("UI_ITEM_CONFLICT", "Existing Astrale UI initialization differs from the requested state.", "Run astrale ui doctor, then repeat init with --force after reviewing the changes.");
|
|
83788
83891
|
}
|
|
@@ -83801,6 +83904,8 @@ async function initUi(options, dependencies = {}) {
|
|
|
83801
83904
|
cssRelative,
|
|
83802
83905
|
"components.json",
|
|
83803
83906
|
"astrale-ui.lock.json",
|
|
83907
|
+
...project2.isAstraleDomain ? ["components/package.json"] : [],
|
|
83908
|
+
...project2.isAstraleDomain && project2.manager === "pnpm" ? ["pnpm-workspace.yaml"] : [],
|
|
83804
83909
|
...project2.lockPath ? [projectRelative(project2, project2.lockPath)] : []
|
|
83805
83910
|
],
|
|
83806
83911
|
tooling: {
|
|
@@ -83812,11 +83917,19 @@ async function initUi(options, dependencies = {}) {
|
|
|
83812
83917
|
};
|
|
83813
83918
|
if (options.dryRun)
|
|
83814
83919
|
return plan;
|
|
83920
|
+
if (project2.isAstraleDomain) {
|
|
83921
|
+
await assertSafePlannedTarget(project2, "components/package.json");
|
|
83922
|
+
if (project2.manager === "pnpm") {
|
|
83923
|
+
await assertSafePlannedTarget(project2, "pnpm-workspace.yaml");
|
|
83924
|
+
}
|
|
83925
|
+
}
|
|
83815
83926
|
const mutationPaths = [
|
|
83816
83927
|
project2.packageJsonPath,
|
|
83817
83928
|
project2.cssPath,
|
|
83818
83929
|
project2.componentsPath,
|
|
83819
83930
|
project2.uiLockPath,
|
|
83931
|
+
...project2.isAstraleDomain ? [domainRegistryPackagePath(project2)] : [],
|
|
83932
|
+
...project2.isAstraleDomain && project2.manager === "pnpm" ? [pnpmWorkspacePath(project2)] : [],
|
|
83820
83933
|
...project2.lockPath ? [project2.lockPath] : []
|
|
83821
83934
|
];
|
|
83822
83935
|
const snapshots = new Map;
|
|
@@ -83828,6 +83941,7 @@ async function initUi(options, dependencies = {}) {
|
|
|
83828
83941
|
...manifestDependencies(manifest),
|
|
83829
83942
|
[UI_PACKAGE]: release.version
|
|
83830
83943
|
};
|
|
83944
|
+
await writeDomainRegistryWorkspace(project2, manifest);
|
|
83831
83945
|
await writeJson(project2.packageJsonPath, manifest);
|
|
83832
83946
|
const currentCss = await readOptional(project2.cssPath) ?? "";
|
|
83833
83947
|
const imports = [
|
|
@@ -83900,6 +84014,17 @@ async function initUi(options, dependencies = {}) {
|
|
|
83900
84014
|
throw error52;
|
|
83901
84015
|
}
|
|
83902
84016
|
}
|
|
84017
|
+
async function pinUiDependency(project2, version2, runner) {
|
|
84018
|
+
const manifest = JSON.parse(await readFile18(project2.packageJsonPath, "utf8"));
|
|
84019
|
+
if (manifestDependencies(manifest)[UI_PACKAGE] === version2)
|
|
84020
|
+
return;
|
|
84021
|
+
manifest.dependencies = { ...manifestDependencies(manifest), [UI_PACKAGE]: version2 };
|
|
84022
|
+
await writeJson(project2.packageJsonPath, manifest);
|
|
84023
|
+
const result = await runner(project2.manager, ["install"], project2.root);
|
|
84024
|
+
if (result.code !== 0) {
|
|
84025
|
+
throw new UiError("UI_DEPENDENCY_INSTALL_FAILED", project2.manager + " install failed while restoring the locked UI release.", result.stderr.trim() || undefined);
|
|
84026
|
+
}
|
|
84027
|
+
}
|
|
83903
84028
|
async function listUi(query4, options, dependencies = {}) {
|
|
83904
84029
|
const release = await resolveUiRelease(options.version, dependencies.fetcher);
|
|
83905
84030
|
const needle = query4?.trim().toLowerCase();
|
|
@@ -83965,6 +84090,7 @@ async function addUi(addresses, options, dependencies = {}) {
|
|
|
83965
84090
|
throw new UiError("UI_TOOL_FAILED", "The pinned shadcn operation omitted an item file.");
|
|
83966
84091
|
}
|
|
83967
84092
|
}
|
|
84093
|
+
await pinUiDependency(project2, lock.package.version, dependencies.runner ?? defaultUiRunner);
|
|
83968
84094
|
}
|
|
83969
84095
|
} catch (error52) {
|
|
83970
84096
|
for (const [target2, previous] of snapshots) {
|
|
@@ -84148,6 +84274,7 @@ async function writeJson(target2, value3) {
|
|
|
84148
84274
|
`, "utf8");
|
|
84149
84275
|
}
|
|
84150
84276
|
var init_operations3 = __esm(() => {
|
|
84277
|
+
init_dist();
|
|
84151
84278
|
init_lock();
|
|
84152
84279
|
init_model7();
|
|
84153
84280
|
init_project3();
|
package/package.json
CHANGED
|
@@ -94,7 +94,11 @@ async function fixture(): Promise<string> {
|
|
|
94
94
|
JSON.stringify({
|
|
95
95
|
name: 'fixture',
|
|
96
96
|
private: true,
|
|
97
|
-
dependencies: {
|
|
97
|
+
dependencies: {
|
|
98
|
+
react: '19.2.8',
|
|
99
|
+
'react-dom': '19.2.8',
|
|
100
|
+
tailwindcss: '4.3.3',
|
|
101
|
+
},
|
|
98
102
|
packageManager: 'pnpm@11.13.1',
|
|
99
103
|
}),
|
|
100
104
|
)
|
|
@@ -102,6 +106,16 @@ async function fixture(): Promise<string> {
|
|
|
102
106
|
return root
|
|
103
107
|
}
|
|
104
108
|
|
|
109
|
+
async function lockedFixture(): Promise<string> {
|
|
110
|
+
const root = await fixture()
|
|
111
|
+
const manifestPath = path.join(root, 'package.json')
|
|
112
|
+
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'))
|
|
113
|
+
manifest.dependencies['@astrale-os/ui'] = '0.3.0-beta.0'
|
|
114
|
+
await writeFile(manifestPath, JSON.stringify(manifest))
|
|
115
|
+
await writeFile(path.join(root, 'astrale-ui.lock.json'), JSON.stringify(lock()))
|
|
116
|
+
return root
|
|
117
|
+
}
|
|
118
|
+
|
|
105
119
|
function lock(): UiLock {
|
|
106
120
|
return {
|
|
107
121
|
$schema: 'https://example.invalid/ui-lock.schema.json',
|
|
@@ -360,6 +374,10 @@ describe('UI initialization transaction', () => {
|
|
|
360
374
|
JSON.stringify({ name: 'astrale-frontend', private: true, type: 'module' }),
|
|
361
375
|
)
|
|
362
376
|
await writeFile(path.join(root, 'frontend/src/styles.css'), '/* Domain frontend */\n')
|
|
377
|
+
await mkdir(path.join(root, 'ui'), { recursive: true })
|
|
378
|
+
await writeFile(path.join(root, 'ui/index.ts'), 'export {}\n')
|
|
379
|
+
await writeFile(path.join(root, 'astrale.config.ts'), 'export default {}\n')
|
|
380
|
+
await writeFile(path.join(root, 'pnpm-workspace.yaml'), "packages:\n - 'frontend'\n")
|
|
363
381
|
const rootCssBefore = await readFile(path.join(root, 'src/index.css'), 'utf8')
|
|
364
382
|
|
|
365
383
|
await initUi(
|
|
@@ -375,8 +393,14 @@ describe('UI initialization transaction', () => {
|
|
|
375
393
|
expect(await readFile(path.join(root, 'src/index.css'), 'utf8')).toBe(rootCssBefore)
|
|
376
394
|
expect(
|
|
377
395
|
JSON.parse(await readFile(path.join(root, 'package.json'), 'utf8')).dependencies,
|
|
378
|
-
).toHaveProperty('@astrale-os/ui')
|
|
396
|
+
).toHaveProperty('@astrale-os/ui', '0.3.0-beta.0')
|
|
379
397
|
expect(await Bun.file(path.join(root, 'src/astrale-ui.css')).exists()).toBe(false)
|
|
398
|
+
expect(JSON.parse(await readFile(path.join(root, 'components/package.json'), 'utf8'))).toEqual({
|
|
399
|
+
name: 'fixture-ui-registry',
|
|
400
|
+
private: true,
|
|
401
|
+
type: 'module',
|
|
402
|
+
})
|
|
403
|
+
expect(await readFile(path.join(root, 'pnpm-workspace.yaml'), 'utf8')).toContain('components')
|
|
380
404
|
|
|
381
405
|
await writeFile(path.join(root, 'src/app.css'), '/* later root stylesheet */\n')
|
|
382
406
|
const repeated = await initUi(
|
|
@@ -386,6 +410,153 @@ describe('UI initialization transaction', () => {
|
|
|
386
410
|
expect(repeated.status).toBe('unchanged')
|
|
387
411
|
})
|
|
388
412
|
|
|
413
|
+
test('adds the private registry workspace to an npm-authored Domain manifest', async () => {
|
|
414
|
+
const root = await fixture()
|
|
415
|
+
const manifestPath = path.join(root, 'package.json')
|
|
416
|
+
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'))
|
|
417
|
+
manifest.packageManager = 'npm@11.16.0'
|
|
418
|
+
manifest.workspaces = ['frontend']
|
|
419
|
+
await writeFile(manifestPath, JSON.stringify(manifest))
|
|
420
|
+
await writeFile(path.join(root, 'package-lock.json'), '{}')
|
|
421
|
+
await mkdir(path.join(root, 'frontend/src'), { recursive: true })
|
|
422
|
+
await writeFile(
|
|
423
|
+
path.join(root, 'frontend/package.json'),
|
|
424
|
+
JSON.stringify({ name: 'astrale-frontend', private: true }),
|
|
425
|
+
)
|
|
426
|
+
await writeFile(path.join(root, 'frontend/src/styles.css'), '/* Domain frontend */\n')
|
|
427
|
+
await mkdir(path.join(root, 'ui'), { recursive: true })
|
|
428
|
+
await writeFile(path.join(root, 'ui/index.ts'), 'export {}\n')
|
|
429
|
+
await writeFile(path.join(root, 'astrale.config.ts'), 'export default {}\n')
|
|
430
|
+
|
|
431
|
+
const result = await initUi(
|
|
432
|
+
{ path: root, version: '0.3.0-beta.0', install: false },
|
|
433
|
+
{ fetcher: mockFetch() },
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
expect(result.files).toEqual(expect.arrayContaining(['components/package.json']))
|
|
437
|
+
expect(JSON.parse(await readFile(manifestPath, 'utf8')).workspaces).toEqual([
|
|
438
|
+
'frontend',
|
|
439
|
+
'components',
|
|
440
|
+
])
|
|
441
|
+
expect(JSON.parse(await readFile(path.join(root, 'components/package.json'), 'utf8'))).toEqual({
|
|
442
|
+
name: 'fixture-ui-registry',
|
|
443
|
+
private: true,
|
|
444
|
+
type: 'module',
|
|
445
|
+
})
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
test('rejects a Domain registry workspace whose physical parent escapes the project', async () => {
|
|
449
|
+
const root = await fixture()
|
|
450
|
+
const outside = await fixture()
|
|
451
|
+
await mkdir(path.join(root, 'frontend/src'), { recursive: true })
|
|
452
|
+
await writeFile(
|
|
453
|
+
path.join(root, 'frontend/package.json'),
|
|
454
|
+
JSON.stringify({ name: 'astrale-frontend', private: true }),
|
|
455
|
+
)
|
|
456
|
+
await writeFile(path.join(root, 'frontend/src/styles.css'), '/* Domain frontend */\n')
|
|
457
|
+
await mkdir(path.join(root, 'ui'), { recursive: true })
|
|
458
|
+
await writeFile(path.join(root, 'ui/index.ts'), 'export {}\n')
|
|
459
|
+
await writeFile(path.join(root, 'astrale.config.ts'), 'export default {}\n')
|
|
460
|
+
await symlink(outside, path.join(root, 'components'), 'dir')
|
|
461
|
+
const outsideManifest = await readFile(path.join(outside, 'package.json'), 'utf8')
|
|
462
|
+
const rootManifest = await readFile(path.join(root, 'package.json'), 'utf8')
|
|
463
|
+
|
|
464
|
+
await expect(
|
|
465
|
+
initUi({ path: root, version: '0.3.0-beta.0', install: false }, { fetcher: mockFetch() }),
|
|
466
|
+
).rejects.toMatchObject({ code: 'UI_LOCK_INVALID' })
|
|
467
|
+
expect(await readFile(path.join(outside, 'package.json'), 'utf8')).toBe(outsideManifest)
|
|
468
|
+
expect(await readFile(path.join(root, 'package.json'), 'utf8')).toBe(rootManifest)
|
|
469
|
+
expect(await Bun.file(path.join(root, 'astrale-ui.lock.json')).exists()).toBe(false)
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
test('rejects a symlinked pnpm workspace manifest before any Domain mutation', async () => {
|
|
473
|
+
const root = await fixture()
|
|
474
|
+
const outside = await fixture()
|
|
475
|
+
await mkdir(path.join(root, 'frontend/src'), { recursive: true })
|
|
476
|
+
await writeFile(
|
|
477
|
+
path.join(root, 'frontend/package.json'),
|
|
478
|
+
JSON.stringify({ name: 'astrale-frontend', private: true }),
|
|
479
|
+
)
|
|
480
|
+
await writeFile(path.join(root, 'frontend/src/styles.css'), '/* Domain frontend */\n')
|
|
481
|
+
await mkdir(path.join(root, 'ui'), { recursive: true })
|
|
482
|
+
await writeFile(path.join(root, 'ui/index.ts'), 'export {}\n')
|
|
483
|
+
await writeFile(path.join(root, 'astrale.config.ts'), 'export default {}\n')
|
|
484
|
+
const outsideWorkspace = path.join(outside, 'workspace.yaml')
|
|
485
|
+
await writeFile(outsideWorkspace, "packages:\n - 'outside'\n")
|
|
486
|
+
await symlink(outsideWorkspace, path.join(root, 'pnpm-workspace.yaml'), 'file')
|
|
487
|
+
const outsideBefore = await readFile(outsideWorkspace, 'utf8')
|
|
488
|
+
|
|
489
|
+
await expect(
|
|
490
|
+
initUi({ path: root, version: '0.3.0-beta.0', install: false }, { fetcher: mockFetch() }),
|
|
491
|
+
).rejects.toMatchObject({ code: 'UI_LOCK_INVALID' })
|
|
492
|
+
expect(await readFile(outsideWorkspace, 'utf8')).toBe(outsideBefore)
|
|
493
|
+
expect(await Bun.file(path.join(root, 'components/package.json')).exists()).toBe(false)
|
|
494
|
+
expect(await Bun.file(path.join(root, 'astrale-ui.lock.json')).exists()).toBe(false)
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
test('restores every Domain workspace mutation when dependency installation fails', async () => {
|
|
498
|
+
const root = await fixture()
|
|
499
|
+
await mkdir(path.join(root, 'frontend/src'), { recursive: true })
|
|
500
|
+
await writeFile(
|
|
501
|
+
path.join(root, 'frontend/package.json'),
|
|
502
|
+
JSON.stringify({ name: 'astrale-frontend', private: true }),
|
|
503
|
+
)
|
|
504
|
+
await writeFile(path.join(root, 'frontend/src/styles.css'), '/* Domain frontend */\n')
|
|
505
|
+
await mkdir(path.join(root, 'ui'), { recursive: true })
|
|
506
|
+
await writeFile(path.join(root, 'ui/index.ts'), 'export {}\n')
|
|
507
|
+
await writeFile(path.join(root, 'astrale.config.ts'), 'export default {}\n')
|
|
508
|
+
await writeFile(path.join(root, 'pnpm-workspace.yaml'), "packages:\n - 'frontend'\n")
|
|
509
|
+
const manifestBefore = await readFile(path.join(root, 'package.json'), 'utf8')
|
|
510
|
+
const cssBefore = await readFile(path.join(root, 'frontend/src/styles.css'), 'utf8')
|
|
511
|
+
const workspaceBefore = await readFile(path.join(root, 'pnpm-workspace.yaml'), 'utf8')
|
|
512
|
+
|
|
513
|
+
await expect(
|
|
514
|
+
initUi(
|
|
515
|
+
{ path: root, version: '0.3.0-beta.0' },
|
|
516
|
+
{
|
|
517
|
+
fetcher: mockFetch(),
|
|
518
|
+
runner: async () => ({ code: 1, stdout: '', stderr: 'install failed' }),
|
|
519
|
+
},
|
|
520
|
+
),
|
|
521
|
+
).rejects.toMatchObject({ code: 'UI_DEPENDENCY_INSTALL_FAILED' })
|
|
522
|
+
|
|
523
|
+
expect(await readFile(path.join(root, 'package.json'), 'utf8')).toBe(manifestBefore)
|
|
524
|
+
expect(await readFile(path.join(root, 'frontend/src/styles.css'), 'utf8')).toBe(cssBefore)
|
|
525
|
+
expect(await readFile(path.join(root, 'pnpm-workspace.yaml'), 'utf8')).toBe(workspaceBefore)
|
|
526
|
+
expect(await Bun.file(path.join(root, 'components/package.json')).exists()).toBe(false)
|
|
527
|
+
expect(await Bun.file(path.join(root, 'components.json')).exists()).toBe(false)
|
|
528
|
+
expect(await Bun.file(path.join(root, 'astrale-ui.lock.json')).exists()).toBe(false)
|
|
529
|
+
expect(await Bun.file(path.join(root, 'pnpm-lock.yaml')).exists()).toBe(false)
|
|
530
|
+
})
|
|
531
|
+
|
|
532
|
+
test('rejects a registry workspace that could shadow the public UI package', async () => {
|
|
533
|
+
const root = await fixture()
|
|
534
|
+
await mkdir(path.join(root, 'frontend/src'), { recursive: true })
|
|
535
|
+
await writeFile(
|
|
536
|
+
path.join(root, 'frontend/package.json'),
|
|
537
|
+
JSON.stringify({ name: 'astrale-frontend', private: true }),
|
|
538
|
+
)
|
|
539
|
+
await writeFile(path.join(root, 'frontend/src/styles.css'), '/* Domain frontend */\n')
|
|
540
|
+
await mkdir(path.join(root, 'ui'), { recursive: true })
|
|
541
|
+
await writeFile(path.join(root, 'ui/index.ts'), 'export {}\n')
|
|
542
|
+
await writeFile(path.join(root, 'astrale.config.ts'), 'export default {}\n')
|
|
543
|
+
await writeFile(path.join(root, 'pnpm-workspace.yaml'), "packages:\n - 'frontend'\n")
|
|
544
|
+
await mkdir(path.join(root, 'components'), { recursive: true })
|
|
545
|
+
await writeFile(
|
|
546
|
+
path.join(root, 'components/package.json'),
|
|
547
|
+
JSON.stringify({ name: '@astrale-os/ui', private: true, version: '0.3.0-beta.0' }),
|
|
548
|
+
)
|
|
549
|
+
const workspaceBefore = await readFile(path.join(root, 'pnpm-workspace.yaml'), 'utf8')
|
|
550
|
+
|
|
551
|
+
await expect(
|
|
552
|
+
initUi({ path: root, version: '0.3.0-beta.0', install: false }, { fetcher: mockFetch() }),
|
|
553
|
+
).rejects.toMatchObject({ code: 'UI_PROJECT_UNSUPPORTED' })
|
|
554
|
+
expect(await readFile(path.join(root, 'pnpm-workspace.yaml'), 'utf8')).toBe(workspaceBefore)
|
|
555
|
+
expect(
|
|
556
|
+
JSON.parse(await readFile(path.join(root, 'components/package.json'), 'utf8')).name,
|
|
557
|
+
).toBe('@astrale-os/ui')
|
|
558
|
+
})
|
|
559
|
+
|
|
389
560
|
test('rejects configured and discovered stylesheets whose physical parent escapes', async () => {
|
|
390
561
|
const root = await fixture()
|
|
391
562
|
const outside = await fixture()
|
|
@@ -526,8 +697,7 @@ describe('UI source operations', () => {
|
|
|
526
697
|
})
|
|
527
698
|
|
|
528
699
|
test('add dry-run invokes the exact shadcn version and does not advance the lock', async () => {
|
|
529
|
-
const root = await
|
|
530
|
-
await writeFile(path.join(root, 'astrale-ui.lock.json'), JSON.stringify(lock()))
|
|
700
|
+
const root = await lockedFixture()
|
|
531
701
|
const before = await readFile(path.join(root, 'astrale-ui.lock.json'), 'utf8')
|
|
532
702
|
const calls: Array<{ file: string; args: string[] }> = []
|
|
533
703
|
const result = await addUi(
|
|
@@ -555,8 +725,7 @@ describe('UI source operations', () => {
|
|
|
555
725
|
})
|
|
556
726
|
|
|
557
727
|
test('successful add records installed file digests and doctor detects later edits', async () => {
|
|
558
|
-
const root = await
|
|
559
|
-
await writeFile(path.join(root, 'astrale-ui.lock.json'), JSON.stringify(lock()))
|
|
728
|
+
const root = await lockedFixture()
|
|
560
729
|
await writeFile(
|
|
561
730
|
path.join(root, 'components.json'),
|
|
562
731
|
JSON.stringify({ style: 'base-nova', tailwind: { css: 'src/index.css' } }),
|
|
@@ -609,9 +778,80 @@ describe('UI source operations', () => {
|
|
|
609
778
|
).rejects.toBeInstanceOf(UiError)
|
|
610
779
|
})
|
|
611
780
|
|
|
781
|
+
test('restores the exact locked UI dependency after shadcn applies its compatible range', async () => {
|
|
782
|
+
const root = await lockedFixture()
|
|
783
|
+
const installed = path.join(root, 'components/astrale/pattern/chart/line-basic.tsx')
|
|
784
|
+
const calls: Array<{ file: string; args: string[] }> = []
|
|
785
|
+
|
|
786
|
+
await addUi(
|
|
787
|
+
['pattern/chart/line/basic'],
|
|
788
|
+
{ project: root, yes: true },
|
|
789
|
+
{
|
|
790
|
+
fetcher: mockFetch(),
|
|
791
|
+
runner: async (file, args) => {
|
|
792
|
+
calls.push({ file, args })
|
|
793
|
+
if (args[0] === 'dlx') {
|
|
794
|
+
await mkdir(path.dirname(installed), { recursive: true })
|
|
795
|
+
await writeFile(installed, 'export const Chart = true\n')
|
|
796
|
+
const manifestPath = path.join(root, 'package.json')
|
|
797
|
+
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'))
|
|
798
|
+
manifest.dependencies['@astrale-os/ui'] = '^0.3.0-beta.0'
|
|
799
|
+
await writeFile(manifestPath, JSON.stringify(manifest))
|
|
800
|
+
}
|
|
801
|
+
return { code: 0, stdout: '', stderr: '' }
|
|
802
|
+
},
|
|
803
|
+
},
|
|
804
|
+
)
|
|
805
|
+
|
|
806
|
+
expect(calls).toEqual([
|
|
807
|
+
expect.objectContaining({ file: 'pnpm', args: expect.arrayContaining(['dlx']) }),
|
|
808
|
+
{ file: 'pnpm', args: ['install'] },
|
|
809
|
+
])
|
|
810
|
+
expect(
|
|
811
|
+
JSON.parse(await readFile(path.join(root, 'package.json'), 'utf8')).dependencies[
|
|
812
|
+
'@astrale-os/ui'
|
|
813
|
+
],
|
|
814
|
+
).toBe('0.3.0-beta.0')
|
|
815
|
+
})
|
|
816
|
+
|
|
817
|
+
test('rolls back item and package state when restoring the locked dependency fails', async () => {
|
|
818
|
+
const root = await lockedFixture()
|
|
819
|
+
const manifestPath = path.join(root, 'package.json')
|
|
820
|
+
const lockPath = path.join(root, 'astrale-ui.lock.json')
|
|
821
|
+
const installed = path.join(root, 'components/astrale/pattern/chart/line-basic.tsx')
|
|
822
|
+
const manifestBefore = await readFile(manifestPath, 'utf8')
|
|
823
|
+
const lockBefore = await readFile(lockPath, 'utf8')
|
|
824
|
+
|
|
825
|
+
await expect(
|
|
826
|
+
addUi(
|
|
827
|
+
['pattern/chart/line/basic'],
|
|
828
|
+
{ project: root, yes: true },
|
|
829
|
+
{
|
|
830
|
+
fetcher: mockFetch(),
|
|
831
|
+
runner: async (_file, args) => {
|
|
832
|
+
if (args[0] === 'dlx') {
|
|
833
|
+
await mkdir(path.dirname(installed), { recursive: true })
|
|
834
|
+
await writeFile(installed, 'export const Chart = true\n')
|
|
835
|
+
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'))
|
|
836
|
+
manifest.dependencies['@astrale-os/ui'] = '^0.3.0-beta.0'
|
|
837
|
+
await writeFile(manifestPath, JSON.stringify(manifest))
|
|
838
|
+
return { code: 0, stdout: '', stderr: '' }
|
|
839
|
+
}
|
|
840
|
+
await writeFile(path.join(root, 'pnpm-lock.yaml'), 'partial lock\n')
|
|
841
|
+
return { code: 1, stdout: '', stderr: 'registry timeout' }
|
|
842
|
+
},
|
|
843
|
+
},
|
|
844
|
+
),
|
|
845
|
+
).rejects.toMatchObject({ code: 'UI_DEPENDENCY_INSTALL_FAILED' })
|
|
846
|
+
|
|
847
|
+
expect(await readFile(manifestPath, 'utf8')).toBe(manifestBefore)
|
|
848
|
+
expect(await readFile(lockPath, 'utf8')).toBe(lockBefore)
|
|
849
|
+
expect(await Bun.file(installed).exists()).toBe(false)
|
|
850
|
+
expect(await Bun.file(path.join(root, 'pnpm-lock.yaml')).exists()).toBe(false)
|
|
851
|
+
})
|
|
852
|
+
|
|
612
853
|
test('preflights symlink targets without invoking shadcn', async () => {
|
|
613
|
-
const root = await
|
|
614
|
-
await writeFile(path.join(root, 'astrale-ui.lock.json'), JSON.stringify(lock()))
|
|
854
|
+
const root = await lockedFixture()
|
|
615
855
|
const outside = await mkdtemp(path.join(tmpdir(), 'astrale-ui-outside-'))
|
|
616
856
|
temporary.push(outside)
|
|
617
857
|
await symlink(outside, path.join(root, 'components'))
|
|
@@ -633,8 +873,7 @@ describe('UI source operations', () => {
|
|
|
633
873
|
})
|
|
634
874
|
|
|
635
875
|
test('restores declared files and package state after a partial shadcn failure', async () => {
|
|
636
|
-
const root = await
|
|
637
|
-
await writeFile(path.join(root, 'astrale-ui.lock.json'), JSON.stringify(lock()))
|
|
876
|
+
const root = await lockedFixture()
|
|
638
877
|
const first = path.join(root, 'components/astrale/pattern/chart/line-basic.tsx')
|
|
639
878
|
const second = path.join(root, 'components/astrale/pattern/chart/summary.tsx')
|
|
640
879
|
await mkdir(path.dirname(first), { recursive: true })
|
|
@@ -676,8 +915,7 @@ describe('UI source operations', () => {
|
|
|
676
915
|
})
|
|
677
916
|
|
|
678
917
|
test('overwrite requires explicit yes confirmation before invoking shadcn', async () => {
|
|
679
|
-
const root = await
|
|
680
|
-
await writeFile(path.join(root, 'astrale-ui.lock.json'), JSON.stringify(lock()))
|
|
918
|
+
const root = await lockedFixture()
|
|
681
919
|
let invoked = false
|
|
682
920
|
await expect(
|
|
683
921
|
addUi(
|
|
@@ -697,8 +935,7 @@ describe('UI source operations', () => {
|
|
|
697
935
|
|
|
698
936
|
/** @evidence TEST-CLI-UI-EXACT-ITEM-SOURCE */
|
|
699
937
|
test('rejects a built item that differs from the admitted release index before invoking shadcn', async () => {
|
|
700
|
-
const root = await
|
|
701
|
-
await writeFile(path.join(root, 'astrale-ui.lock.json'), JSON.stringify(lock()))
|
|
938
|
+
const root = await lockedFixture()
|
|
702
939
|
const fallback = mockFetch()
|
|
703
940
|
let invoked = false
|
|
704
941
|
const malformed = (async (input: string | URL | Request, init?: RequestInit) => {
|
package/src/ui/operations.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { access, lstat, mkdir, readFile, realpath, rm, writeFile } from 'node:fs/promises'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
+
import { isSeq, parseDocument } from 'yaml'
|
|
3
4
|
|
|
4
5
|
import { digest, parseUiLock, readUiLock } from './lock'
|
|
5
6
|
import {
|
|
@@ -42,6 +43,140 @@ function manifestDependencies(manifest: Record<string, unknown>): Record<string,
|
|
|
42
43
|
return { ...(manifest.dependencies as Record<string, string> | undefined) }
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
function domainRegistryPackagePath(project: UiProject): string {
|
|
47
|
+
return path.join(project.root, 'components/package.json')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function pnpmWorkspacePath(project: UiProject): string {
|
|
51
|
+
return path.join(project.root, 'pnpm-workspace.yaml')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function domainRegistryPackageName(manifest: Record<string, unknown>): string {
|
|
55
|
+
const name = typeof manifest.name === 'string' ? manifest.name : 'astrale-domain'
|
|
56
|
+
return name + '-ui-registry'
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function appendWorkspace(manifest: Record<string, unknown>, workspace: string): void {
|
|
60
|
+
const configured = manifest.workspaces
|
|
61
|
+
if (configured === undefined) {
|
|
62
|
+
manifest.workspaces = [workspace]
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
if (Array.isArray(configured) && configured.every((value) => typeof value === 'string')) {
|
|
66
|
+
if (!configured.includes(workspace)) manifest.workspaces = [...configured, workspace]
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
if (configured && typeof configured === 'object') {
|
|
70
|
+
const candidate = configured as { packages?: unknown }
|
|
71
|
+
if (
|
|
72
|
+
Array.isArray(candidate.packages) &&
|
|
73
|
+
candidate.packages.every((value) => typeof value === 'string')
|
|
74
|
+
) {
|
|
75
|
+
if (!candidate.packages.includes(workspace)) {
|
|
76
|
+
manifest.workspaces = { ...candidate, packages: [...candidate.packages, workspace] }
|
|
77
|
+
}
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
throw new UiError('UI_PROJECT_UNSUPPORTED', 'package.json has an invalid workspaces field.')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function appendPnpmWorkspace(project: UiProject, workspace: string): Promise<void> {
|
|
85
|
+
const target = pnpmWorkspacePath(project)
|
|
86
|
+
const source = await readOptional(target)
|
|
87
|
+
if (source === undefined) {
|
|
88
|
+
await writeFile(target, "packages:\n - '" + workspace + "'\n", 'utf8')
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
const document = parseDocument(source)
|
|
92
|
+
if (document.errors.length > 0) {
|
|
93
|
+
throw new UiError('UI_PROJECT_UNSUPPORTED', 'pnpm-workspace.yaml is not valid YAML.')
|
|
94
|
+
}
|
|
95
|
+
const packages = document.get('packages', true)
|
|
96
|
+
if (!isSeq(packages)) {
|
|
97
|
+
throw new UiError(
|
|
98
|
+
'UI_PROJECT_UNSUPPORTED',
|
|
99
|
+
'pnpm-workspace.yaml must declare a packages sequence.',
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
const values = packages.toJSON()
|
|
103
|
+
if (!Array.isArray(values) || !values.every((value) => typeof value === 'string')) {
|
|
104
|
+
throw new UiError(
|
|
105
|
+
'UI_PROJECT_UNSUPPORTED',
|
|
106
|
+
'pnpm-workspace.yaml must declare a packages sequence.',
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
if (!values.includes(workspace)) packages.add(workspace)
|
|
110
|
+
await writeFile(target, String(document), 'utf8')
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function hasDomainRegistryWorkspace(project: UiProject): Promise<boolean> {
|
|
114
|
+
if (!project.isAstraleDomain || !(await exists(domainRegistryPackagePath(project)))) return false
|
|
115
|
+
const registryManifest = JSON.parse(
|
|
116
|
+
await readFile(domainRegistryPackagePath(project), 'utf8'),
|
|
117
|
+
) as Record<string, unknown>
|
|
118
|
+
if (
|
|
119
|
+
registryManifest.private !== true ||
|
|
120
|
+
typeof registryManifest.name !== 'string' ||
|
|
121
|
+
registryManifest.name === UI_PACKAGE ||
|
|
122
|
+
registryManifest.name === project.packageJson.name
|
|
123
|
+
) {
|
|
124
|
+
return false
|
|
125
|
+
}
|
|
126
|
+
if (project.manager === 'pnpm') {
|
|
127
|
+
const source = await readOptional(pnpmWorkspacePath(project))
|
|
128
|
+
if (source === undefined) return false
|
|
129
|
+
const document = parseDocument(source)
|
|
130
|
+
if (document.errors.length > 0) return false
|
|
131
|
+
const packages = document.get('packages', true)
|
|
132
|
+
const values = isSeq(packages) ? packages.toJSON() : undefined
|
|
133
|
+
return Array.isArray(values) && values.includes('components')
|
|
134
|
+
}
|
|
135
|
+
const configured = project.packageJson.workspaces
|
|
136
|
+
const workspaces = Array.isArray(configured)
|
|
137
|
+
? configured
|
|
138
|
+
: configured && typeof configured === 'object'
|
|
139
|
+
? (configured as { packages?: unknown }).packages
|
|
140
|
+
: undefined
|
|
141
|
+
return Array.isArray(workspaces) && workspaces.includes('components')
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function writeDomainRegistryWorkspace(
|
|
145
|
+
project: UiProject,
|
|
146
|
+
manifest: Record<string, unknown>,
|
|
147
|
+
): Promise<void> {
|
|
148
|
+
if (!project.isAstraleDomain) return
|
|
149
|
+
if (project.manager === 'pnpm') await appendPnpmWorkspace(project, 'components')
|
|
150
|
+
else appendWorkspace(manifest, 'components')
|
|
151
|
+
const target = domainRegistryPackagePath(project)
|
|
152
|
+
const existing = await readOptional(target)
|
|
153
|
+
const registryManifest = existing
|
|
154
|
+
? (JSON.parse(existing) as Record<string, unknown>)
|
|
155
|
+
: { name: domainRegistryPackageName(manifest) }
|
|
156
|
+
if (registryManifest.private === false) {
|
|
157
|
+
throw new UiError(
|
|
158
|
+
'UI_PROJECT_UNSUPPORTED',
|
|
159
|
+
'The Astrale registry source workspace must remain private.',
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
if (
|
|
163
|
+
registryManifest.name === UI_PACKAGE ||
|
|
164
|
+
registryManifest.name === manifest.name ||
|
|
165
|
+
(registryManifest.name !== undefined && typeof registryManifest.name !== 'string')
|
|
166
|
+
) {
|
|
167
|
+
throw new UiError(
|
|
168
|
+
'UI_PROJECT_UNSUPPORTED',
|
|
169
|
+
'The Astrale registry source workspace must have a distinct package name.',
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
await writeJson(target, {
|
|
173
|
+
...registryManifest,
|
|
174
|
+
name: registryManifest.name ?? domainRegistryPackageName(manifest),
|
|
175
|
+
private: true,
|
|
176
|
+
type: registryManifest.type ?? 'module',
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
|
|
45
180
|
export type InitUiOptions = {
|
|
46
181
|
path?: string
|
|
47
182
|
preset?: UiPreset
|
|
@@ -73,7 +208,8 @@ export async function initUi(
|
|
|
73
208
|
(!options.preset || options.preset === lock.preset) &&
|
|
74
209
|
css.includes(UI_PACKAGE + '/theme.css') &&
|
|
75
210
|
css.includes(UI_PACKAGE + '/presets/' + lock.preset + '.css') &&
|
|
76
|
-
components?.style === 'base-nova'
|
|
211
|
+
components?.style === 'base-nova' &&
|
|
212
|
+
(!project.isAstraleDomain || (await hasDomainRegistryWorkspace(project)))
|
|
77
213
|
if (!desired) {
|
|
78
214
|
throw new UiError(
|
|
79
215
|
'UI_ITEM_CONFLICT',
|
|
@@ -96,6 +232,8 @@ export async function initUi(
|
|
|
96
232
|
cssRelative,
|
|
97
233
|
'components.json',
|
|
98
234
|
'astrale-ui.lock.json',
|
|
235
|
+
...(project.isAstraleDomain ? ['components/package.json'] : []),
|
|
236
|
+
...(project.isAstraleDomain && project.manager === 'pnpm' ? ['pnpm-workspace.yaml'] : []),
|
|
99
237
|
...(project.lockPath ? [projectRelative(project, project.lockPath)] : []),
|
|
100
238
|
],
|
|
101
239
|
tooling: {
|
|
@@ -107,11 +245,20 @@ export async function initUi(
|
|
|
107
245
|
}
|
|
108
246
|
if (options.dryRun) return plan
|
|
109
247
|
|
|
248
|
+
if (project.isAstraleDomain) {
|
|
249
|
+
await assertSafePlannedTarget(project, 'components/package.json')
|
|
250
|
+
if (project.manager === 'pnpm') {
|
|
251
|
+
await assertSafePlannedTarget(project, 'pnpm-workspace.yaml')
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
110
255
|
const mutationPaths = [
|
|
111
256
|
project.packageJsonPath,
|
|
112
257
|
project.cssPath,
|
|
113
258
|
project.componentsPath,
|
|
114
259
|
project.uiLockPath,
|
|
260
|
+
...(project.isAstraleDomain ? [domainRegistryPackagePath(project)] : []),
|
|
261
|
+
...(project.isAstraleDomain && project.manager === 'pnpm' ? [pnpmWorkspacePath(project)] : []),
|
|
115
262
|
...(project.lockPath ? [project.lockPath] : []),
|
|
116
263
|
]
|
|
117
264
|
const snapshots = new Map<string, string | undefined>()
|
|
@@ -123,6 +270,7 @@ export async function initUi(
|
|
|
123
270
|
...manifestDependencies(manifest),
|
|
124
271
|
[UI_PACKAGE]: release.version,
|
|
125
272
|
}
|
|
273
|
+
await writeDomainRegistryWorkspace(project, manifest)
|
|
126
274
|
await writeJson(project.packageJsonPath, manifest)
|
|
127
275
|
|
|
128
276
|
const currentCss = (await readOptional(project.cssPath)) ?? ''
|
|
@@ -214,6 +362,28 @@ export async function initUi(
|
|
|
214
362
|
}
|
|
215
363
|
}
|
|
216
364
|
|
|
365
|
+
async function pinUiDependency(
|
|
366
|
+
project: UiProject,
|
|
367
|
+
version: string,
|
|
368
|
+
runner: UiRunner,
|
|
369
|
+
): Promise<void> {
|
|
370
|
+
const manifest = JSON.parse(await readFile(project.packageJsonPath, 'utf8')) as Record<
|
|
371
|
+
string,
|
|
372
|
+
unknown
|
|
373
|
+
>
|
|
374
|
+
if (manifestDependencies(manifest)[UI_PACKAGE] === version) return
|
|
375
|
+
manifest.dependencies = { ...manifestDependencies(manifest), [UI_PACKAGE]: version }
|
|
376
|
+
await writeJson(project.packageJsonPath, manifest)
|
|
377
|
+
const result = await runner(project.manager, ['install'], project.root)
|
|
378
|
+
if (result.code !== 0) {
|
|
379
|
+
throw new UiError(
|
|
380
|
+
'UI_DEPENDENCY_INSTALL_FAILED',
|
|
381
|
+
project.manager + ' install failed while restoring the locked UI release.',
|
|
382
|
+
result.stderr.trim() || undefined,
|
|
383
|
+
)
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
217
387
|
export async function listUi(
|
|
218
388
|
query: string | undefined,
|
|
219
389
|
options: { type?: string; limit?: number; version?: string },
|
|
@@ -323,6 +493,7 @@ export async function addUi(
|
|
|
323
493
|
throw new UiError('UI_TOOL_FAILED', 'The pinned shadcn operation omitted an item file.')
|
|
324
494
|
}
|
|
325
495
|
}
|
|
496
|
+
await pinUiDependency(project, lock.package.version, dependencies.runner ?? defaultUiRunner)
|
|
326
497
|
}
|
|
327
498
|
} catch (error) {
|
|
328
499
|
for (const [target, previous] of snapshots) {
|
package/src/ui/project.ts
CHANGED
|
@@ -12,6 +12,7 @@ export type UiProject = {
|
|
|
12
12
|
cssPath: string
|
|
13
13
|
componentsPath: string
|
|
14
14
|
uiLockPath: string
|
|
15
|
+
isAstraleDomain: boolean
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
const MANAGERS: readonly [string, PackageManager][] = [
|
|
@@ -173,6 +174,10 @@ export async function discoverUiProject(input = process.cwd()): Promise<UiProjec
|
|
|
173
174
|
cssPath: path.join(root, cssRelative),
|
|
174
175
|
componentsPath,
|
|
175
176
|
uiLockPath: path.join(root, 'astrale-ui.lock.json'),
|
|
177
|
+
isAstraleDomain:
|
|
178
|
+
(await exists(path.join(root, 'astrale.config.ts'))) &&
|
|
179
|
+
(await exists(path.join(root, 'ui/index.ts'))) &&
|
|
180
|
+
(await exists(path.join(root, 'frontend/package.json'))),
|
|
176
181
|
}
|
|
177
182
|
}
|
|
178
183
|
|