@astrale-os/cli 1.0.0-beta.22 → 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.
- package/dist/astrale.js +650 -620
- package/dist/public/connect-core.js +4 -4
- package/dist/types/admin/contract.d.ts +2 -2
- package/package.json +4 -4
- package/src/admin/contract.ts +3 -2
- package/src/commands/__tests__/read-commands.test.ts +56 -0
- package/src/commands/__tests__/view.test.ts +1 -2
- package/src/commands/introspect.ts +8 -9
- package/src/ui/.spec/architecture.md +8 -7
- package/src/ui/.spec/laws.ts +7 -0
- package/src/ui/__tests__/ui.test.ts +147 -2
- package/src/ui/operations.ts +73 -12
- package/src/ui/release.ts +9 -2
- package/studio/client/dist/assets/{index-7YHdeCxp.js → index-OeJK1TjG.js} +2 -2
- package/studio/client/dist/assets/{schema-studio-CABpTfvH.js → schema-studio-DRdfu_cQ.js} +2 -2
- package/studio/client/dist/index.html +1 -1
- package/studio/package.json +3 -3
- package/studio/server/cli-consumers.test.ts +0 -1
- package/studio/server/introspect/anatomy/views.ts +1 -2
- package/studio/server/introspect/anatomy-extras.test.ts +3 -14
- package/studio/server/introspect/canonical-schema.test.ts +1 -1
- package/studio/server/introspect/canonical-schema.ts +0 -4
- package/studio/server/introspect/diff.test.ts +10 -2
- package/studio/server/introspect/diff.ts +1 -9
- package/studio/server/introspect/runtime.test.ts +1 -1
- package/studio/server/introspect/schema-ir-json.test.ts +12 -1
- package/studio/server/introspect/schema-ir-json.ts +1 -1
- package/studio/server/views/target.test.ts +1 -2
- package/studio/shared/contracts/schema.ts +1 -3
- package/studio/tsconfig.json +2 -1
- package/viewer/dist/main.js +13 -13
|
@@ -8242,14 +8242,14 @@ function isPidAlive(pid) {
|
|
|
8242
8242
|
function sleep(milliseconds) {
|
|
8243
8243
|
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
8244
8244
|
}
|
|
8245
|
-
// node_modules/.pnpm/@astrale-os+kernel-dsl@0.2.0-beta.
|
|
8245
|
+
// node_modules/.pnpm/@astrale-os+kernel-dsl@0.2.0-beta.13/node_modules/@astrale-os/kernel-dsl/dist/value/limits.js
|
|
8246
8246
|
var defaultLimits = Object.freeze({
|
|
8247
8247
|
maxBytes: 16 * 1024 * 1024,
|
|
8248
8248
|
maxDepth: 64,
|
|
8249
8249
|
maxMembers: 1e5,
|
|
8250
8250
|
maxStringBytes: 1024 * 1024
|
|
8251
8251
|
});
|
|
8252
|
-
// node_modules/.pnpm/@astrale-os+kernel-dsl@0.2.0-beta.
|
|
8252
|
+
// node_modules/.pnpm/@astrale-os+kernel-dsl@0.2.0-beta.13/node_modules/@astrale-os/kernel-dsl/dist/value/canonical.js
|
|
8253
8253
|
var encoder2 = new TextEncoder;
|
|
8254
8254
|
// node_modules/.pnpm/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/impl/scanner.js
|
|
8255
8255
|
var CharacterCodes;
|
|
@@ -8429,7 +8429,7 @@ var ParseErrorCode;
|
|
|
8429
8429
|
ParseErrorCode2[ParseErrorCode2["InvalidCharacter"] = 16] = "InvalidCharacter";
|
|
8430
8430
|
})(ParseErrorCode || (ParseErrorCode = {}));
|
|
8431
8431
|
|
|
8432
|
-
// node_modules/.pnpm/@astrale-os+kernel-dsl@0.2.0-beta.
|
|
8432
|
+
// node_modules/.pnpm/@astrale-os+kernel-dsl@0.2.0-beta.13/node_modules/@astrale-os/kernel-dsl/dist/value/decode.js
|
|
8433
8433
|
var decoder2 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
8434
8434
|
// node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
|
|
8435
8435
|
var exports_external = {};
|
|
@@ -23659,7 +23659,7 @@ var log = {
|
|
|
23659
23659
|
dim: (msg) => console.log(source_default.dim(msg))
|
|
23660
23660
|
};
|
|
23661
23661
|
var IS_CI = !!(process.env.CI || process.env.CONTINUOUS_INTEGRATION || process.env.NO_SPINNER);
|
|
23662
|
-
// node_modules/.pnpm/@astrale-os+kernel-core@0.9.0-beta.
|
|
23662
|
+
// node_modules/.pnpm/@astrale-os+kernel-core@0.9.0-beta.16/node_modules/@astrale-os/kernel-core/dist/dns-label.js
|
|
23663
23663
|
var DNS_LABEL_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
23664
23664
|
function isDnsLabel(value) {
|
|
23665
23665
|
return DNS_LABEL_RE.test(value);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Input } from '@astrale-os/sdk/client';
|
|
2
|
-
import type { ClientSession } from '@astrale-os/sdk/client/session';
|
|
2
|
+
import type { ClientSession, SessionRequestOptions } from '@astrale-os/sdk/client/session';
|
|
3
3
|
import type { ClassRef } from '@astrale-os/sdk/schema';
|
|
4
4
|
import { Path } from '@astrale-os/sdk/graph/path';
|
|
5
5
|
export declare const AdminContract: Readonly<{
|
|
@@ -23,4 +23,4 @@ export declare const AdminContract: Readonly<{
|
|
|
23
23
|
}>;
|
|
24
24
|
}>;
|
|
25
25
|
/** Invoke one stable Admin instance Method without schema discovery or reflection. */
|
|
26
|
-
export declare function callAdminMethod(session: ClientSession, receiver: Path, method: string, input: Input): Promise<unknown>;
|
|
26
|
+
export declare function callAdminMethod(session: ClientSession, receiver: Path, method: string, input: Input, options?: SessionRequestOptions): Promise<unknown>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astrale-os/cli",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.23",
|
|
4
4
|
"description": "Astrale CLI — connect to existing Astrale kernels",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"astrale",
|
|
@@ -62,8 +62,8 @@
|
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"@astrale-os/ox": ">=0.1.3 <1.0.0",
|
|
65
|
-
"@astrale-os/sdk": "0.5.0-beta.
|
|
66
|
-
"@astrale-os/shell": "0.4.2-beta.
|
|
65
|
+
"@astrale-os/sdk": "0.5.0-beta.51",
|
|
66
|
+
"@astrale-os/shell": "0.4.2-beta.6",
|
|
67
67
|
"@astrale/commitlint-config": "npm:@jsr/astrale__commitlint-config@~2.0.1",
|
|
68
68
|
"@commitlint/cli": "21.2.2",
|
|
69
69
|
"@commitlint/config-conventional": "21.2.2",
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
"scripts": {
|
|
99
99
|
"build": "bun scripts/build.ts",
|
|
100
100
|
"package:check": "pnpm run typecheck && pnpm run test && pnpm run build && node scripts/verify-public-exports.mjs && node scripts/verify-public-dependency-boundary.mjs",
|
|
101
|
-
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.tests.json && tsc --noEmit -p viewer && pnpm --dir studio run typecheck",
|
|
101
|
+
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.tests.json && tsc --noEmit -p tsconfig.scripts.json && tsc --noEmit -p viewer && pnpm --dir studio run typecheck",
|
|
102
102
|
"lint": "oxlint .",
|
|
103
103
|
"lint:fix": "oxlint --fix .",
|
|
104
104
|
"format": "pnpm exec oxfmt --write .",
|
package/src/admin/contract.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Input } from '@astrale-os/sdk/client'
|
|
2
|
-
import type { ClientSession } from '@astrale-os/sdk/client/session'
|
|
2
|
+
import type { ClientSession, SessionRequestOptions } from '@astrale-os/sdk/client/session'
|
|
3
3
|
import type { ClassRef } from '@astrale-os/sdk/schema'
|
|
4
4
|
|
|
5
5
|
import { call } from '@astrale-os/sdk/client'
|
|
@@ -41,6 +41,7 @@ export function callAdminMethod(
|
|
|
41
41
|
receiver: Path,
|
|
42
42
|
method: string,
|
|
43
43
|
input: Input,
|
|
44
|
+
options?: SessionRequestOptions,
|
|
44
45
|
): Promise<unknown> {
|
|
45
|
-
return session.call(call(Path.instanceMethod(receiver, method), input))
|
|
46
|
+
return session.call(call(Path.instanceMethod(receiver, method), input), options)
|
|
46
47
|
}
|
|
@@ -6,6 +6,12 @@ type GraphContext = {
|
|
|
6
6
|
getOrThrow: typeof getOrThrowMock
|
|
7
7
|
mutate: typeof mutateMock
|
|
8
8
|
}
|
|
9
|
+
session: {
|
|
10
|
+
schema: {
|
|
11
|
+
inspect: typeof inspectMock
|
|
12
|
+
bundle: typeof bundleMock
|
|
13
|
+
}
|
|
14
|
+
}
|
|
9
15
|
}
|
|
10
16
|
|
|
11
17
|
type RunOpts = {
|
|
@@ -36,6 +42,8 @@ let mutations: unknown[] = []
|
|
|
36
42
|
let queryResult: unknown
|
|
37
43
|
let getResult: unknown
|
|
38
44
|
let mutationResult: unknown
|
|
45
|
+
let inspectResult: unknown
|
|
46
|
+
let bundleResult: unknown
|
|
39
47
|
let selfFailure: unknown
|
|
40
48
|
let selfContexts: unknown[] = []
|
|
41
49
|
|
|
@@ -51,6 +59,8 @@ const mutateMock = mock(async (mutation: unknown) => {
|
|
|
51
59
|
mutations.push(mutation)
|
|
52
60
|
return mutationResult
|
|
53
61
|
})
|
|
62
|
+
const inspectMock = mock(async (_origin: string) => inspectResult)
|
|
63
|
+
const bundleMock = mock(async (_origin: string) => bundleResult)
|
|
54
64
|
const expandSelfInPathMock = mock(async (path: string, context: unknown) => {
|
|
55
65
|
selfContexts.push(context)
|
|
56
66
|
if (selfFailure !== undefined) throw selfFailure
|
|
@@ -65,6 +75,7 @@ const expandSelfInPathMock = mock(async (path: string, context: unknown) => {
|
|
|
65
75
|
const withSelfHintMock = mock(async (action: () => Promise<unknown>) => action())
|
|
66
76
|
const commandContext = {
|
|
67
77
|
graph: { query: queryMock, getOrThrow: getOrThrowMock, mutate: mutateMock },
|
|
78
|
+
session: { schema: { inspect: inspectMock, bundle: bundleMock } },
|
|
68
79
|
}
|
|
69
80
|
const runKernelCommandMock = mock(async (run: RunOpts) => {
|
|
70
81
|
const result = await run.fn(commandContext)
|
|
@@ -93,11 +104,22 @@ beforeEach(() => {
|
|
|
93
104
|
}
|
|
94
105
|
getResult = undefined
|
|
95
106
|
mutationResult = { createdNodes: {} }
|
|
107
|
+
inspectResult = {
|
|
108
|
+
origin: 'notes.example.dev',
|
|
109
|
+
revision: 'notes-v1',
|
|
110
|
+
generation: 'sha256:generation',
|
|
111
|
+
}
|
|
112
|
+
bundleResult = {
|
|
113
|
+
domain: inspectResult,
|
|
114
|
+
bundle: { origin: 'notes.example.dev', revision: 'notes-v1' },
|
|
115
|
+
}
|
|
96
116
|
selfFailure = undefined
|
|
97
117
|
selfContexts = []
|
|
98
118
|
queryMock.mockClear()
|
|
99
119
|
getOrThrowMock.mockClear()
|
|
100
120
|
mutateMock.mockClear()
|
|
121
|
+
inspectMock.mockClear()
|
|
122
|
+
bundleMock.mockClear()
|
|
101
123
|
expandSelfInPathMock.mockClear()
|
|
102
124
|
withSelfHintMock.mockClear()
|
|
103
125
|
runKernelCommandMock.mockClear()
|
|
@@ -280,6 +302,40 @@ describe('get command', () => {
|
|
|
280
302
|
})
|
|
281
303
|
})
|
|
282
304
|
|
|
305
|
+
describe('introspect command', () => {
|
|
306
|
+
test('uses the split SDK Schema API without recreating the retired introspection request', async () => {
|
|
307
|
+
const { introspectCommand } = await import('../introspect')
|
|
308
|
+
|
|
309
|
+
await introspectCommand(
|
|
310
|
+
'notes.example.dev',
|
|
311
|
+
{ json: true },
|
|
312
|
+
{
|
|
313
|
+
runKernelCommand: runKernelCommandMock as never,
|
|
314
|
+
},
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
expect(inspectMock).toHaveBeenCalledWith('notes.example.dev')
|
|
318
|
+
expect(bundleMock).not.toHaveBeenCalled()
|
|
319
|
+
expect(JSON.parse(stdout)).toEqual(inspectResult)
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
test('requests the immutable bundle only when explicitly selected', async () => {
|
|
323
|
+
const { introspectCommand } = await import('../introspect')
|
|
324
|
+
|
|
325
|
+
await introspectCommand(
|
|
326
|
+
'notes.example.dev',
|
|
327
|
+
{ json: true, bundle: true },
|
|
328
|
+
{
|
|
329
|
+
runKernelCommand: runKernelCommandMock as never,
|
|
330
|
+
},
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
expect(bundleMock).toHaveBeenCalledWith('notes.example.dev')
|
|
334
|
+
expect(inspectMock).not.toHaveBeenCalled()
|
|
335
|
+
expect(JSON.parse(stdout)).toEqual(bundleResult)
|
|
336
|
+
})
|
|
337
|
+
})
|
|
338
|
+
|
|
283
339
|
describe('mutate command', () => {
|
|
284
340
|
/** @evidence TEST-CLI-MUTATE-DISPATCHES-CANONICAL-V3 */
|
|
285
341
|
test('admits authoring input and dispatches one canonical Mutation V3 document', async () => {
|
|
@@ -52,7 +52,6 @@ const resolved = [
|
|
|
52
52
|
kind: 'definition' as const,
|
|
53
53
|
definitions: [{ origin: 'ai-gateway.astrale.ai', kind: 'class' as const, name: 'Model' }],
|
|
54
54
|
},
|
|
55
|
-
auth: 'required' as const,
|
|
56
55
|
},
|
|
57
56
|
}),
|
|
58
57
|
route({
|
|
@@ -62,7 +61,7 @@ const resolved = [
|
|
|
62
61
|
issuer: 'https://ai-gateway.astrale.ai',
|
|
63
62
|
etag: `sha256:${'c'.repeat(64)}`,
|
|
64
63
|
revision: `sha256:${'d'.repeat(64)}`,
|
|
65
|
-
declaration: { target: { kind: 'domain' as const }
|
|
64
|
+
declaration: { target: { kind: 'domain' as const } },
|
|
66
65
|
}),
|
|
67
66
|
]
|
|
68
67
|
|
|
@@ -38,14 +38,13 @@ export async function introspectCommand(
|
|
|
38
38
|
opts,
|
|
39
39
|
label: `Introspect ${origin}`,
|
|
40
40
|
fn: async ({ session }) => {
|
|
41
|
-
const includeBundle = wantsCallable || opts.bundle === true
|
|
42
|
-
const result = await readInstalledDomain(session.schema, origin, includeBundle)
|
|
43
41
|
if (wantsCallable) {
|
|
44
|
-
const
|
|
42
|
+
const result = await readInstalledDomain(session.schema, origin, true)
|
|
43
|
+
const described = describeCallableFromBundle(path, result.bundle)
|
|
45
44
|
if (described === undefined) throw missingCallableDescription(path.raw)
|
|
46
45
|
return described
|
|
47
46
|
}
|
|
48
|
-
return
|
|
47
|
+
return readInstalledDomain(session.schema, origin, opts.bundle === true)
|
|
49
48
|
},
|
|
50
49
|
format: (value, format) => output(value, format),
|
|
51
50
|
})
|
|
@@ -110,11 +109,11 @@ export default {
|
|
|
110
109
|
description: 'Read installed Domain schema from the Kernel Schema syscall',
|
|
111
110
|
afterHelpText: `
|
|
112
111
|
Behavior:
|
|
113
|
-
Calls the public
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
schema bundle. A method or Function Path projects that
|
|
117
|
-
input/output from the installed bundle.
|
|
112
|
+
Calls the public SDK Schema API for one installed Domain. A bare origin
|
|
113
|
+
(kernel.astrale.ai or /:kernel.astrale.ai) prints its exact revision,
|
|
114
|
+
generation, publication, readiness, capabilities, and bindings. --bundle
|
|
115
|
+
includes the schema bundle. A method or Function Path projects that
|
|
116
|
+
callable's input/output from the installed bundle.
|
|
118
117
|
|
|
119
118
|
Examples:
|
|
120
119
|
$ astrale introspect kernel.astrale.ai
|
|
@@ -4,10 +4,11 @@ This private CLI owner manages an existing local React project. It does not conn
|
|
|
4
4
|
does not add React, Base UI, Tailwind, shadcn, or the UI runtime to the installed CLI dependency
|
|
5
5
|
graph.
|
|
6
6
|
|
|
7
|
-
One registry operation resolves one immutable UI commit.
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
One registry operation resolves one immutable UI commit. During the V1 prerelease, operations
|
|
8
|
+
without an explicit version resolve the npm `beta` dist-tag; the legacy `latest` channel is not a V1
|
|
9
|
+
source. Package compatibility and registry reads use that commit. Patterns and blocks invoke the
|
|
10
|
+
release-qualified shadcn CLI on demand through the detected package manager. Released themes copy
|
|
11
|
+
the exact admitted embedded CSS directly, while a local playground export is admitted and copied
|
|
12
|
+
without registry or shadcn. Both theme paths install consumer-owned source and activate it through
|
|
13
|
+
one project-relative CSS import, and complete application writes before the lock advances. Dry-run,
|
|
14
|
+
list, and doctor do not claim a file mutation.
|
package/src/ui/.spec/laws.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { defineLaw } from '@astrale-os/spec/authoring'
|
|
2
2
|
|
|
3
|
+
export const CLI_UI_BETA_DEFAULT = defineLaw({
|
|
4
|
+
id: 'CLI-UI-BETA-DEFAULT',
|
|
5
|
+
statement:
|
|
6
|
+
'Until the UI V1 channel is promoted, an operation without an explicit version resolves the npm beta dist-tag and never the legacy latest dist-tag.',
|
|
7
|
+
tests: [{ file: '../__tests__/ui.test.ts', id: 'TEST-CLI-UI-BETA-DEFAULT' }],
|
|
8
|
+
})
|
|
9
|
+
|
|
3
10
|
export const CLI_UI_ONE_SNAPSHOT = defineLaw({
|
|
4
11
|
id: 'CLI-UI-ONE-SNAPSHOT',
|
|
5
12
|
statement:
|
|
@@ -28,6 +28,30 @@ const registry: UiRegistry = {
|
|
|
28
28
|
},
|
|
29
29
|
],
|
|
30
30
|
}
|
|
31
|
+
const componentRegistry: UiRegistry = {
|
|
32
|
+
name: 'astrale-ui',
|
|
33
|
+
items: [
|
|
34
|
+
{
|
|
35
|
+
name: 'component-sidebar',
|
|
36
|
+
type: 'registry:component',
|
|
37
|
+
title: 'Component · sidebar',
|
|
38
|
+
dependencies: ['@astrale-os/ui@^0.3.0-beta.0', 'lucide-react@1.32.0'],
|
|
39
|
+
files: [
|
|
40
|
+
{
|
|
41
|
+
path: 'sidebar.tsx',
|
|
42
|
+
type: 'registry:component',
|
|
43
|
+
target: 'components/astrale/component/sidebar/sidebar.tsx',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
path: 'use-mobile.ts',
|
|
47
|
+
type: 'registry:hook',
|
|
48
|
+
target: 'components/astrale/component/sidebar/use-mobile.ts',
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
meta: { canonicalAddress: 'component/sidebar', ownership: 'consumer-source' },
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
}
|
|
31
55
|
const themeCss = `/* Generated from observatory.astrale-theme.json. Consumer-owned after installation. */
|
|
32
56
|
:root,
|
|
33
57
|
[data-ui-theme='observatory'] {
|
|
@@ -257,15 +281,17 @@ function lock(): UiLock {
|
|
|
257
281
|
}
|
|
258
282
|
|
|
259
283
|
describe('UI release and runner contracts', () => {
|
|
284
|
+
/** @evidence TEST-CLI-UI-BETA-DEFAULT */
|
|
260
285
|
test('resolves the default release from the public beta channel', async () => {
|
|
261
286
|
const seen: string[] = []
|
|
287
|
+
const fallback = mockFetch()
|
|
262
288
|
const fetcher = (async (input: string | URL | Request, init?: RequestInit) => {
|
|
263
289
|
const url = String(input)
|
|
290
|
+
seen.push(url)
|
|
264
291
|
if (url.endsWith('/@astrale-os/ui/beta')) {
|
|
265
|
-
seen.push(url)
|
|
266
292
|
return Response.json({ version: '0.3.0-beta.1' })
|
|
267
293
|
}
|
|
268
|
-
return
|
|
294
|
+
return fallback(input, init)
|
|
269
295
|
}) as typeof fetch
|
|
270
296
|
|
|
271
297
|
const release = await resolveUiRelease(undefined, fetcher)
|
|
@@ -322,6 +348,36 @@ describe('UI release and runner contracts', () => {
|
|
|
322
348
|
])
|
|
323
349
|
})
|
|
324
350
|
|
|
351
|
+
test('admits an exact consumer-owned component with item-local dependencies and files', async () => {
|
|
352
|
+
const release = await resolveUiRelease('0.3.0-beta.0', mockFetch([], componentRegistry))
|
|
353
|
+
expect(release.registry.items).toEqual([
|
|
354
|
+
expect.objectContaining({
|
|
355
|
+
name: 'component-sidebar',
|
|
356
|
+
type: 'registry:component',
|
|
357
|
+
dependencies: ['@astrale-os/ui@^0.3.0-beta.0', 'lucide-react@1.32.0'],
|
|
358
|
+
meta: expect.objectContaining({ canonicalAddress: 'component/sidebar' }),
|
|
359
|
+
files: [
|
|
360
|
+
expect.objectContaining({
|
|
361
|
+
type: 'registry:component',
|
|
362
|
+
target: 'components/astrale/component/sidebar/sidebar.tsx',
|
|
363
|
+
}),
|
|
364
|
+
expect.objectContaining({
|
|
365
|
+
type: 'registry:hook',
|
|
366
|
+
target: 'components/astrale/component/sidebar/use-mobile.ts',
|
|
367
|
+
}),
|
|
368
|
+
],
|
|
369
|
+
}),
|
|
370
|
+
])
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
test('rejects a component item that targets another registry ownership category', async () => {
|
|
374
|
+
const invalid = structuredClone(componentRegistry)
|
|
375
|
+
invalid.items[0]!.files[0]!.target = 'components/astrale/pattern/sidebar/sidebar.tsx'
|
|
376
|
+
await expect(resolveUiRelease('0.3.0-beta.0', mockFetch([], invalid))).rejects.toMatchObject({
|
|
377
|
+
code: 'UI_REGISTRY_UNAVAILABLE',
|
|
378
|
+
})
|
|
379
|
+
})
|
|
380
|
+
|
|
325
381
|
test('lists release themes through the public type filter', async () => {
|
|
326
382
|
const themes = await listUi(
|
|
327
383
|
undefined,
|
|
@@ -795,8 +851,60 @@ describe('UI initialization transaction', () => {
|
|
|
795
851
|
)
|
|
796
852
|
})
|
|
797
853
|
|
|
854
|
+
test('preserves an existing development-only UI dependency during initialization', async () => {
|
|
855
|
+
const root = await fixture()
|
|
856
|
+
const manifestPath = path.join(root, 'package.json')
|
|
857
|
+
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'))
|
|
858
|
+
manifest.devDependencies = { '@astrale-os/ui': '0.3.0-beta.0' }
|
|
859
|
+
await writeFile(manifestPath, JSON.stringify(manifest))
|
|
860
|
+
|
|
861
|
+
await initUi({ path: root, version: '0.3.0-beta.0', install: false }, { fetcher: mockFetch() })
|
|
862
|
+
|
|
863
|
+
const written = JSON.parse(await readFile(manifestPath, 'utf8'))
|
|
864
|
+
expect(written.dependencies['@astrale-os/ui']).toBeUndefined()
|
|
865
|
+
expect(written.devDependencies['@astrale-os/ui']).toBe('0.3.0-beta.0')
|
|
866
|
+
})
|
|
867
|
+
|
|
868
|
+
test('doctor accepts one exact development dependency and rejects drift or duplication', async () => {
|
|
869
|
+
const root = await lockedFixture()
|
|
870
|
+
const manifestPath = path.join(root, 'package.json')
|
|
871
|
+
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'))
|
|
872
|
+
delete manifest.dependencies['@astrale-os/ui']
|
|
873
|
+
manifest.devDependencies = { '@astrale-os/ui': '0.3.0-beta.0' }
|
|
874
|
+
await writeFile(manifestPath, JSON.stringify(manifest))
|
|
875
|
+
await writeFile(
|
|
876
|
+
path.join(root, 'components.json'),
|
|
877
|
+
JSON.stringify({ style: 'base-nova', tailwind: { css: 'src/index.css' } }),
|
|
878
|
+
)
|
|
879
|
+
await writeFile(
|
|
880
|
+
path.join(root, 'src/index.css'),
|
|
881
|
+
"@import '@astrale-os/ui/theme.css';\n@import '@astrale-os/ui/presets/astrale.css';\n",
|
|
882
|
+
)
|
|
883
|
+
|
|
884
|
+
expect((await doctorUi(root)).healthy).toBe(true)
|
|
885
|
+
|
|
886
|
+
manifest.devDependencies['@astrale-os/ui'] = '0.3.0-beta.1'
|
|
887
|
+
await writeFile(manifestPath, JSON.stringify(manifest))
|
|
888
|
+
expect((await doctorUi(root)).checks.find(({ check }) => check === 'package')).toMatchObject({
|
|
889
|
+
ok: false,
|
|
890
|
+
detail: 'devDependencies:0.3.0-beta.1',
|
|
891
|
+
})
|
|
892
|
+
|
|
893
|
+
manifest.devDependencies['@astrale-os/ui'] = '0.3.0-beta.0'
|
|
894
|
+
manifest.dependencies['@astrale-os/ui'] = '0.3.0-beta.0'
|
|
895
|
+
await writeFile(manifestPath, JSON.stringify(manifest))
|
|
896
|
+
expect((await doctorUi(root)).checks.find(({ check }) => check === 'package')).toMatchObject({
|
|
897
|
+
ok: false,
|
|
898
|
+
detail: 'declared in dependencies and devDependencies',
|
|
899
|
+
})
|
|
900
|
+
})
|
|
901
|
+
|
|
798
902
|
test('repeated exact init performs no release fetch while requested drift rejects', async () => {
|
|
799
903
|
const root = await fixture()
|
|
904
|
+
const manifestPath = path.join(root, 'package.json')
|
|
905
|
+
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'))
|
|
906
|
+
manifest.dependencies['@astrale-os/ui'] = '0.3.0-beta.0'
|
|
907
|
+
await writeFile(manifestPath, JSON.stringify(manifest))
|
|
800
908
|
await writeFile(path.join(root, 'astrale-ui.lock.json'), JSON.stringify(lock()))
|
|
801
909
|
await writeFile(
|
|
802
910
|
path.join(root, 'components.json'),
|
|
@@ -821,6 +929,11 @@ describe('UI initialization transaction', () => {
|
|
|
821
929
|
await expect(initUi({ path: root, preset: 'compact' })).rejects.toMatchObject({
|
|
822
930
|
code: 'UI_ITEM_CONFLICT',
|
|
823
931
|
})
|
|
932
|
+
manifest.dependencies['@astrale-os/ui'] = '0.3.0-beta.1'
|
|
933
|
+
await writeFile(manifestPath, JSON.stringify(manifest))
|
|
934
|
+
await expect(initUi({ path: root, preset: 'astrale' })).rejects.toMatchObject({
|
|
935
|
+
code: 'UI_ITEM_CONFLICT',
|
|
936
|
+
})
|
|
824
937
|
})
|
|
825
938
|
})
|
|
826
939
|
|
|
@@ -1132,6 +1245,38 @@ describe('UI source operations', () => {
|
|
|
1132
1245
|
).toBe('0.3.0-beta.0')
|
|
1133
1246
|
})
|
|
1134
1247
|
|
|
1248
|
+
test('restores a development-only UI dependency without retaining shadcn duplication', async () => {
|
|
1249
|
+
const root = await lockedFixture()
|
|
1250
|
+
const manifestPath = path.join(root, 'package.json')
|
|
1251
|
+
const initial = JSON.parse(await readFile(manifestPath, 'utf8'))
|
|
1252
|
+
delete initial.dependencies['@astrale-os/ui']
|
|
1253
|
+
initial.devDependencies = { '@astrale-os/ui': '0.3.0-beta.0' }
|
|
1254
|
+
await writeFile(manifestPath, JSON.stringify(initial))
|
|
1255
|
+
const installed = path.join(root, 'components/astrale/pattern/chart/line-basic.tsx')
|
|
1256
|
+
|
|
1257
|
+
await addUi(
|
|
1258
|
+
['pattern/chart/line/basic'],
|
|
1259
|
+
{ project: root, yes: true },
|
|
1260
|
+
{
|
|
1261
|
+
fetcher: mockFetch(),
|
|
1262
|
+
runner: async (_file, args) => {
|
|
1263
|
+
if (args[0] === 'dlx') {
|
|
1264
|
+
await mkdir(path.dirname(installed), { recursive: true })
|
|
1265
|
+
await writeFile(installed, 'export const Chart = true\n')
|
|
1266
|
+
const changed = JSON.parse(await readFile(manifestPath, 'utf8'))
|
|
1267
|
+
changed.dependencies['@astrale-os/ui'] = '^0.3.0-beta.0'
|
|
1268
|
+
await writeFile(manifestPath, JSON.stringify(changed))
|
|
1269
|
+
}
|
|
1270
|
+
return { code: 0, stdout: '', stderr: '' }
|
|
1271
|
+
},
|
|
1272
|
+
},
|
|
1273
|
+
)
|
|
1274
|
+
|
|
1275
|
+
const written = JSON.parse(await readFile(manifestPath, 'utf8'))
|
|
1276
|
+
expect(written.dependencies['@astrale-os/ui']).toBeUndefined()
|
|
1277
|
+
expect(written.devDependencies['@astrale-os/ui']).toBe('0.3.0-beta.0')
|
|
1278
|
+
})
|
|
1279
|
+
|
|
1135
1280
|
test('rolls back item and package state when restoring the locked dependency fails', async () => {
|
|
1136
1281
|
const root = await lockedFixture()
|
|
1137
1282
|
const manifestPath = path.join(root, 'package.json')
|
package/src/ui/operations.ts
CHANGED
|
@@ -134,8 +134,45 @@ function activateTheme(current: string, statement: string): string {
|
|
|
134
134
|
.replace(/^\n/u, '')
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
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
|
-
|
|
471
|
-
|
|
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) {
|
|
@@ -663,6 +705,7 @@ export async function addUi(
|
|
|
663
705
|
return addLocalTheme(localThemes[0]!, project, options)
|
|
664
706
|
}
|
|
665
707
|
const { lock, release } = await lockedRelease(project, dependencies.fetcher)
|
|
708
|
+
const dependencySection = uiDependencySection(project.packageJson)
|
|
666
709
|
const items = addresses.map((address) => findItem(release, address))
|
|
667
710
|
const themes = items.filter((item) => item.type === 'registry:theme')
|
|
668
711
|
if (themes.length > 1) {
|
|
@@ -731,7 +774,12 @@ export async function addUi(
|
|
|
731
774
|
throw new UiError('UI_TOOL_FAILED', 'The pinned shadcn operation omitted an item file.')
|
|
732
775
|
}
|
|
733
776
|
}
|
|
734
|
-
await pinUiDependency(
|
|
777
|
+
await pinUiDependency(
|
|
778
|
+
project,
|
|
779
|
+
lock.package.version,
|
|
780
|
+
dependencySection,
|
|
781
|
+
dependencies.runner ?? defaultUiRunner,
|
|
782
|
+
)
|
|
735
783
|
const theme = themes[0]
|
|
736
784
|
const themeTarget = theme?.files[0]?.target
|
|
737
785
|
if (themeTarget) {
|
|
@@ -821,11 +869,24 @@ export async function doctorUi(
|
|
|
821
869
|
detail: error instanceof Error ? error.message : String(error),
|
|
822
870
|
})
|
|
823
871
|
}
|
|
824
|
-
const
|
|
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
|
|
825
880
|
checks.push({
|
|
826
881
|
check: 'package',
|
|
827
|
-
ok:
|
|
828
|
-
detail:
|
|
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,
|
|
829
890
|
})
|
|
830
891
|
const css = (await readOptional(project.cssPath)) ?? ''
|
|
831
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
|
)
|