@astrale-os/cli 0.8.1-alpha.6 → 1.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +18 -5
  2. package/dist/astrale.js +2661 -2714
  3. package/dist/public/connect-core.js +2019 -3050
  4. package/dist/public/keys/index.js +1851 -2885
  5. package/dist/public/paths/index.js +1830 -2872
  6. package/dist/types/connection/auth.d.ts +3 -0
  7. package/dist/types/lib/instance.d.ts +10 -0
  8. package/package.json +9 -9
  9. package/src/commands/__tests__/domain-install-operation.test.ts +121 -0
  10. package/src/commands/__tests__/domain-install-owned.test.ts +66 -0
  11. package/src/commands/__tests__/domain-uninstall.test.ts +53 -0
  12. package/src/commands/__tests__/install-direct.test.ts +3 -2
  13. package/src/commands/__tests__/install-identity-override.test.ts +14 -3
  14. package/src/commands/__tests__/instance-bookmark.test.ts +66 -1
  15. package/src/commands/__tests__/instance-list-rows.test.ts +1 -0
  16. package/src/commands/__tests__/instance-use.test.ts +67 -0
  17. package/src/commands/__tests__/view-build.test.ts +58 -0
  18. package/src/commands/domain/install.ts +84 -20
  19. package/src/commands/domain/uninstall.ts +128 -0
  20. package/src/commands/instance/active.ts +13 -1
  21. package/src/commands/instance/bookmark.ts +26 -3
  22. package/src/commands/instance/list.ts +18 -4
  23. package/src/commands/instance/use.ts +54 -7
  24. package/src/commands/view.ts +28 -16
  25. package/src/connection/.spec/architecture.md +5 -0
  26. package/src/connection/.spec/laws/connection.ts +20 -0
  27. package/src/connection/.spec/layout.ts +1 -0
  28. package/src/connection/__tests__/auth.test.ts +27 -1
  29. package/src/connection/__tests__/ca-fetch.test.ts +8 -1
  30. package/src/connection/__tests__/errors.test.ts +483 -33
  31. package/src/connection/__tests__/exchange.test.ts +46 -5
  32. package/src/connection/__tests__/reasons.test.ts +78 -0
  33. package/src/connection/auth.ts +11 -9
  34. package/src/connection/command.ts +9 -1
  35. package/src/connection/errors.ts +149 -159
  36. package/src/connection/exchange.ts +14 -2
  37. package/src/connection/index.ts +1 -1
  38. package/src/connection/reasons.ts +193 -0
  39. package/src/lib/__tests__/instance.test.ts +51 -1
  40. package/src/lib/__tests__/view-assets.test.ts +33 -1
  41. package/src/lib/__tests__/view-server.test.ts +68 -0
  42. package/src/lib/ca-fetch.ts +9 -3
  43. package/src/lib/instance.ts +31 -0
  44. package/src/lib/view/assets.ts +16 -2
  45. package/src/program/__tests__/program.test.ts +2 -1
  46. package/src/program/build.ts +2 -1
  47. package/studio/client/dist/assets/{elk-api-D0cBetPW.js → elk-api-D2xgMJvi.js} +1 -1
  48. package/studio/client/dist/assets/{index-BQnJ5sgd.css → index-BMdnsIJA.css} +1 -1
  49. package/studio/client/dist/assets/index-D-vRV8w7.js +8 -0
  50. package/studio/client/dist/assets/index-LGSWRrk8.js +81 -0
  51. package/studio/client/dist/index.html +2 -2
  52. package/studio/package.json +8 -9
  53. package/studio/server/agent/prompts/anchors.test.ts +74 -0
  54. package/studio/server/agent/prompts/anchors.ts +85 -15
  55. package/studio/server/agent/prompts/system.test.ts +12 -0
  56. package/studio/server/agent/prompts/system.ts +6 -6
  57. package/studio/server/api.ts +1 -5
  58. package/studio/server/cache.ts +5 -2
  59. package/studio/server/domain.test.ts +67 -0
  60. package/studio/server/domain.ts +29 -6
  61. package/studio/server/index.ts +1 -3
  62. package/studio/server/introspect/anatomy-extras.test.ts +104 -1
  63. package/studio/server/introspect/anatomy-extras.ts +340 -8
  64. package/studio/server/introspect/anatomy.test.ts +33 -0
  65. package/studio/server/introspect/anatomy.ts +22 -7
  66. package/studio/server/introspect/bundle.ts +7 -1
  67. package/studio/server/introspect/canonical-schema.test.ts +395 -0
  68. package/studio/server/introspect/canonical-schema.ts +751 -0
  69. package/studio/server/introspect/core-extractor.ts +30 -10
  70. package/studio/server/introspect/core.ts +4 -2
  71. package/studio/server/introspect/diff.test.ts +124 -0
  72. package/studio/server/introspect/diff.ts +252 -23
  73. package/studio/server/introspect/extractor.ts +46 -14
  74. package/studio/server/introspect/overlay-tsmorph.test.ts +164 -1
  75. package/studio/server/introspect/overlay-tsmorph.ts +381 -106
  76. package/studio/server/introspect/overlay.test.ts +72 -0
  77. package/studio/server/introspect/overlay.ts +16 -6
  78. package/studio/server/introspect/runtime.test.ts +217 -0
  79. package/studio/server/introspect/runtime.ts +18 -6
  80. package/studio/server/introspect/schema-refs.test.ts +100 -0
  81. package/studio/server/introspect/schema-refs.ts +23 -2
  82. package/studio/server/state/baseline.test.ts +51 -0
  83. package/studio/server/state/baseline.ts +31 -2
  84. package/studio/server/state/create.test.ts +37 -0
  85. package/studio/server/state/create.ts +21 -15
  86. package/studio/server/state/instance.test.ts +63 -0
  87. package/studio/server/state/instance.ts +65 -29
  88. package/studio/server/state/views.test.ts +209 -9
  89. package/studio/server/state/views.ts +176 -69
  90. package/studio/server/watch.test.ts +36 -0
  91. package/studio/server/watch.ts +24 -16
  92. package/studio/server/workspace-watch.ts +8 -3
  93. package/studio/shared/types.ts +164 -33
  94. package/viewer/dist/main.js +57 -57
  95. package/studio/client/dist/assets/index-Dspir4w7.js +0 -81
  96. package/studio/client/dist/assets/index-bVD2KJgz.js +0 -8
  97. package/studio/server/view-dev-server.test.ts +0 -111
  98. package/studio/server/view-dev-server.ts +0 -372
