@astrale-os/cli 1.0.0-beta.1 → 1.0.0-beta.2
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/README.md +7 -11
- package/dist/astrale.js +3737 -3523
- package/dist/public/connect-core.js +396 -392
- package/dist/public/keys/index.js +371 -378
- package/dist/public/paths/index.js +371 -378
- package/dist/types/lib/idp.d.ts +6 -0
- package/dist/types/lib/meta.d.ts +2 -2
- package/package.json +6 -4
- package/src/admin/binding.ts +168 -0
- package/src/admin/catalog/__tests__/client.test.ts +2 -2
- package/src/admin/catalog/client.ts +4 -9
- package/src/admin/instance/__tests__/client.test.ts +2 -2
- package/src/admin/instance/client.ts +6 -11
- package/src/commands/__tests__/auth-login.test.ts +54 -1
- package/src/commands/__tests__/call.test.ts +5 -1
- package/src/commands/__tests__/domain-install-operation.test.ts +23 -7
- package/src/commands/__tests__/domain-install-owned.test.ts +5 -1
- package/src/commands/__tests__/domain-list.test.ts +83 -4
- package/src/commands/__tests__/install-identity-override.test.ts +30 -19
- package/src/commands/__tests__/studio-descriptor.test.ts +22 -0
- package/src/commands/__tests__/update.test.ts +23 -16
- package/src/commands/domain/install.ts +11 -15
- package/src/commands/domain/list.ts +17 -12
- package/src/commands/studio.ts +20 -0
- package/src/commands/update.ts +9 -5
- package/src/connection/__tests__/errors.test.ts +16 -12
- package/src/connection/__tests__/exchange.test.ts +6 -5
- package/src/connection/exchange.ts +5 -4
- package/src/lib/__tests__/domain-publication.test.ts +144 -0
- package/src/lib/__tests__/idp-session.test.ts +22 -0
- package/src/lib/__tests__/idp.test.ts +2 -0
- package/src/lib/__tests__/instance.test.ts +6 -0
- package/src/lib/__tests__/studio-server-deps.test.ts +53 -16
- package/src/lib/__tests__/update.test.ts +12 -0
- package/src/lib/domain-publication.ts +102 -0
- package/src/lib/idp.ts +5 -1
- package/src/lib/instance.ts +6 -1
- package/src/lib/login-flow.ts +13 -3
- package/src/lib/meta.ts +2 -2
- package/src/lib/update.ts +4 -4
- package/src/program/__tests__/program.test.ts +1 -1
- package/studio/client/dist/assets/{elk-api-D2xgMJvi.js → elk-api-lwhFo7vB.js} +1 -1
- package/studio/client/dist/assets/index-B-c-smo9.js +8 -0
- package/studio/client/dist/assets/index-BckHuAWk.js +81 -0
- package/studio/client/dist/assets/index-C4aNQ6dz.css +1 -0
- package/studio/client/dist/index.html +2 -2
- package/studio/package.json +3 -1
- package/studio/server/agent/bridge/stdio.ts +8 -1
- package/studio/server/agent/conversation.test.ts +10 -5
- package/studio/server/agent/conversation.ts +56 -8
- package/studio/server/agent/harness/codex/loadout.ts +18 -21
- package/studio/server/agent/harness/gateway/config.ts +20 -18
- package/studio/server/agent/harness/selection.ts +9 -1
- package/studio/server/agent/prompts/system.ts +3 -2
- package/studio/server/agent/prompts/turn.ts +13 -4
- package/studio/server/agent/run/completion.ts +2 -2
- package/studio/server/agent/run/preparation.ts +6 -5
- package/studio/server/agent/run/transcript.test.ts +35 -1
- package/studio/server/agent/run/transcript.ts +169 -3
- package/studio/server/agent/run/usage.test.ts +24 -0
- package/studio/server/agent/run/usage.ts +26 -1
- package/studio/server/api/canvas.ts +44 -0
- package/studio/server/api/comments.ts +85 -0
- package/studio/server/api/context.ts +61 -0
- package/studio/server/api/deployment.ts +39 -0
- package/studio/server/api/documents.ts +55 -0
- package/studio/server/api/domain.ts +59 -0
- package/studio/server/api/http.ts +45 -0
- package/studio/server/api/project.ts +57 -0
- package/studio/server/api/schema.ts +12 -0
- package/studio/server/api/views.ts +57 -0
- package/studio/server/api/workspace.ts +60 -0
- package/studio/server/api.test.ts +147 -0
- package/studio/server/api.ts +20 -422
- package/studio/server/cache.test.ts +111 -0
- package/studio/server/cache.ts +163 -9
- package/studio/server/cli-consumers.test.ts +494 -0
- package/studio/server/cli.test.ts +88 -0
- package/studio/server/cli.ts +240 -0
- package/studio/server/client-package.ts +25 -1
- package/studio/server/domain.test.ts +30 -2
- package/studio/server/domain.ts +14 -2
- package/studio/server/environment/dotenv-preview.test.ts +39 -0
- package/studio/server/environment/dotenv-preview.ts +27 -0
- package/studio/server/environment/files.test.ts +53 -0
- package/studio/server/{state/env.ts → environment/files.ts} +7 -48
- package/studio/server/{state → handoff}/copy.ts +21 -6
- package/studio/server/handoff/service.test.ts +22 -0
- package/studio/server/{state/handoff.ts → handoff/service.ts} +21 -14
- package/studio/server/instances/active.ts +72 -0
- package/studio/server/instances/deploy-record.test.ts +31 -0
- package/studio/server/instances/deploy-record.ts +37 -0
- package/studio/server/instances/deploy.ts +56 -0
- package/studio/server/{state/instance.test.ts → instances/probe.test.ts} +25 -21
- package/studio/server/instances/probe.ts +67 -0
- package/studio/server/instances/status.test.ts +12 -0
- package/studio/server/instances/status.ts +49 -0
- package/studio/server/introspect/anatomy/client-tree.ts +57 -0
- package/studio/server/introspect/anatomy/env-fields.ts +66 -0
- package/studio/server/introspect/anatomy/schema-definition.ts +53 -0
- package/studio/server/introspect/anatomy/source.ts +181 -0
- package/studio/server/introspect/anatomy/views/legacy.ts +232 -0
- package/studio/server/introspect/anatomy/views/routes.ts +105 -0
- package/studio/server/introspect/anatomy/views.ts +58 -0
- package/studio/server/introspect/anatomy-extras.test.ts +91 -4
- package/studio/server/introspect/anatomy-extras.ts +7 -726
- package/studio/server/introspect/anatomy.ts +46 -19
- package/studio/server/introspect/bundle.ts +15 -6
- package/studio/server/introspect/canonical-schema.test.ts +79 -0
- package/studio/server/introspect/canonical-schema.ts +80 -18
- package/studio/server/introspect/config-preview.test.ts +32 -0
- package/studio/server/introspect/config-preview.ts +119 -0
- package/studio/server/introspect/diff.test.ts +7 -11
- package/studio/server/introspect/diff.ts +28 -37
- package/studio/server/introspect/extractor.ts +8 -6
- package/studio/server/introspect/hash.ts +5 -2
- package/studio/server/introspect/overlay-tsmorph.ts +7 -1305
- package/studio/server/introspect/revision.test.ts +54 -0
- package/studio/server/introspect/revision.ts +49 -0
- package/studio/server/introspect/runtime.test.ts +63 -5
- package/studio/server/introspect/runtime.ts +49 -3
- package/studio/server/introspect/schema-ir-json.ts +181 -0
- package/studio/server/introspect/source-overlay/annotations.ts +14 -0
- package/studio/server/introspect/source-overlay/handlers.ts +681 -0
- package/studio/server/introspect/source-overlay/kernel-calls.ts +49 -0
- package/studio/server/introspect/source-overlay/project.ts +248 -0
- package/studio/server/introspect/source-overlay/spans.ts +360 -0
- package/studio/server/json.ts +46 -0
- package/studio/server/lifecycle.ts +5 -1
- package/studio/server/sse.test.ts +26 -0
- package/studio/server/sse.ts +4 -1
- package/studio/server/state/baseline.test.ts +212 -1
- package/studio/server/state/baseline.ts +145 -29
- package/studio/server/state/comments.test.ts +66 -1
- package/studio/server/state/comments.ts +173 -10
- package/studio/server/state/context.ts +34 -2
- package/studio/server/state/documents.ts +42 -1
- package/studio/server/state/integrations.ts +27 -1
- package/studio/server/state/layout.test.ts +34 -0
- package/studio/server/state/layout.ts +44 -17
- package/studio/server/state/settings.ts +29 -22
- package/studio/server/state/store.test.ts +122 -0
- package/studio/server/state/store.ts +66 -17
- package/studio/server/state/visibility.ts +22 -7
- package/studio/server/views/model.test.ts +57 -0
- package/studio/server/views/model.ts +66 -0
- package/studio/server/views/runtime.ts +40 -0
- package/studio/server/views/selection-repository.test.ts +31 -0
- package/studio/server/views/selection-repository.ts +60 -0
- package/studio/server/views/session.test.ts +73 -0
- package/studio/server/views/session.ts +166 -0
- package/studio/server/{state/views.test.ts → views/target.test.ts} +8 -159
- package/studio/server/views/target.ts +248 -0
- package/studio/server/watch.ts +1 -1
- package/studio/server/workspace/catalog.test.ts +17 -0
- package/studio/server/workspace/catalog.ts +43 -0
- package/studio/server/{state → workspace}/create.test.ts +1 -1
- package/studio/server/{state → workspace}/create.ts +1 -1
- package/studio/server/{state → workspace}/git.ts +1 -1
- package/studio/server/workspace/updates.ts +83 -0
- package/studio/server/workspace-state.ts +1 -1
- package/studio/shared/contracts/README.md +16 -0
- package/studio/shared/contracts/agent.ts +270 -0
- package/studio/shared/contracts/runtime.ts +50 -0
- package/studio/shared/contracts/schema.ts +462 -0
- package/studio/shared/contracts/surface.test.ts +319 -0
- package/studio/shared/contracts/workspace.ts +274 -0
- package/studio/shared/layout.test.ts +116 -0
- package/studio/shared/schema/identity.test.ts +58 -0
- package/studio/shared/schema/identity.ts +82 -0
- package/studio/shared/types.ts +11 -1099
- package/studio/tsconfig.json +1 -1
- package/viewer/dist/main.js +35 -35
- package/studio/client/dist/assets/index-BMdnsIJA.css +0 -1
- package/studio/client/dist/assets/index-D-vRV8w7.js +0 -8
- package/studio/client/dist/assets/index-LGSWRrk8.js +0 -81
- package/studio/server/state/catalog.ts +0 -117
- package/studio/server/state/instance.ts +0 -280
- package/studio/server/state/updates.ts +0 -63
- package/studio/server/state/views.ts +0 -535
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { defineDomain } from '@astrale-os/sdk'
|
|
2
|
+
import { issuer } from '@astrale-os/sdk/auth'
|
|
3
|
+
import { createDeployment } from '@astrale-os/sdk/deployment'
|
|
4
|
+
import { defineSchema } from '@astrale-os/sdk/schema/v1'
|
|
1
5
|
import { afterAll, describe, expect, test } from 'bun:test'
|
|
2
6
|
|
|
3
7
|
import { domainRefFromTarget, isIdentityOverride, probeDeclaredOrigin } from '../domain/install'
|
|
@@ -29,46 +33,53 @@ describe('identity-override detection', () => {
|
|
|
29
33
|
})
|
|
30
34
|
})
|
|
31
35
|
|
|
32
|
-
describe('declared-origin probe
|
|
36
|
+
describe('declared-origin Publication probe', () => {
|
|
33
37
|
const servers: { stop(): void }[] = []
|
|
34
38
|
afterAll(() => {
|
|
35
39
|
for (const s of servers) s.stop()
|
|
36
40
|
})
|
|
37
41
|
|
|
38
|
-
function
|
|
42
|
+
function servePublication(handler: (req: Request) => Response): string {
|
|
39
43
|
const server = Bun.serve({ port: 0, fetch: handler })
|
|
40
44
|
servers.push(server)
|
|
41
45
|
return `http://localhost:${server.port}`
|
|
42
46
|
}
|
|
43
47
|
|
|
44
|
-
test('reads origin from a well-formed
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
48
|
+
test('reads origin from a well-formed canonical Publication', async () => {
|
|
49
|
+
const schema = defineSchema('crm.acme.dev', {})
|
|
50
|
+
const definition = defineDomain({
|
|
51
|
+
schema,
|
|
52
|
+
handlers: { functions: {}, classes: {}, interfaces: {} },
|
|
53
|
+
})
|
|
54
|
+
const deployed = createDeployment({
|
|
55
|
+
definition,
|
|
56
|
+
issuer: issuer.accept('https://crm.acme.dev'),
|
|
57
|
+
bundleHref: 'https://crm.acme.dev/domain.bundle.json',
|
|
58
|
+
bindings: { callables: [] },
|
|
59
|
+
}).publication
|
|
60
|
+
const url = servePublication((req) =>
|
|
61
|
+
new URL(req.url).pathname === '/.well-known/astrale/domain.json'
|
|
62
|
+
? Response.json(deployed)
|
|
54
63
|
: new Response('nope', { status: 404 }),
|
|
55
64
|
)
|
|
56
65
|
expect(await probeDeclaredOrigin(url)).toBe('crm.acme.dev')
|
|
57
66
|
})
|
|
58
67
|
|
|
59
|
-
test('
|
|
60
|
-
const legacy =
|
|
61
|
-
|
|
68
|
+
test('does not accept the removed pre-Kernel-V2 domainName shape', async () => {
|
|
69
|
+
const legacy = servePublication(() =>
|
|
70
|
+
Response.json({ iss: 'https://x', domainName: 'crm.acme.dev' }),
|
|
71
|
+
)
|
|
72
|
+
expect(await probeDeclaredOrigin(legacy)).toBeUndefined()
|
|
62
73
|
})
|
|
63
74
|
|
|
64
|
-
test('degrades to undefined on
|
|
65
|
-
const noName =
|
|
75
|
+
test('degrades to undefined on invalid Publication, non-200, bad JSON, or dead host', async () => {
|
|
76
|
+
const noName = servePublication(() => Response.json({ iss: 'https://x' }))
|
|
66
77
|
expect(await probeDeclaredOrigin(noName)).toBeUndefined()
|
|
67
78
|
|
|
68
|
-
const error =
|
|
79
|
+
const error = servePublication(() => new Response('boom', { status: 500 }))
|
|
69
80
|
expect(await probeDeclaredOrigin(error)).toBeUndefined()
|
|
70
81
|
|
|
71
|
-
const badJson =
|
|
82
|
+
const badJson = servePublication(() => new Response('<html>', { status: 200 }))
|
|
72
83
|
expect(await probeDeclaredOrigin(badJson)).toBeUndefined()
|
|
73
84
|
|
|
74
85
|
expect(await probeDeclaredOrigin('http://127.0.0.1:1')).toBeUndefined()
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test'
|
|
2
|
+
import { realpathSync } from 'node:fs'
|
|
3
|
+
|
|
4
|
+
import { encodeStudioCliDescriptor } from '../studio'
|
|
5
|
+
|
|
6
|
+
test('Studio descriptor preserves the exact runtime and script entrypoint', () => {
|
|
7
|
+
expect(JSON.parse(encodeStudioCliDescriptor(process.execPath, import.meta.path))).toEqual({
|
|
8
|
+
version: 1,
|
|
9
|
+
executable: process.execPath,
|
|
10
|
+
args: [realpathSync(import.meta.path)],
|
|
11
|
+
})
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('Studio descriptor reinvokes a compiled executable without its virtual bunfs entry', () => {
|
|
15
|
+
expect(JSON.parse(encodeStudioCliDescriptor('/opt/bin/astrale', '/$bunfs/root/astrale'))).toEqual(
|
|
16
|
+
{
|
|
17
|
+
version: 1,
|
|
18
|
+
executable: '/opt/bin/astrale',
|
|
19
|
+
args: [],
|
|
20
|
+
},
|
|
21
|
+
)
|
|
22
|
+
})
|
|
@@ -6,14 +6,17 @@ describe('CLI update staleness', () => {
|
|
|
6
6
|
test('trusts the release manifest for a script install without consulting npm latest', async () => {
|
|
7
7
|
const fetchPackageVersion = mock(async () => '0.8.1-alpha.7')
|
|
8
8
|
const result = await cliStale(
|
|
9
|
-
{
|
|
9
|
+
{},
|
|
10
10
|
{
|
|
11
|
-
update: async () =>
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
update: async ({ channel }) => {
|
|
12
|
+
expect(channel).toBe('beta')
|
|
13
|
+
return {
|
|
14
|
+
status: 'up-to-date',
|
|
15
|
+
currentVersion: '1.0.0-beta.0',
|
|
16
|
+
latestVersion: '1.0.0-beta.0',
|
|
17
|
+
channel: 'beta',
|
|
18
|
+
}
|
|
19
|
+
},
|
|
17
20
|
fetchPackageVersion,
|
|
18
21
|
},
|
|
19
22
|
)
|
|
@@ -28,37 +31,41 @@ describe('CLI update staleness', () => {
|
|
|
28
31
|
expect(fetchPackageVersion).not.toHaveBeenCalled()
|
|
29
32
|
})
|
|
30
33
|
|
|
31
|
-
test('uses
|
|
34
|
+
test('uses an explicit npm dist-tag only for package-managed installs', async () => {
|
|
32
35
|
const result = await cliStale(
|
|
33
|
-
{ channel: '
|
|
36
|
+
{ channel: 'canary' },
|
|
34
37
|
{
|
|
35
38
|
update: async () => {
|
|
36
39
|
throw new Error('package managed')
|
|
37
40
|
},
|
|
38
41
|
fetchPackageVersion: async ({ channel }) => {
|
|
39
|
-
expect(channel).toBe('
|
|
40
|
-
return '1.0.0-
|
|
42
|
+
expect(channel).toBe('canary')
|
|
43
|
+
return '1.0.0-canary.0'
|
|
41
44
|
},
|
|
42
45
|
},
|
|
43
46
|
)
|
|
44
47
|
|
|
45
48
|
expect(result).toMatchObject({
|
|
46
49
|
managed: true,
|
|
47
|
-
latest: '1.0.0-
|
|
50
|
+
latest: '1.0.0-canary.0',
|
|
48
51
|
channel: 'npm',
|
|
49
52
|
})
|
|
50
53
|
})
|
|
51
54
|
|
|
52
|
-
test('maps the stable channel to
|
|
55
|
+
test('maps the default to beta and the stable channel to npm latest', async () => {
|
|
53
56
|
const originalFetch = globalThis.fetch
|
|
54
57
|
const fetchMock = mock(async (input: RequestInfo | URL) => {
|
|
55
|
-
|
|
56
|
-
return Response.json({ version: '1.0.0' })
|
|
58
|
+
const target = String(input).split('/').at(-1)
|
|
59
|
+
return Response.json({ version: target === 'beta' ? '1.0.0-beta.0' : '1.0.0' })
|
|
57
60
|
})
|
|
58
61
|
globalThis.fetch = fetchMock as unknown as typeof fetch
|
|
59
62
|
try {
|
|
63
|
+
expect(await fetchNpmTargetVersion({})).toBe('1.0.0-beta.0')
|
|
60
64
|
expect(await fetchNpmTargetVersion({ channel: 'stable' })).toBe('1.0.0')
|
|
61
|
-
expect(fetchMock).
|
|
65
|
+
expect(fetchMock.mock.calls.map(([input]) => String(input))).toEqual([
|
|
66
|
+
'https://registry.npmjs.org/@astrale-os/cli/beta',
|
|
67
|
+
'https://registry.npmjs.org/@astrale-os/cli/latest',
|
|
68
|
+
])
|
|
62
69
|
} finally {
|
|
63
70
|
globalThis.fetch = originalFetch
|
|
64
71
|
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from '../../lib/admin-domain'
|
|
16
16
|
import { listOwnedInstancesInContext, type OwnedInstanceInfo } from '../../lib/admin-instance'
|
|
17
17
|
import { ADMIN_TARGET_OPTIONS, type AdminTargetCommandOpts } from '../../lib/admin-target'
|
|
18
|
+
import { fetchDomainPublication } from '../../lib/domain-publication'
|
|
18
19
|
import { getActive } from '../../lib/instance'
|
|
19
20
|
import { fatal, log, withSpinner } from '../../lib/log'
|
|
20
21
|
import { isMachine, output } from '../../lib/output'
|
|
@@ -429,11 +430,11 @@ export async function installDirect(
|
|
|
429
430
|
log.dim(` operation: ${result.operation}`)
|
|
430
431
|
// Belt-and-braces: the kernel-confirmed origin is authoritative. If it
|
|
431
432
|
// aliases the host and the pre-install gate never consented to THAT
|
|
432
|
-
// origin (lying or
|
|
433
|
+
// origin (lying or unavailable Publication), say so loudly after the fact.
|
|
433
434
|
if (isIdentityOverride(installed.origin, host) && installed.origin !== consentedOrigin) {
|
|
434
435
|
log.warn(
|
|
435
436
|
`Installed origin "${installed.origin}" differs from the serving host "${host}" ` +
|
|
436
|
-
`and was not confirmed before install (the worker
|
|
437
|
+
`and was not confirmed before install (the worker Publication was unavailable). ` +
|
|
437
438
|
`Every ${installed.origin}/* call on this instance now routes to ${host}.`,
|
|
438
439
|
)
|
|
439
440
|
}
|
|
@@ -478,10 +479,10 @@ export function isIdentityOverride(origin: string, host: string): boolean {
|
|
|
478
479
|
* Claiming an origin that differs from the serving host is an explicit actAs
|
|
479
480
|
* and needs typed consent (or `--allow-identity-override` in scripts).
|
|
480
481
|
*
|
|
481
|
-
* The pre-install check reads the worker's
|
|
482
|
+
* The pre-install check reads the worker's canonical Publication origin,
|
|
482
483
|
* so it is consent UX, not enforcement — a hostile worker can lie here, and
|
|
483
484
|
* the kernel anchors the cryptographic identity (`iss`) on the real URL
|
|
484
|
-
* regardless. When
|
|
485
|
+
* regardless. When the Publication is unreachable or invalid, the gate
|
|
485
486
|
* degrades to a warning and the kernel-confirmed origin is re-checked after
|
|
486
487
|
* install (see `format` above).
|
|
487
488
|
*
|
|
@@ -496,7 +497,8 @@ async function ensureIdentityOverrideConsent(
|
|
|
496
497
|
const origin = await probeDeclaredOrigin(url)
|
|
497
498
|
if (origin === undefined) {
|
|
498
499
|
log.warn(
|
|
499
|
-
`Could not read a declared origin from
|
|
500
|
+
`Could not read a declared origin from ` +
|
|
501
|
+
`${new URL('/.well-known/astrale/domain.json', url).href} — ` +
|
|
500
502
|
`skipping the pre-install identity check (the installed origin is verified after install).`,
|
|
501
503
|
)
|
|
502
504
|
return undefined
|
|
@@ -534,19 +536,13 @@ async function ensureIdentityOverrideConsent(
|
|
|
534
536
|
return origin
|
|
535
537
|
}
|
|
536
538
|
|
|
537
|
-
/** Best-effort read of the worker's
|
|
539
|
+
/** Best-effort read of the worker's origin from its admitted canonical Publication. */
|
|
538
540
|
export async function probeDeclaredOrigin(url: string): Promise<string | undefined> {
|
|
539
541
|
try {
|
|
540
|
-
|
|
541
|
-
if (!res.ok) return undefined
|
|
542
|
-
// SDK workers serve `origin`; `domainName` is the pre-Kernel-V2 name, kept
|
|
543
|
-
// as a fallback for workers deployed before the rename.
|
|
544
|
-
const body = (await res.json()) as { origin?: unknown; domainName?: unknown }
|
|
545
|
-
const declared = body.origin ?? body.domainName
|
|
546
|
-
return typeof declared === 'string' && declared.length > 0 ? declared : undefined
|
|
542
|
+
return (await fetchDomainPublication(url, AbortSignal.timeout(10_000))).origin
|
|
547
543
|
} catch {
|
|
548
|
-
//
|
|
549
|
-
// itself will surface a dead worker with its own error.
|
|
544
|
+
// An unreachable or invalid Publication is not fatal here: the caller warns
|
|
545
|
+
// and the install itself will surface a dead worker with its own error.
|
|
550
546
|
return undefined
|
|
551
547
|
}
|
|
552
548
|
}
|
|
@@ -6,6 +6,7 @@ import type { CommandDefinition } from '../../program/index'
|
|
|
6
6
|
|
|
7
7
|
import { listAdminDomains, type DomainInfo } from '../../lib/admin-domain'
|
|
8
8
|
import { ADMIN_TARGET_OPTIONS, type AdminTargetCommandOpts } from '../../lib/admin-target'
|
|
9
|
+
import { fetchDomainPublication } from '../../lib/domain-publication'
|
|
9
10
|
import { fatal, withSpinner } from '../../lib/log'
|
|
10
11
|
import { isMachine, presentList } from '../../lib/output'
|
|
11
12
|
|
|
@@ -23,7 +24,7 @@ type ListOpts = KernelCommandOpts &
|
|
|
23
24
|
/** A catalog entry, optionally enriched with a live `--check` probe. */
|
|
24
25
|
export type DomainRow = DomainInfo & {
|
|
25
26
|
reachable?: boolean
|
|
26
|
-
|
|
27
|
+
schemaRevision?: string
|
|
27
28
|
checkError?: string | null
|
|
28
29
|
}
|
|
29
30
|
|
|
@@ -73,8 +74,8 @@ Behavior:
|
|
|
73
74
|
one install URL per line (pipeable into \`domain install\`); --count prints
|
|
74
75
|
only the number. --default-only keeps the install-by-default entries (what
|
|
75
76
|
every new instance gets via alphaCreate). --check probes each published URL's
|
|
76
|
-
|
|
77
|
-
machine output).
|
|
77
|
+
canonical Publication and adds a live/unreachable STATUS column
|
|
78
|
+
(+ reachable/schemaRevision in machine output).
|
|
78
79
|
|
|
79
80
|
The admin kernel is selected like every admin op — the configured default,
|
|
80
81
|
or --admin <bookmark> / --admin-url <url>.
|
|
@@ -87,7 +88,10 @@ Examples:
|
|
|
87
88
|
`,
|
|
88
89
|
options: [
|
|
89
90
|
...ADMIN_TARGET_OPTIONS,
|
|
90
|
-
{
|
|
91
|
+
{
|
|
92
|
+
flags: '--check',
|
|
93
|
+
description: "Probe each domain's canonical Publication + schema revision",
|
|
94
|
+
},
|
|
91
95
|
{ flags: '--default-only', description: 'Only show install-by-default domains' },
|
|
92
96
|
{ flags: '-q, --quiet', description: 'One install URL per line (unix-pipeable)' },
|
|
93
97
|
{ flags: '--count', description: 'Print only the number of published domains' },
|
|
@@ -103,7 +107,7 @@ Examples:
|
|
|
103
107
|
const filtered = opts.defaultOnly ? list.filter((d) => d.installByDefault) : list
|
|
104
108
|
filtered.sort(byDefaultThenName)
|
|
105
109
|
if (!opts.check) return filtered as DomainRow[]
|
|
106
|
-
// Reachability is a direct client-side
|
|
110
|
+
// Reachability is a direct client-side Publication fetch per entry, in
|
|
107
111
|
// parallel — no admin round-trip, and version-independent of the
|
|
108
112
|
// admin worker (mirrors `domain install`'s probeDeclaredOrigin).
|
|
109
113
|
return Promise.all(filtered.map(probe))
|
|
@@ -129,19 +133,20 @@ export function byDefaultThenName(a: DomainInfo, b: DomainInfo): number {
|
|
|
129
133
|
|
|
130
134
|
/**
|
|
131
135
|
* Enrich one entry with a reachability probe: fetch the published worker's
|
|
132
|
-
*
|
|
133
|
-
*
|
|
136
|
+
* canonical Publication and read its schema revision. A dead or missing URL is
|
|
137
|
+
* itself a result, not a throw.
|
|
134
138
|
*/
|
|
135
|
-
async function probe(d: DomainInfo): Promise<DomainRow> {
|
|
139
|
+
export async function probe(d: DomainInfo): Promise<DomainRow> {
|
|
136
140
|
if (!d.url) return { ...d, reachable: false, checkError: 'no url published' }
|
|
137
141
|
try {
|
|
138
|
-
const
|
|
139
|
-
if (
|
|
140
|
-
|
|
142
|
+
const deployed = await fetchDomainPublication(d.url, AbortSignal.timeout(10_000))
|
|
143
|
+
if (deployed.origin !== d.origin) {
|
|
144
|
+
throw new Error(`Domain origin mismatch: deployed=${deployed.origin} expected=${d.origin}`)
|
|
145
|
+
}
|
|
141
146
|
return {
|
|
142
147
|
...d,
|
|
143
148
|
reachable: true,
|
|
144
|
-
|
|
149
|
+
schemaRevision: deployed.schema.revision,
|
|
145
150
|
checkError: null,
|
|
146
151
|
}
|
|
147
152
|
} catch (err) {
|
package/src/commands/studio.ts
CHANGED
|
@@ -25,6 +25,23 @@ type StudioOpts = RawOutputOpts & {
|
|
|
25
25
|
const STUDIO_PORT_BASE = 4319
|
|
26
26
|
const VITE_PORT_BASE = 5273
|
|
27
27
|
const PORT_SPAN = 20
|
|
28
|
+
const STUDIO_CLI_DESCRIPTOR_ENV = 'DOMAIN_STUDIO_CLI_DESCRIPTOR'
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Describe this exact CLI invocation for the Studio server. A source/published
|
|
32
|
+
* script needs its entrypoint after node/bun; a Bun-compiled executable is
|
|
33
|
+
* self-contained and therefore has no prefix argument.
|
|
34
|
+
*/
|
|
35
|
+
export function encodeStudioCliDescriptor(
|
|
36
|
+
executable = process.execPath,
|
|
37
|
+
entry = process.argv[1],
|
|
38
|
+
): string {
|
|
39
|
+
const args =
|
|
40
|
+
entry && entry !== executable && !entry.startsWith('/$bunfs') && existsSync(entry)
|
|
41
|
+
? [realpathSync(entry)]
|
|
42
|
+
: []
|
|
43
|
+
return JSON.stringify({ version: 1, executable, args })
|
|
44
|
+
}
|
|
28
45
|
|
|
29
46
|
/**
|
|
30
47
|
* Locate the studio package shipped with / alongside the CLI. Anchored to the
|
|
@@ -217,6 +234,7 @@ Examples:
|
|
|
217
234
|
log.dim(` using ASTRALE_STUDIO_DIR=${studioDir}`)
|
|
218
235
|
}
|
|
219
236
|
await ensureBun()
|
|
237
|
+
const cliDescriptor = encodeStudioCliDescriptor()
|
|
220
238
|
|
|
221
239
|
// Default to PROD — serve the prebuilt client. It always renders, is what a
|
|
222
240
|
// published/global install runs, and avoids dev's heavy source-module graph.
|
|
@@ -342,6 +360,7 @@ Examples:
|
|
|
342
360
|
VITE_URL: `http://127.0.0.1:${vitePort}`,
|
|
343
361
|
PORT: String(studioPort),
|
|
344
362
|
DOMAIN_STUDIO_HOST: '127.0.0.1',
|
|
363
|
+
[STUDIO_CLI_DESCRIPTOR_ENV]: cliDescriptor,
|
|
345
364
|
...(opts.harness ? { DOMAIN_STUDIO_HARNESS: opts.harness } : {}),
|
|
346
365
|
},
|
|
347
366
|
},
|
|
@@ -367,6 +386,7 @@ Examples:
|
|
|
367
386
|
DOMAIN_STUDIO_DIST: dist,
|
|
368
387
|
PORT: String(studioPort),
|
|
369
388
|
DOMAIN_STUDIO_HOST: '127.0.0.1',
|
|
389
|
+
[STUDIO_CLI_DESCRIPTOR_ENV]: cliDescriptor,
|
|
370
390
|
...(opts.harness ? { DOMAIN_STUDIO_HARNESS: opts.harness } : {}),
|
|
371
391
|
},
|
|
372
392
|
},
|
package/src/commands/update.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
type SdkOutdated,
|
|
13
13
|
} from '../lib/sdk-deps'
|
|
14
14
|
import { ASTRALE_CLI_SKILL, detectSkill, installSkills, SKILL_INSTALL_HINT } from '../lib/skills'
|
|
15
|
-
import { updateAstrale } from '../lib/update'
|
|
15
|
+
import { DEFAULT_UPDATE_CHANNEL, updateAstrale } from '../lib/update'
|
|
16
16
|
|
|
17
17
|
type UpdateOpts = RawOutputOpts & {
|
|
18
18
|
check?: boolean
|
|
@@ -130,11 +130,12 @@ export async function cliStale(
|
|
|
130
130
|
dependencies: CliStaleDependencies = CLI_STALE_DEPENDENCIES,
|
|
131
131
|
): Promise<StaleReport['cli']> {
|
|
132
132
|
const running = pkg.version
|
|
133
|
+
const target = { ...opts, channel: opts.channel ?? DEFAULT_UPDATE_CHANNEL }
|
|
133
134
|
try {
|
|
134
135
|
const r = await dependencies.update({
|
|
135
136
|
check: true,
|
|
136
|
-
channel:
|
|
137
|
-
version:
|
|
137
|
+
channel: target.channel,
|
|
138
|
+
version: target.version,
|
|
138
139
|
currentVersion: running,
|
|
139
140
|
})
|
|
140
141
|
if (r.status === 'updated') {
|
|
@@ -154,7 +155,7 @@ export async function cliStale(
|
|
|
154
155
|
channel: r.channel,
|
|
155
156
|
}
|
|
156
157
|
} catch {
|
|
157
|
-
const latest = await dependencies.fetchPackageVersion(
|
|
158
|
+
const latest = await dependencies.fetchPackageVersion(target).catch(() => undefined)
|
|
158
159
|
return {
|
|
159
160
|
stale: latest !== undefined && latest !== running,
|
|
160
161
|
managed: true,
|
|
@@ -167,7 +168,8 @@ export async function cliStale(
|
|
|
167
168
|
export async function fetchNpmTargetVersion(
|
|
168
169
|
opts: Pick<UpdateOpts, 'channel' | 'version'>,
|
|
169
170
|
): Promise<string> {
|
|
170
|
-
const
|
|
171
|
+
const channel = opts.channel ?? DEFAULT_UPDATE_CHANNEL
|
|
172
|
+
const target = opts.version ?? (channel === 'stable' ? 'latest' : channel)
|
|
171
173
|
const response = await fetch(`https://registry.npmjs.org/@astrale-os/cli/${target}`)
|
|
172
174
|
if (!response.ok) throw new Error(`npm registry HTTP ${response.status}`)
|
|
173
175
|
const body: unknown = await response.json()
|
|
@@ -197,6 +199,7 @@ export default {
|
|
|
197
199
|
{
|
|
198
200
|
flags: '--channel <name>',
|
|
199
201
|
description: 'Update from a release channel (alpha, beta, rc, canary, stable)',
|
|
202
|
+
default: DEFAULT_UPDATE_CHANNEL,
|
|
200
203
|
},
|
|
201
204
|
{ flags: '--version <version>', description: 'Update to an exact version tag' },
|
|
202
205
|
{ flags: '--no-skills', description: 'Skip refreshing the astrale agent skills' },
|
|
@@ -221,6 +224,7 @@ Behavior:
|
|
|
221
224
|
the lockfile, honoring your registry + supply-chain age policy; run "pnpm
|
|
222
225
|
install" to materialize).
|
|
223
226
|
|
|
227
|
+
The default release channel is beta; --channel overrides it for one run.
|
|
224
228
|
--check is a dry run (binary + SDK deps; exit 10 if anything is available) and
|
|
225
229
|
never writes. With --json it emits a unified staleness report
|
|
226
230
|
({ stale, cli, sdk }) for tooling — non-throwing, skills omitted (they ride
|
|
@@ -5,6 +5,10 @@ import { formatKernelError, functionInputIssues, schemaUpgradeHint } from '../er
|
|
|
5
5
|
|
|
6
6
|
const CONFLICT = 4001
|
|
7
7
|
const VALIDATION = 1003
|
|
8
|
+
const TEST_INVOCATION = {
|
|
9
|
+
source: 'https://kernel.test',
|
|
10
|
+
id: 'connection-errors',
|
|
11
|
+
} as ConstructorParameters<typeof ResponseError>[2]
|
|
8
12
|
|
|
9
13
|
describe('formatKernelError', () => {
|
|
10
14
|
/** @evidence TEST-CLI-CONNECTION-MAPS-TYPED-TRANSPORT */
|
|
@@ -87,7 +91,7 @@ describe('formatKernelError', () => {
|
|
|
87
91
|
}) as typeof process.stderr.write
|
|
88
92
|
try {
|
|
89
93
|
await formatKernelError(
|
|
90
|
-
new ResponseError(CONFLICT, 'Schema change requires migration.', {
|
|
94
|
+
new ResponseError(CONFLICT, 'Schema change requires migration.', TEST_INVOCATION, {
|
|
91
95
|
code: 'DATA_MIGRATION_REQUIRED',
|
|
92
96
|
details: {
|
|
93
97
|
requirements: [
|
|
@@ -138,7 +142,7 @@ describe('formatKernelError', () => {
|
|
|
138
142
|
}) as typeof process.stderr.write
|
|
139
143
|
try {
|
|
140
144
|
await formatKernelError(
|
|
141
|
-
new ResponseError(CONFLICT, 'Schema change requires migration.', {
|
|
145
|
+
new ResponseError(CONFLICT, 'Schema change requires migration.', TEST_INVOCATION, {
|
|
142
146
|
code: 'DATA_MIGRATION_REQUIRED',
|
|
143
147
|
details: {
|
|
144
148
|
requirements: [
|
|
@@ -193,7 +197,7 @@ describe('formatKernelError', () => {
|
|
|
193
197
|
console.log = (...values: unknown[]) => hints.push(values.map(String).join(' '))
|
|
194
198
|
try {
|
|
195
199
|
await formatKernelError(
|
|
196
|
-
new ResponseError(CONFLICT, 'Schema change requires migration.', {
|
|
200
|
+
new ResponseError(CONFLICT, 'Schema change requires migration.', TEST_INVOCATION, {
|
|
197
201
|
code: 'DATA_MIGRATION_REQUIRED',
|
|
198
202
|
details: {
|
|
199
203
|
requirements: [
|
|
@@ -254,7 +258,7 @@ describe('formatKernelError', () => {
|
|
|
254
258
|
console.log = (...values: unknown[]) => lines.push(values.join(' '))
|
|
255
259
|
try {
|
|
256
260
|
await formatKernelError(
|
|
257
|
-
new ResponseError(VALIDATION, 'Function input is invalid.', {
|
|
261
|
+
new ResponseError(VALIDATION, 'Function input is invalid.', TEST_INVOCATION, {
|
|
258
262
|
code: 'FUNCTION_INPUT_INVALID',
|
|
259
263
|
details: {
|
|
260
264
|
issues: [
|
|
@@ -279,7 +283,7 @@ describe('formatKernelError', () => {
|
|
|
279
283
|
false,
|
|
280
284
|
)
|
|
281
285
|
await formatKernelError(
|
|
282
|
-
new ResponseError(VALIDATION, 'Query input is invalid.', {
|
|
286
|
+
new ResponseError(VALIDATION, 'Query input is invalid.', TEST_INVOCATION, {
|
|
283
287
|
code: 'QUERY_INPUT_INVALID',
|
|
284
288
|
details: {
|
|
285
289
|
phase: 'plan',
|
|
@@ -310,7 +314,7 @@ describe('formatKernelError', () => {
|
|
|
310
314
|
}) as typeof process.stderr.write
|
|
311
315
|
try {
|
|
312
316
|
await formatKernelError(
|
|
313
|
-
new ResponseError(5001, 'Schema operation is not supported.', {
|
|
317
|
+
new ResponseError(5001, 'Schema operation is not supported.', TEST_INVOCATION, {
|
|
314
318
|
code: 'SCHEMA_UPGRADE_INCOMPATIBLE',
|
|
315
319
|
details: {
|
|
316
320
|
phase: 'upgrade',
|
|
@@ -358,7 +362,7 @@ describe('formatKernelError', () => {
|
|
|
358
362
|
console.log = (...values: unknown[]) => details.push(values.map(String).join(' '))
|
|
359
363
|
try {
|
|
360
364
|
await formatKernelError(
|
|
361
|
-
new ResponseError(5001, 'Schema operation is not supported.', {
|
|
365
|
+
new ResponseError(5001, 'Schema operation is not supported.', TEST_INVOCATION, {
|
|
362
366
|
code: 'SCHEMA_UPGRADE_INCOMPATIBLE',
|
|
363
367
|
details: {
|
|
364
368
|
phase: 'upgrade',
|
|
@@ -391,7 +395,7 @@ describe('formatKernelError', () => {
|
|
|
391
395
|
}) as typeof process.stderr.write
|
|
392
396
|
try {
|
|
393
397
|
await formatKernelError(
|
|
394
|
-
new ResponseError(1003, 'Function input is invalid.', {
|
|
398
|
+
new ResponseError(1003, 'Function input is invalid.', TEST_INVOCATION, {
|
|
395
399
|
code: 'SCHEMA_DOMAIN_ADDRESS_NOT_PUBLIC',
|
|
396
400
|
details: { phase: 'publication', requiredScope: 'public' },
|
|
397
401
|
}),
|
|
@@ -421,7 +425,7 @@ describe('formatKernelError', () => {
|
|
|
421
425
|
console.log = (...values: unknown[]) => hints.push(values.map(String).join(' '))
|
|
422
426
|
try {
|
|
423
427
|
await formatKernelError(
|
|
424
|
-
new ResponseError(1003, 'Function input is invalid.', {
|
|
428
|
+
new ResponseError(1003, 'Function input is invalid.', TEST_INVOCATION, {
|
|
425
429
|
code: 'SCHEMA_DOMAIN_ADDRESS_NOT_PUBLIC',
|
|
426
430
|
details: { phase: 'publication', requiredScope: 'public' },
|
|
427
431
|
}),
|
|
@@ -448,7 +452,7 @@ describe('formatKernelError', () => {
|
|
|
448
452
|
}) as typeof process.stderr.write
|
|
449
453
|
try {
|
|
450
454
|
await formatKernelError(
|
|
451
|
-
new ResponseError(1003, 'Function input is invalid.', {
|
|
455
|
+
new ResponseError(1003, 'Function input is invalid.', TEST_INVOCATION, {
|
|
452
456
|
code: 'FUNCTION_INPUT_INVALID',
|
|
453
457
|
details: {
|
|
454
458
|
issues: [
|
|
@@ -495,7 +499,7 @@ describe('formatKernelError', () => {
|
|
|
495
499
|
console.log = (...values: unknown[]) => details.push(values.map(String).join(' '))
|
|
496
500
|
try {
|
|
497
501
|
await formatKernelError(
|
|
498
|
-
new ResponseError(1003, 'Function input is invalid.', {
|
|
502
|
+
new ResponseError(1003, 'Function input is invalid.', TEST_INVOCATION, {
|
|
499
503
|
code: 'FUNCTION_INPUT_INVALID',
|
|
500
504
|
details: {
|
|
501
505
|
issues: [
|
|
@@ -530,7 +534,7 @@ describe('formatKernelError', () => {
|
|
|
530
534
|
}) as typeof process.stderr.write
|
|
531
535
|
try {
|
|
532
536
|
await formatKernelError(
|
|
533
|
-
new ResponseError(1003, 'Function input is invalid.', {
|
|
537
|
+
new ResponseError(1003, 'Function input is invalid.', TEST_INVOCATION, {
|
|
534
538
|
code: 'FUNCTION_INPUT_INVALID',
|
|
535
539
|
details: { issues: [{ code: 'INVALID_FORMAT', path: '/customer/email' }] },
|
|
536
540
|
}),
|
|
@@ -47,7 +47,7 @@ describe('Domain token exchange', () => {
|
|
|
47
47
|
observed.push({ url, init, body })
|
|
48
48
|
const index = observed.filter((entry) => entry.url === INVOCATION).length
|
|
49
49
|
return invocationResponse(
|
|
50
|
-
body.
|
|
50
|
+
body.requestId,
|
|
51
51
|
index === 1
|
|
52
52
|
? { id: 'user-1' }
|
|
53
53
|
: index === 2
|
|
@@ -121,7 +121,7 @@ describe('Domain token exchange', () => {
|
|
|
121
121
|
if (url === INVOCATION) {
|
|
122
122
|
const body = JSON.parse(await new Response(init?.body).text()) as Record<string, any>
|
|
123
123
|
return invocationResponse(
|
|
124
|
-
body.
|
|
124
|
+
body.requestId,
|
|
125
125
|
body.call.input && Object.keys(body.call.input).length === 0
|
|
126
126
|
? { id: 'user-1' }
|
|
127
127
|
: 'kernel-destination-envelope',
|
|
@@ -201,7 +201,7 @@ function exchangeFetch(
|
|
|
201
201
|
if (url === INVOCATION) {
|
|
202
202
|
const body = JSON.parse(await new Response(init?.body).text()) as Record<string, any>
|
|
203
203
|
return invocationResponse(
|
|
204
|
-
body.
|
|
204
|
+
body.requestId,
|
|
205
205
|
body.call.input && Object.keys(body.call.input).length === 0
|
|
206
206
|
? { id: 'user-1' }
|
|
207
207
|
: 'kernel-destination-envelope',
|
|
@@ -233,8 +233,9 @@ function configuration(enabled: boolean) {
|
|
|
233
233
|
}
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
-
function invocationResponse(
|
|
237
|
-
|
|
236
|
+
function invocationResponse(requestId: unknown, result: unknown, contentType: string): Response {
|
|
237
|
+
const invocation = { source: KERNEL, id: `exchange-${String(requestId)}` }
|
|
238
|
+
return new Response(JSON.stringify({ requestId, invocation, result }), {
|
|
238
239
|
headers: { 'content-type': contentType, 'cache-control': 'no-store' },
|
|
239
240
|
})
|
|
240
241
|
}
|
|
@@ -33,12 +33,13 @@ export function createExchangeCredentialResolver(
|
|
|
33
33
|
const client = new Client({ url: `${kernelIssuer}/invoke`, fetch, timeoutMs })
|
|
34
34
|
try {
|
|
35
35
|
const authenticated = client.as(sourceToken)
|
|
36
|
-
const auth = createAuth((path, input, options) =>
|
|
37
|
-
authenticated.call(call(path, input), {
|
|
36
|
+
const auth = createAuth(async (path, input, options) => {
|
|
37
|
+
const result = await authenticated.call(call(path, input), {
|
|
38
38
|
...options,
|
|
39
39
|
delegate: { ttlSeconds: delegationTtlSeconds },
|
|
40
|
-
})
|
|
41
|
-
|
|
40
|
+
})
|
|
41
|
+
return result.value
|
|
42
|
+
})
|
|
42
43
|
const user = await auth.whoami({ signal })
|
|
43
44
|
const key = Object.freeze({
|
|
44
45
|
kernelIssuer,
|