@@ -0,0 +1,193 @@
1
+ export type FunctionInputIssue = Readonly<{
2
+ code: string
3
+ path?: string
4
+ message: string
5
+ }>
6
+
7
+ export type QueryInputRepair =
8
+ | Readonly<{ phase: 'decode' | 'input'; path: string }>
9
+ | Readonly<{ phase: 'plan'; issue: string; path?: string }>
10
+ | Readonly<{
11
+ phase: 'limit'
12
+ limit: string
13
+ maximum: number
14
+ actual: number
15
+ path?: string
16
+ }>
17
+
18
+ export type SchemaUpgradeDetails =
19
+ | {
20
+ readonly origin?: string
21
+ readonly issue?: undefined
22
+ }
23
+ | {
24
+ readonly origin: string
25
+ readonly issue: 'issuer-changed'
26
+ readonly installedIssuer: string
27
+ readonly replacementIssuer: string
28
+ }
29
+
30
+ const JSON_POINTER = /^(?:\/(?:[^~/]|~[01])*)*$/u
31
+ const MAXIMUM_FUNCTION_ISSUES = 32
32
+ const MAXIMUM_FUNCTION_ISSUE_MESSAGE_LENGTH = 512
33
+
34
+ export function reasonCode(reason: unknown): string | undefined {
35
+ if (!record(reason) || typeof reason.code !== 'string') return undefined
36
+ return stableCode(reason.code) ? reason.code : undefined
37
+ }
38
+
39
+ /** Admit only bounded caller-safe Function input issues established by the Kernel. */
40
+ export function functionInputIssues(reason: unknown): readonly FunctionInputIssue[] {
41
+ if (!reasonWithCode(reason, 'FUNCTION_INPUT_INVALID')) return Object.freeze([])
42
+ const issues = reason.details.issues
43
+ if (!Array.isArray(issues)) return Object.freeze([])
44
+ return Object.freeze(
45
+ issues.slice(0, MAXIMUM_FUNCTION_ISSUES).flatMap((candidate): FunctionInputIssue[] => {
46
+ if (
47
+ !record(candidate) ||
48
+ typeof candidate.code !== 'string' ||
49
+ !stableCode(candidate.code) ||
50
+ typeof candidate.message !== 'string' ||
51
+ candidate.message.length === 0 ||
52
+ candidate.message.length > MAXIMUM_FUNCTION_ISSUE_MESSAGE_LENGTH ||
53
+ candidate.message.normalize('NFC') !== candidate.message ||
54
+ containsControl(candidate.message) ||
55
+ (candidate.path !== undefined && !coordinate(candidate.path))
56
+ ) {
57
+ return []
58
+ }
59
+ return [
60
+ Object.freeze({
61
+ code: candidate.code,
62
+ ...(candidate.path === undefined ? {} : { path: candidate.path }),
63
+ message: candidate.message,
64
+ }),
65
+ ]
66
+ }),
67
+ )
68
+ }
69
+
70
+ /** Admit only public Query-input repair variants; unknown details remain machine-only. */
71
+ export function queryInputRepair(reason: unknown): QueryInputRepair | undefined {
72
+ if (!reasonWithCode(reason, 'QUERY_INPUT_INVALID')) return undefined
73
+ const details = reason.details
74
+ if (details.phase === 'decode' || details.phase === 'input') {
75
+ if (!exact(details, ['phase', 'path']) || !coordinate(details.path)) return undefined
76
+ return Object.freeze({ phase: details.phase, path: details.path })
77
+ }
78
+ if (details.phase === 'plan') {
79
+ const fields = details.path === undefined ? ['phase', 'issue'] : ['phase', 'issue', 'path']
80
+ if (
81
+ !exact(details, fields) ||
82
+ typeof details.issue !== 'string' ||
83
+ !stableCode(details.issue) ||
84
+ (details.path !== undefined && !coordinate(details.path))
85
+ ) {
86
+ return undefined
87
+ }
88
+ return Object.freeze({
89
+ phase: 'plan',
90
+ issue: details.issue,
91
+ ...(details.path === undefined ? {} : { path: details.path }),
92
+ })
93
+ }
94
+ if (details.phase !== 'limit') return undefined
95
+ const fields =
96
+ details.path === undefined
97
+ ? ['phase', 'limit', 'maximum', 'actual']
98
+ : ['phase', 'limit', 'maximum', 'actual', 'path']
99
+ if (
100
+ !exact(details, fields) ||
101
+ typeof details.limit !== 'string' ||
102
+ !Number.isSafeInteger(details.maximum) ||
103
+ (details.maximum as number) < 0 ||
104
+ !Number.isSafeInteger(details.actual) ||
105
+ (details.actual as number) <= (details.maximum as number) ||
106
+ (details.path !== undefined && !coordinate(details.path))
107
+ ) {
108
+ return undefined
109
+ }
110
+ return Object.freeze({
111
+ phase: 'limit',
112
+ limit: details.limit,
113
+ maximum: details.maximum as number,
114
+ actual: details.actual as number,
115
+ ...(details.path === undefined ? {} : { path: details.path }),
116
+ })
117
+ }
118
+
119
+ /** Decode bounded recovery guidance while preserving the admitted reason itself elsewhere. */
120
+ export function schemaUpgradeDetails(reason: unknown): SchemaUpgradeDetails | undefined {
121
+ if (!record(reason) || reason.code !== 'SCHEMA_UPGRADE_INCOMPATIBLE') return undefined
122
+ const details = record(reason.details) ? reason.details : {}
123
+ const origin = typeof details.origin === 'string' ? details.origin : undefined
124
+ if (
125
+ details.issue === 'issuer-changed' &&
126
+ origin !== undefined &&
127
+ typeof details.installedIssuer === 'string' &&
128
+ typeof details.replacementIssuer === 'string'
129
+ ) {
130
+ return {
131
+ origin,
132
+ issue: details.issue,
133
+ installedIssuer: details.installedIssuer,
134
+ replacementIssuer: details.replacementIssuer,
135
+ }
136
+ }
137
+ return origin === undefined ? {} : { origin }
138
+ }
139
+
140
+ export function schemaUpgradeHint(details: SchemaUpgradeDetails): string {
141
+ const target = details.origin ?? '<origin>'
142
+ const explanation =
143
+ details.issue === 'issuer-changed'
144
+ ? 'A replacement cannot change an installed Domain issuer.'
145
+ : 'The replacement changes an immutable part of the installed Domain.'
146
+ return (
147
+ `${explanation} If this change is intentional, first run ` +
148
+ `\`astrale domain uninstall ${target}\`, then install it again. ` +
149
+ 'The Kernel refuses uninstall while dependents or business data remain; uninstall never deletes business data.'
150
+ )
151
+ }
152
+
153
+ function reasonWithCode(
154
+ input: unknown,
155
+ code: string,
156
+ ): input is Readonly<{ code: string; details: Readonly<Record<string, unknown>> }> {
157
+ return (
158
+ record(input) &&
159
+ exact(input, ['code', 'details']) &&
160
+ input.code === code &&
161
+ record(input.details)
162
+ )
163
+ }
164
+
165
+ function coordinate(input: unknown): input is string {
166
+ return (
167
+ typeof input === 'string' &&
168
+ input.length <= 1_024 &&
169
+ input.normalize('NFC') === input &&
170
+ JSON_POINTER.test(input)
171
+ )
172
+ }
173
+
174
+ function stableCode(input: string): boolean {
175
+ return /^[A-Z][A-Z0-9_]{0,127}$/u.test(input) && input.normalize('NFC') === input
176
+ }
177
+
178
+ function containsControl(input: string): boolean {
179
+ for (const character of input) {
180
+ const point = character.codePointAt(0)!
181
+ if (point <= 0x1f || point === 0x7f) return true
182
+ }
183
+ return false
184
+ }
185
+
186
+ function record(input: unknown): input is Readonly<Record<string, unknown>> {
187
+ return input !== null && typeof input === 'object' && !Array.isArray(input)
188
+ }
189
+
190
+ function exact(input: Readonly<Record<string, unknown>>, fields: readonly string[]): boolean {
191
+ const actual = Object.keys(input)
192
+ return actual.length === fields.length && actual.every((field) => fields.includes(field))
193
+ }
@@ -1,6 +1,11 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
2
 
3
- import { InstanceStoreSchema, normalizeInstanceKernelUrl, sanitizeStore } from '../instance'
3
+ import {
4
+ findBookmarkTrustConflicts,
5
+ InstanceStoreSchema,
6
+ normalizeInstanceKernelUrl,
7
+ sanitizeStore,
8
+ } from '../instance'
4
9
 
5
10
  describe('InstanceStoreSchema', () => {
6
11
  test('parses valid store with url', () => {
@@ -134,3 +139,48 @@ describe('sanitizeStore — read must not rewrite', () => {
134
139
  expect(changed).toBe(false)
135
140
  })
136
141
  })
142
+
143
+ describe('bookmark TLS trust collisions', () => {
144
+ test('finds the same normalized URL with a different CA configuration', () => {
145
+ const store = InstanceStoreSchema.parse({
146
+ active: 'stable',
147
+ instances: {
148
+ stable: {
149
+ url: 'https://local.example/kernel/',
150
+ caFile: '/certs/stable.pem',
151
+ },
152
+ alias: {
153
+ url: 'https://local.example/kernel',
154
+ caFile: '/certs/old.pem',
155
+ },
156
+ other: {
157
+ url: 'https://other.example/kernel',
158
+ caFile: '/certs/old.pem',
159
+ },
160
+ },
161
+ })
162
+
163
+ expect(
164
+ findBookmarkTrustConflicts(
165
+ store,
166
+ 'stable',
167
+ 'https://local.example/kernel',
168
+ '/certs/stable.pem',
169
+ ),
170
+ ).toEqual([{ name: 'alias', caFile: '/certs/old.pem' }])
171
+ })
172
+
173
+ test('treats custom CA versus system trust as a meaningful difference', () => {
174
+ const store = InstanceStoreSchema.parse({
175
+ active: 'custom',
176
+ instances: {
177
+ custom: { url: 'https://local.example', caFile: '/certs/local.pem' },
178
+ system: { url: 'https://local.example' },
179
+ },
180
+ })
181
+
182
+ expect(
183
+ findBookmarkTrustConflicts(store, 'custom', 'https://local.example', '/certs/local.pem'),
184
+ ).toEqual([{ name: 'system', caFile: null }])
185
+ })
186
+ })
@@ -1,5 +1,14 @@
1
1
  import { afterEach, describe, expect, test } from 'bun:test'
2
- import { mkdtemp, mkdir, readFile, realpath, rm, symlink, writeFile } from 'node:fs/promises'
2
+ import {
3
+ mkdtemp,
4
+ mkdir,
5
+ readFile,
6
+ realpath,
7
+ rm,
8
+ symlink,
9
+ utimes,
10
+ writeFile,
11
+ } from 'node:fs/promises'
3
12
  import { tmpdir } from 'node:os'
4
13
  import { dirname, join, relative } from 'node:path'
5
14
  import { pathToFileURL } from 'node:url'
@@ -61,6 +70,29 @@ describe('viewer asset resolution', () => {
61
70
  expect(await readFile(join(dist, 'main.js'), 'utf8')).toContain('viewer ready')
62
71
  })
63
72
 
73
+ test('rebuilds stale viewer assets in a source checkout', async () => {
74
+ const root = await mkdtemp(join(tmpdir(), 'astrale-view-stale-'))
75
+ temporaryDirectories.push(root)
76
+ const module = join(root, 'src', 'lib', 'view', 'assets.ts')
77
+ const source = join(root, 'viewer')
78
+ const dist = join(source, 'dist')
79
+
80
+ await mkdir(dirname(module), { recursive: true })
81
+ await mkdir(dist, { recursive: true })
82
+ await writeFile(module, '')
83
+ await writeFile(join(source, 'main.ts'), 'document.body.textContent = "fresh viewer"\n')
84
+ await writeFile(join(source, 'index.html'), '<!doctype html><body>fresh</body>\n')
85
+ await writeFile(join(dist, 'main.js'), 'document.body.textContent = "stale viewer"\n')
86
+ await writeFile(join(dist, 'index.html'), '<!doctype html><body>stale</body>\n')
87
+ const future = new Date(Date.now() + 2_000)
88
+ await utimes(join(source, 'main.ts'), future, future)
89
+
90
+ await ensureViewerAssets(pathToFileURL(module).href, join(root, 'bin', 'astrale'))
91
+
92
+ expect(await readFile(join(dist, 'main.js'), 'utf8')).toContain('fresh viewer')
93
+ expect(await readFile(join(dist, 'index.html'), 'utf8')).toContain('fresh')
94
+ })
95
+
64
96
  test('uses the bundled module location when invoked through a global bin symlink', async () => {
65
97
  const root = await mkdtemp(join(tmpdir(), 'astrale-view-bundle-'))
66
98
  temporaryDirectories.push(root)
@@ -0,0 +1,68 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { once } from 'node:events'
3
+
4
+ import type { ViewServeConfig } from '../view/session'
5
+
6
+ import { findFreePort } from '../port'
7
+ import { startViewServer } from '../view/server'
8
+
9
+ const digest = (character: string) => `sha256:${character.repeat(64)}` as const
10
+ const target = (value: string) => value as ViewServeConfig['session']['view']['target']
11
+ const issuer = (value: string) => value as ViewServeConfig['session']['view']['route']['issuer']
12
+ const revision = (character: string) =>
13
+ digest(character) as ViewServeConfig['session']['view']['route']['revision']
14
+
15
+ describe('view session server credentials', () => {
16
+ /** @evidence TEST-CLI-PLAIN-VIEW-RECEIVES-NO-CREDENTIAL */
17
+ test('refuses to mint a token for a handshake-none View', async () => {
18
+ const nonce = 'plain-view'
19
+ const port = await findFreePort(48_000, 200)
20
+ if (port === null) throw new Error('test port window exhausted')
21
+ const config = {
22
+ session: {
23
+ id: 'v-plain',
24
+ pid: 0,
25
+ port,
26
+ nonce,
27
+ pageUrl: `http://127.0.0.1:${port}/`,
28
+ view: {
29
+ target: target('/:example.test'),
30
+ route: {
31
+ key: 'example.test:view.public',
32
+ declaration: { target: { kind: 'domain' }, auth: 'required' },
33
+ href: 'https://example.test/ui/public',
34
+ handshake: 'none',
35
+ issuer: issuer('https://example.test'),
36
+ etag: digest('a'),
37
+ revision: revision('b'),
38
+ },
39
+ },
40
+ createdAt: '2026-08-20T00:00:00.000Z',
41
+ },
42
+ kernel: { creds: 'must-not-be-used' },
43
+ proxy: {
44
+ kernelUrl: 'https://kernel.test',
45
+ issuer: 'https://kernel.test',
46
+ direct: true,
47
+ },
48
+ idleMs: 60_000,
49
+ } satisfies ViewServeConfig
50
+ const server = startViewServer(config)
51
+ await once(server, 'listening')
52
+
53
+ try {
54
+ const response = await fetch(`http://127.0.0.1:${port}/s/${nonce}/token`, {
55
+ method: 'POST',
56
+ })
57
+
58
+ expect(response.status).toBe(403)
59
+ expect(await response.json()).toEqual({
60
+ error: 'plain views have no Astrale credential privilege',
61
+ })
62
+ } finally {
63
+ await new Promise<void>((resolve, reject) => {
64
+ server.close((error) => (error ? reject(error) : resolve()))
65
+ })
66
+ }
67
+ })
68
+ })
@@ -3,6 +3,7 @@ import type { IncomingHttpHeaders, request as httpRequest } from 'node:http'
3
3
  import { Buffer } from 'node:buffer'
4
4
  import { readFileSync } from 'node:fs'
5
5
  import { request as httpsRequest } from 'node:https'
6
+ import { rootCertificates } from 'node:tls'
6
7
 
7
8
  /** Create a Fetch capability whose HTTPS requests trust one CLI-selected CA file. */
8
9
  export function fetchWithCaFile(
@@ -32,7 +33,9 @@ function fetchWithNode(url: URL, init: RequestInit | undefined, ca: Buffer): Pro
32
33
  {
33
34
  method: init?.method ?? 'GET',
34
35
  headers: headersInitToRecord(init?.headers),
35
- ca,
36
+ // `ca` replaces Node's default trust set. Retain public roots because one
37
+ // Client Session may reach both a private Kernel and a public Domain issuer.
38
+ ca: [...rootCertificates, ca],
36
39
  },
37
40
  (response) => {
38
41
  const chunks: Buffer[] = []
@@ -40,8 +43,11 @@ function fetchWithNode(url: URL, init: RequestInit | undefined, ca: Buffer): Pro
40
43
  chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)),
41
44
  )
42
45
  response.on('end', () => {
43
- const result = new Response(Buffer.concat(chunks), {
44
- status: response.statusCode ?? 0,
46
+ const status = response.statusCode ?? 0
47
+ const body =
48
+ status === 204 || status === 205 || status === 304 ? null : Buffer.concat(chunks)
49
+ const result = new Response(body, {
50
+ status,
45
51
  statusText: response.statusMessage,
46
52
  headers: responseHeaders(response.headers),
47
53
  })
@@ -68,6 +68,11 @@ export type ResolvedInstance = {
68
68
  status?: string
69
69
  }
70
70
 
71
+ export type BookmarkTrustConflict = {
72
+ readonly name: string
73
+ readonly caFile: string | null
74
+ }
75
+
71
76
  function seed(): InstanceStore {
72
77
  return { active: '', instances: {} }
73
78
  }
@@ -198,6 +203,32 @@ export function resolveInstanceKey(store: InstanceStore, identifier: string): st
198
203
  return null
199
204
  }
200
205
 
206
+ /**
207
+ * Find other bookmarks for the same normalized Kernel URL whose TLS trust
208
+ * configuration differs. System trust (`undefined`) is a configuration too:
209
+ * mixing it with a custom CA is exactly as significant as mixing two CA files.
210
+ */
211
+ export function findBookmarkTrustConflicts(
212
+ store: InstanceStore,
213
+ name: string,
214
+ url: string,
215
+ caFile?: string,
216
+ ): BookmarkTrustConflict[] {
217
+ const normalizedUrl = normalizeInstanceKernelUrl(url)
218
+ const configuredCa = caFile ?? null
219
+ return Object.entries(store.instances).flatMap(([candidateName, entry]) => {
220
+ if (
221
+ candidateName === name ||
222
+ entry.url === undefined ||
223
+ normalizeInstanceKernelUrl(entry.url) !== normalizedUrl ||
224
+ (entry.caFile ?? null) === configuredCa
225
+ ) {
226
+ return []
227
+ }
228
+ return [{ name: candidateName, caFile: entry.caFile ?? null }]
229
+ })
230
+ }
231
+
201
232
  export async function addInstance(key: string, opts: AddInstanceOpts = {}): Promise<InstanceEntry> {
202
233
  validateName(key, 'Instance')
203
234
  if (RESERVED_SLUGS.has(key)) throw new ReservedSlugError(key)
@@ -1,4 +1,4 @@
1
- import { existsSync } from 'node:fs'
1
+ import { existsSync, statSync } from 'node:fs'
2
2
  import { copyFile } from 'node:fs/promises'
3
3
  import { dirname, join } from 'node:path'
4
4
  import { fileURLToPath } from 'node:url'
@@ -34,8 +34,8 @@ export async function ensureViewerAssets(
34
34
  entry = process.argv[1] ?? '.',
35
35
  ): Promise<string> {
36
36
  const dist = viewerDistDir(moduleUrl, entry)
37
- if (hasViewerBundle(dist)) return dist
38
37
  const srcDir = join(dist, '..')
38
+ if (hasViewerBundle(dist) && !viewerSourceIsNewer(srcDir, dist)) return dist
39
39
  const bun = (
40
40
  globalThis as { Bun?: { build: (o: object) => Promise<{ success: boolean; logs: unknown[] }> } }
41
41
  ).Bun
@@ -62,3 +62,17 @@ function hasViewerBundle(directory: string): boolean {
62
62
  function hasViewerSource(directory: string): boolean {
63
63
  return existsSync(join(directory, 'main.ts')) && existsSync(join(directory, 'index.html'))
64
64
  }
65
+
66
+ function viewerSourceIsNewer(source: string, dist: string): boolean {
67
+ if (!hasViewerSource(source)) return false
68
+ if (!hasViewerBundle(dist)) return true
69
+ const newestSource = Math.max(
70
+ statSync(join(source, 'main.ts')).mtimeMs,
71
+ statSync(join(source, 'index.html')).mtimeMs,
72
+ )
73
+ const oldestOutput = Math.min(
74
+ statSync(join(dist, 'main.js')).mtimeMs,
75
+ statSync(join(dist, 'index.html')).mtimeMs,
76
+ )
77
+ return newestSource > oldestOutput
78
+ }
@@ -142,6 +142,7 @@ describe('program composition', () => {
142
142
  'domain install',
143
143
  'domain list',
144
144
  'domain publish',
145
+ 'domain uninstall',
145
146
  'get',
146
147
  'identity',
147
148
  'identity create',
@@ -186,7 +187,7 @@ describe('program composition', () => {
186
187
  'whoami',
187
188
  ])
188
189
  expect(createHash('sha256').update(JSON.stringify(surface)).digest('hex')).toBe(
189
- '0f4739269db4c1dc8e1ba8cdf7840d7fbbe9c809234d2e21eef143a9de856382',
190
+ 'b21a8c98f0bb75432b460d4562c2d86f39ffb8e88349962e4611ccbf65fa9f73',
190
191
  )
191
192
  })
192
193
 
@@ -80,11 +80,12 @@ export async function buildProgram(): Promise<Command> {
80
80
 
81
81
  registerGroup(program, {
82
82
  name: 'domain',
83
- description: 'List, publish, and install domains (admin catalog + per-instance install)',
83
+ description: 'List, publish, install, and uninstall domains',
84
84
  commands: [
85
85
  withKernelOptions((await import('../commands/domain/list')).default),
86
86
  withKernelOptions((await import('../commands/domain/publish')).default),
87
87
  withKernelOptions((await import('../commands/domain/install')).default),
88
+ withKernelOptions((await import('../commands/domain/uninstall')).default),
88
89
  ],
89
90
  })
90
91
 
@@ -1 +1 @@
1
- import{m as L}from"./index-Dspir4w7.js";function S(y,b){for(var u=0;u<b.length;u++){const a=b[u];if(typeof a!="string"&&!Array.isArray(a)){for(const i in a)if(i!=="default"&&!(i in y)){const n=Object.getOwnPropertyDescriptor(a,i);n&&Object.defineProperty(y,i,n.get?n:{enumerable:!0,get:()=>a[i]})}}}return Object.freeze(Object.defineProperty(y,Symbol.toStringTag,{value:"Module"}))}function w(y){throw new Error('Could not dynamically require "'+y+'". Please configure the dynamicRequireTargets or/and ignoreDynamicRequires option of @rollup/plugin-commonjs appropriately for this require call to work.')}var O={exports:{}},j;function C(){return j||(j=1,(function(y,b){(function(u){y.exports=u()})(function(){return(function(){function u(a,i,n){function d(f,_){if(!i[f]){if(!a[f]){var h=typeof w=="function"&&w;if(!_&&h)return h(f,!0);if(g)return g(f,!0);var o=new Error("Cannot find module '"+f+"'");throw o.code="MODULE_NOT_FOUND",o}var e=i[f]={exports:{}};a[f][0].call(e.exports,function(r){var t=a[f][1][r];return d(t||r)},e,e.exports,u,a,i,n)}return i[f].exports}for(var g=typeof w=="function"&&w,m=0;m<n.length;m++)d(n[m]);return d}return u})()({1:[function(u,a,i){Object.defineProperty(i,"__esModule",{value:!0}),i.default=void 0;function n(o){"@babel/helpers - typeof";return n=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(e){return typeof e}:function(e){return e&&typeof Symbol=="function"&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},n(o)}function d(o,e){if(!(o instanceof e))throw new TypeError("Cannot call a class as a function")}function g(o,e){for(var r=0;r<e.length;r++){var t=e[r];t.enumerable=t.enumerable||!1,t.configurable=!0,"value"in t&&(t.writable=!0),Object.defineProperty(o,f(t.key),t)}}function m(o,e,r){return e&&g(o.prototype,e),Object.defineProperty(o,"prototype",{writable:!1}),o}function f(o){var e=_(o,"string");return n(e)=="symbol"?e:e+""}function _(o,e){if(n(o)!="object"||!o)return o;var r=o[Symbol.toPrimitive];if(r!==void 0){var t=r.call(o,e);if(n(t)!="object")return t;throw new TypeError("@@toPrimitive must return a primitive value.")}return String(o)}i.default=(function(){function o(){var e=this,r=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},t=r.defaultLayoutOptions,s=t===void 0?{}:t,l=r.algorithms,v=l===void 0?["layered","stress","mrtree","radial","force","disco","sporeOverlap","sporeCompaction","rectpacking"]:l,c=r.workerFactory,p=r.workerUrl;if(d(this,o),this.defaultLayoutOptions=s,this.initialized=!1,typeof p>"u"&&typeof c>"u")throw new Error("Cannot construct an ELK without both 'workerUrl' and 'workerFactory'.");var k=c;typeof p<"u"&&typeof c>"u"&&(k=function(M){return new Worker(M)});var E=k(p);if(typeof E.postMessage!="function")throw new TypeError("Created worker does not provide the required 'postMessage' function.");this.worker=new h(E),this.worker.postMessage({cmd:"register",algorithms:v}).then(function(P){return e.initialized=!0}).catch(console.err)}return m(o,[{key:"layout",value:function(r){var t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},s=t.layoutOptions,l=s===void 0?this.defaultLayoutOptions:s,v=t.logging,c=v===void 0?!1:v,p=t.measureExecutionTime,k=p===void 0?!1:p;return r?this.worker.postMessage({cmd:"layout",graph:r,layoutOptions:l,options:{logging:c,measureExecutionTime:k}}):Promise.reject(new Error("Missing mandatory parameter 'graph'."))}},{key:"knownLayoutAlgorithms",value:function(){return this.worker.postMessage({cmd:"algorithms"})}},{key:"knownLayoutOptions",value:function(){return this.worker.postMessage({cmd:"options"})}},{key:"knownLayoutCategories",value:function(){return this.worker.postMessage({cmd:"categories"})}},{key:"terminateWorker",value:function(){this.worker&&this.worker.terminate()}}])})();var h=(function(){function o(e){var r=this;if(d(this,o),e===void 0)throw new Error("Missing mandatory parameter 'worker'.");this.resolvers={},this.worker=e,this.worker.onmessage=function(t){setTimeout(function(){r.receive(r,t)},0)}}return m(o,[{key:"postMessage",value:function(r){var t=this.id||0;this.id=t+1,r.id=t;var s=this;return new Promise(function(l,v){s.resolvers[t]=function(c,p){c?(s.convertGwtStyleError(c),v(c)):l(p)},s.worker.postMessage(r)})}},{key:"receive",value:function(r,t){var s=t.data,l=r.resolvers[s.id];l&&(delete r.resolvers[s.id],s.error?l(s.error):l(null,s.data))}},{key:"terminate",value:function(){this.worker&&this.worker.terminate()}},{key:"convertGwtStyleError",value:function(r){if(r){var t=r.__java$exception;t&&(t.cause&&t.cause.backingJsObject&&(r.cause=t.cause.backingJsObject,this.convertGwtStyleError(r.cause)),delete r.__java$exception)}}}])})()},{}],2:[function(u,a,i){var n=u("./elk-api.js").default;Object.defineProperty(a.exports,"__esModule",{value:!0}),a.exports=n,n.default=n},{"./elk-api.js":1}]},{},[2])(2)})})(O)),O.exports}var x=C();const A=L(x),q=S({__proto__:null,default:A},[x]);export{q as e};
1
+ import{m as L}from"./index-LGSWRrk8.js";function S(y,b){for(var u=0;u<b.length;u++){const a=b[u];if(typeof a!="string"&&!Array.isArray(a)){for(const i in a)if(i!=="default"&&!(i in y)){const n=Object.getOwnPropertyDescriptor(a,i);n&&Object.defineProperty(y,i,n.get?n:{enumerable:!0,get:()=>a[i]})}}}return Object.freeze(Object.defineProperty(y,Symbol.toStringTag,{value:"Module"}))}function w(y){throw new Error('Could not dynamically require "'+y+'". Please configure the dynamicRequireTargets or/and ignoreDynamicRequires option of @rollup/plugin-commonjs appropriately for this require call to work.')}var O={exports:{}},j;function C(){return j||(j=1,(function(y,b){(function(u){y.exports=u()})(function(){return(function(){function u(a,i,n){function d(f,_){if(!i[f]){if(!a[f]){var h=typeof w=="function"&&w;if(!_&&h)return h(f,!0);if(g)return g(f,!0);var o=new Error("Cannot find module '"+f+"'");throw o.code="MODULE_NOT_FOUND",o}var e=i[f]={exports:{}};a[f][0].call(e.exports,function(r){var t=a[f][1][r];return d(t||r)},e,e.exports,u,a,i,n)}return i[f].exports}for(var g=typeof w=="function"&&w,m=0;m<n.length;m++)d(n[m]);return d}return u})()({1:[function(u,a,i){Object.defineProperty(i,"__esModule",{value:!0}),i.default=void 0;function n(o){"@babel/helpers - typeof";return n=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(e){return typeof e}:function(e){return e&&typeof Symbol=="function"&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},n(o)}function d(o,e){if(!(o instanceof e))throw new TypeError("Cannot call a class as a function")}function g(o,e){for(var r=0;r<e.length;r++){var t=e[r];t.enumerable=t.enumerable||!1,t.configurable=!0,"value"in t&&(t.writable=!0),Object.defineProperty(o,f(t.key),t)}}function m(o,e,r){return e&&g(o.prototype,e),Object.defineProperty(o,"prototype",{writable:!1}),o}function f(o){var e=_(o,"string");return n(e)=="symbol"?e:e+""}function _(o,e){if(n(o)!="object"||!o)return o;var r=o[Symbol.toPrimitive];if(r!==void 0){var t=r.call(o,e);if(n(t)!="object")return t;throw new TypeError("@@toPrimitive must return a primitive value.")}return String(o)}i.default=(function(){function o(){var e=this,r=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},t=r.defaultLayoutOptions,s=t===void 0?{}:t,l=r.algorithms,v=l===void 0?["layered","stress","mrtree","radial","force","disco","sporeOverlap","sporeCompaction","rectpacking"]:l,c=r.workerFactory,p=r.workerUrl;if(d(this,o),this.defaultLayoutOptions=s,this.initialized=!1,typeof p>"u"&&typeof c>"u")throw new Error("Cannot construct an ELK without both 'workerUrl' and 'workerFactory'.");var k=c;typeof p<"u"&&typeof c>"u"&&(k=function(M){return new Worker(M)});var E=k(p);if(typeof E.postMessage!="function")throw new TypeError("Created worker does not provide the required 'postMessage' function.");this.worker=new h(E),this.worker.postMessage({cmd:"register",algorithms:v}).then(function(P){return e.initialized=!0}).catch(console.err)}return m(o,[{key:"layout",value:function(r){var t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},s=t.layoutOptions,l=s===void 0?this.defaultLayoutOptions:s,v=t.logging,c=v===void 0?!1:v,p=t.measureExecutionTime,k=p===void 0?!1:p;return r?this.worker.postMessage({cmd:"layout",graph:r,layoutOptions:l,options:{logging:c,measureExecutionTime:k}}):Promise.reject(new Error("Missing mandatory parameter 'graph'."))}},{key:"knownLayoutAlgorithms",value:function(){return this.worker.postMessage({cmd:"algorithms"})}},{key:"knownLayoutOptions",value:function(){return this.worker.postMessage({cmd:"options"})}},{key:"knownLayoutCategories",value:function(){return this.worker.postMessage({cmd:"categories"})}},{key:"terminateWorker",value:function(){this.worker&&this.worker.terminate()}}])})();var h=(function(){function o(e){var r=this;if(d(this,o),e===void 0)throw new Error("Missing mandatory parameter 'worker'.");this.resolvers={},this.worker=e,this.worker.onmessage=function(t){setTimeout(function(){r.receive(r,t)},0)}}return m(o,[{key:"postMessage",value:function(r){var t=this.id||0;this.id=t+1,r.id=t;var s=this;return new Promise(function(l,v){s.resolvers[t]=function(c,p){c?(s.convertGwtStyleError(c),v(c)):l(p)},s.worker.postMessage(r)})}},{key:"receive",value:function(r,t){var s=t.data,l=r.resolvers[s.id];l&&(delete r.resolvers[s.id],s.error?l(s.error):l(null,s.data))}},{key:"terminate",value:function(){this.worker&&this.worker.terminate()}},{key:"convertGwtStyleError",value:function(r){if(r){var t=r.__java$exception;t&&(t.cause&&t.cause.backingJsObject&&(r.cause=t.cause.backingJsObject,this.convertGwtStyleError(r.cause)),delete r.__java$exception)}}}])})()},{}],2:[function(u,a,i){var n=u("./elk-api.js").default;Object.defineProperty(a.exports,"__esModule",{value:!0}),a.exports=n,n.default=n},{"./elk-api.js":1}]},{},[2])(2)})})(O)),O.exports}var x=C();const A=L(x),q=S({__proto__:null,default:A},[x]);export{q as e};