@astrale-os/cli 0.4.0-alpha.13
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/.check-workspace.cjs +40 -0
- package/README.md +151 -0
- package/dist/astrale.js +59749 -0
- package/package.json +90 -0
- package/src/command.ts +40 -0
- package/src/commands/__tests__/admin-instance.test.ts +73 -0
- package/src/commands/__tests__/auth-login.test.ts +178 -0
- package/src/commands/__tests__/auth-token.test.ts +223 -0
- package/src/commands/__tests__/call.test.ts +72 -0
- package/src/commands/__tests__/domain-list.test.ts +74 -0
- package/src/commands/__tests__/help-contract.test.ts +136 -0
- package/src/commands/__tests__/install-identity-override.test.ts +65 -0
- package/src/commands/__tests__/instance-bookmark.test.ts +101 -0
- package/src/commands/__tests__/instance-create-hosts.test.ts +29 -0
- package/src/commands/__tests__/instance-list-rows.test.ts +63 -0
- package/src/commands/__tests__/logs.test.ts +117 -0
- package/src/commands/__tests__/ls.test.ts +25 -0
- package/src/commands/__tests__/setup-plan.test.ts +61 -0
- package/src/commands/admin/status.ts +61 -0
- package/src/commands/admin/use.ts +77 -0
- package/src/commands/auth/login.ts +82 -0
- package/src/commands/auth/logout.ts +50 -0
- package/src/commands/auth/status.ts +86 -0
- package/src/commands/auth/token.ts +162 -0
- package/src/commands/browser.ts +207 -0
- package/src/commands/call.ts +300 -0
- package/src/commands/describe.ts +182 -0
- package/src/commands/domain/install.ts +420 -0
- package/src/commands/domain/list.ts +154 -0
- package/src/commands/domain/publish.ts +155 -0
- package/src/commands/get.ts +60 -0
- package/src/commands/identity/create.ts +26 -0
- package/src/commands/identity/delete.ts +18 -0
- package/src/commands/identity/export.ts +66 -0
- package/src/commands/identity/import.ts +101 -0
- package/src/commands/identity/list.ts +56 -0
- package/src/commands/identity/register.ts +170 -0
- package/src/commands/identity/sync.ts +32 -0
- package/src/commands/identity/unsync.ts +24 -0
- package/src/commands/identity/use.ts +18 -0
- package/src/commands/identity/whoami.ts +34 -0
- package/src/commands/idp/add.ts +150 -0
- package/src/commands/idp/list.ts +57 -0
- package/src/commands/idp/refresh.ts +38 -0
- package/src/commands/idp/remove.ts +36 -0
- package/src/commands/idp/show.ts +29 -0
- package/src/commands/instance/active.ts +64 -0
- package/src/commands/instance/bookmark.ts +72 -0
- package/src/commands/instance/create.ts +69 -0
- package/src/commands/instance/delete.ts +72 -0
- package/src/commands/instance/forget.ts +26 -0
- package/src/commands/instance/list.ts +149 -0
- package/src/commands/instance/status.ts +42 -0
- package/src/commands/instance/use.ts +210 -0
- package/src/commands/logs.ts +347 -0
- package/src/commands/ls.ts +229 -0
- package/src/commands/query.ts +32 -0
- package/src/commands/setup.ts +54 -0
- package/src/commands/status.ts +60 -0
- package/src/commands/studio.ts +401 -0
- package/src/commands/token.ts +77 -0
- package/src/commands/update.ts +267 -0
- package/src/commands/use.ts +87 -0
- package/src/errors.ts +65 -0
- package/src/kernel/__tests__/auth.test.ts +77 -0
- package/src/kernel/__tests__/errors.test.ts +43 -0
- package/src/kernel/__tests__/remote-routing.test.ts +70 -0
- package/src/kernel/auth.ts +234 -0
- package/src/kernel/ca-fetch.ts +119 -0
- package/src/kernel/client.ts +191 -0
- package/src/kernel/errors.ts +280 -0
- package/src/kernel/expand.ts +217 -0
- package/src/kernel/index.ts +14 -0
- package/src/kernel/options.ts +22 -0
- package/src/kernel/remote-routing.ts +88 -0
- package/src/kernel/run.ts +63 -0
- package/src/kernel/types.ts +14 -0
- package/src/lib/__tests__/admin-target.test.ts +112 -0
- package/src/lib/__tests__/binary.test.ts +56 -0
- package/src/lib/__tests__/command-dx.test.ts +58 -0
- package/src/lib/__tests__/concurrency.test.ts +62 -0
- package/src/lib/__tests__/config.test.ts +53 -0
- package/src/lib/__tests__/design.test.ts +99 -0
- package/src/lib/__tests__/domain-identity.test.ts +60 -0
- package/src/lib/__tests__/format.test.ts +22 -0
- package/src/lib/__tests__/fs-atomic.test.ts +104 -0
- package/src/lib/__tests__/identity.test.ts +79 -0
- package/src/lib/__tests__/idp-session.driver.ts +53 -0
- package/src/lib/__tests__/idp-session.test.ts +357 -0
- package/src/lib/__tests__/idp.test.ts +385 -0
- package/src/lib/__tests__/instance-candidates.test.ts +73 -0
- package/src/lib/__tests__/instance-target.test.ts +183 -0
- package/src/lib/__tests__/instance.test.ts +136 -0
- package/src/lib/__tests__/keys.test.ts +129 -0
- package/src/lib/__tests__/local-status.test.ts +202 -0
- package/src/lib/__tests__/output.test.ts +150 -0
- package/src/lib/__tests__/panel.test.ts +40 -0
- package/src/lib/__tests__/port.test.ts +44 -0
- package/src/lib/__tests__/prompt.test.ts +25 -0
- package/src/lib/__tests__/sdk-deps.test.ts +68 -0
- package/src/lib/__tests__/self.test.ts +272 -0
- package/src/lib/__tests__/studio-server-deps.test.ts +74 -0
- package/src/lib/__tests__/table.test.ts +53 -0
- package/src/lib/__tests__/update.test.ts +246 -0
- package/src/lib/__tests__/use-target.test.ts +56 -0
- package/src/lib/__tests__/validation.test.ts +34 -0
- package/src/lib/admin-domain.ts +25 -0
- package/src/lib/admin-instance.ts +26 -0
- package/src/lib/admin-target.ts +217 -0
- package/src/lib/binary.ts +131 -0
- package/src/lib/browser.ts +150 -0
- package/src/lib/command-dx.ts +161 -0
- package/src/lib/concurrency.ts +31 -0
- package/src/lib/config.ts +45 -0
- package/src/lib/domain-identity.ts +49 -0
- package/src/lib/env.ts +49 -0
- package/src/lib/format.ts +4 -0
- package/src/lib/fs-atomic.ts +126 -0
- package/src/lib/identity.ts +256 -0
- package/src/lib/idp-session.ts +134 -0
- package/src/lib/idp.ts +876 -0
- package/src/lib/instance-candidates.ts +49 -0
- package/src/lib/instance-target.ts +182 -0
- package/src/lib/instance.ts +395 -0
- package/src/lib/keys.ts +294 -0
- package/src/lib/local-status.ts +152 -0
- package/src/lib/log.ts +116 -0
- package/src/lib/login-flow.ts +164 -0
- package/src/lib/meta.ts +86 -0
- package/src/lib/output.ts +222 -0
- package/src/lib/panel.ts +61 -0
- package/src/lib/paths.ts +11 -0
- package/src/lib/port.ts +41 -0
- package/src/lib/proc.ts +82 -0
- package/src/lib/prompt.ts +136 -0
- package/src/lib/provision-instance.ts +170 -0
- package/src/lib/sdk-deps.ts +104 -0
- package/src/lib/self.ts +166 -0
- package/src/lib/skills.ts +171 -0
- package/src/lib/table.ts +62 -0
- package/src/lib/update.ts +315 -0
- package/src/lib/use-target.ts +24 -0
- package/src/lib/validation.ts +59 -0
- package/src/program.ts +200 -0
- package/src/registry.ts +59 -0
- package/src/setup/__tests__/util.test.ts +29 -0
- package/src/setup/engine.ts +83 -0
- package/src/setup/render.ts +109 -0
- package/src/setup/steps/admin.ts +78 -0
- package/src/setup/steps/agent-browser.ts +81 -0
- package/src/setup/steps/auth.ts +54 -0
- package/src/setup/steps/domain.ts +68 -0
- package/src/setup/steps/index.ts +18 -0
- package/src/setup/steps/instance.ts +119 -0
- package/src/setup/steps/skills-bridge.ts +70 -0
- package/src/setup/steps/skills.ts +59 -0
- package/src/setup/types.ts +61 -0
- package/src/setup/util.ts +34 -0
- package/src/test-utils.ts +18 -0
- package/studio/client/dist/assets/index-DOwzZAEK.css +1 -0
- package/studio/client/dist/assets/index-wtU0Zxhy.js +183 -0
- package/studio/client/dist/index.html +13 -0
- package/studio/package.json +62 -0
- package/studio/server/agent/ask.ts +68 -0
- package/studio/server/agent/bridge-mcp.ts +182 -0
- package/studio/server/agent/bridge.ts +188 -0
- package/studio/server/agent/claude.ts +666 -0
- package/studio/server/agent/mock.ts +186 -0
- package/studio/server/agent/prompt.ts +202 -0
- package/studio/server/agent/registry.ts +29 -0
- package/studio/server/agent/runner.ts +484 -0
- package/studio/server/agent/schema-map.ts +112 -0
- package/studio/server/agent/types.ts +120 -0
- package/studio/server/api.ts +574 -0
- package/studio/server/cache.ts +138 -0
- package/studio/server/detect.ts +81 -0
- package/studio/server/domain.ts +70 -0
- package/studio/server/index.ts +136 -0
- package/studio/server/introspect/anatomy-extras.ts +398 -0
- package/studio/server/introspect/anatomy.ts +108 -0
- package/studio/server/introspect/bundle.ts +57 -0
- package/studio/server/introspect/core-extractor.ts +119 -0
- package/studio/server/introspect/core.ts +44 -0
- package/studio/server/introspect/diff.ts +133 -0
- package/studio/server/introspect/extractor.ts +102 -0
- package/studio/server/introspect/hash.ts +21 -0
- package/studio/server/introspect/overlay-tsmorph.ts +874 -0
- package/studio/server/introspect/overlay.ts +57 -0
- package/studio/server/introspect/runtime.ts +99 -0
- package/studio/server/introspect/schema-refs.ts +46 -0
- package/studio/server/lifecycle.ts +38 -0
- package/studio/server/sse.ts +57 -0
- package/studio/server/state/baseline.ts +211 -0
- package/studio/server/state/catalog.ts +117 -0
- package/studio/server/state/comments.ts +321 -0
- package/studio/server/state/context.ts +167 -0
- package/studio/server/state/copy.ts +156 -0
- package/studio/server/state/create.ts +156 -0
- package/studio/server/state/documents.ts +70 -0
- package/studio/server/state/env.ts +161 -0
- package/studio/server/state/git.ts +75 -0
- package/studio/server/state/handoff.ts +55 -0
- package/studio/server/state/harness-gateway.ts +181 -0
- package/studio/server/state/harness-token.ts +0 -0
- package/studio/server/state/instance.ts +244 -0
- package/studio/server/state/integrations.ts +55 -0
- package/studio/server/state/layout.ts +55 -0
- package/studio/server/state/settings.ts +39 -0
- package/studio/server/state/store.ts +97 -0
- package/studio/server/state/updates.ts +63 -0
- package/studio/server/state/usage.ts +37 -0
- package/studio/server/state/views.ts +138 -0
- package/studio/server/state/visibility.ts +33 -0
- package/studio/server/watch.ts +81 -0
- package/studio/server/workspace-state.ts +26 -0
- package/studio/server/workspace-watch.ts +101 -0
- package/studio/shared/types.ts +873 -0
- package/studio/tsconfig.json +23 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
import net from 'node:net'
|
|
3
|
+
|
|
4
|
+
import { findFreePort, portFree } from '../port'
|
|
5
|
+
|
|
6
|
+
function listen(port: number): Promise<net.Server> {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
const s = net.createServer()
|
|
9
|
+
s.once('error', reject)
|
|
10
|
+
s.listen({ port, host: '127.0.0.1', exclusive: true }, () => resolve(s))
|
|
11
|
+
})
|
|
12
|
+
}
|
|
13
|
+
const close = (s: net.Server) => new Promise<void>((r) => s.close(() => r()))
|
|
14
|
+
|
|
15
|
+
describe('port', () => {
|
|
16
|
+
it('portFree: true for an unbound port, false once it is bound', async () => {
|
|
17
|
+
const p = await findFreePort(48000, 200)
|
|
18
|
+
expect(p).not.toBeNull()
|
|
19
|
+
expect(await portFree(p as number)).toBe(true)
|
|
20
|
+
const srv = await listen(p as number)
|
|
21
|
+
try {
|
|
22
|
+
expect(await portFree(p as number)).toBe(false)
|
|
23
|
+
} finally {
|
|
24
|
+
await close(srv)
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('findFreePort: skips a busy port and returns a later free one', async () => {
|
|
29
|
+
const base = await findFreePort(48000, 200)
|
|
30
|
+
expect(base).not.toBeNull()
|
|
31
|
+
const srv = await listen(base as number)
|
|
32
|
+
try {
|
|
33
|
+
const next = await findFreePort(base as number, 10)
|
|
34
|
+
expect(next).not.toBeNull()
|
|
35
|
+
expect(next as number).toBeGreaterThan(base as number)
|
|
36
|
+
} finally {
|
|
37
|
+
await close(srv)
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('findFreePort: returns null when the band is empty (span 0)', async () => {
|
|
42
|
+
expect(await findFreePort(48000, 0)).toBeNull()
|
|
43
|
+
})
|
|
44
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import { promptText } from '../prompt'
|
|
4
|
+
|
|
5
|
+
// The no-TTY branch IS the LLM / CI / piped case. promptText must resolve to a
|
|
6
|
+
// default (never await a stdin read) there, so `astrale domain publish` fails
|
|
7
|
+
// fast on its required-flag error instead of hanging. Force isTTY=false so this
|
|
8
|
+
// holds even when the suite is run from an interactive terminal.
|
|
9
|
+
describe('promptText — non-TTY never blocks on input', () => {
|
|
10
|
+
const original = process.stdin.isTTY
|
|
11
|
+
beforeAll(() => {
|
|
12
|
+
;(process.stdin as { isTTY?: boolean }).isTTY = false
|
|
13
|
+
})
|
|
14
|
+
afterAll(() => {
|
|
15
|
+
;(process.stdin as { isTTY?: boolean }).isTTY = original
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('returns the provided default', async () => {
|
|
19
|
+
expect(await promptText('Domain origin', { default: 'crm.acme.dev' })).toBe('crm.acme.dev')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('returns undefined with no default (caller then hits its required-flag error)', async () => {
|
|
23
|
+
expect(await promptText('Published worker URL')).toBeUndefined()
|
|
24
|
+
})
|
|
25
|
+
})
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
|
|
6
|
+
import { foreignPackageManager, inDomainProject, parseSdkOutdated } from '../sdk-deps'
|
|
7
|
+
|
|
8
|
+
describe('parseSdkOutdated', () => {
|
|
9
|
+
test('keeps only deps with a strictly newer latest; uses wanted when current is absent', () => {
|
|
10
|
+
const json = JSON.stringify({
|
|
11
|
+
'@astrale-os/sdk': { current: '0.1.5', wanted: '0.1.5', latest: '0.1.9' }, // behind
|
|
12
|
+
'@astrale-os/kernel-dsl': { current: '0.1.2', wanted: '0.1.2', latest: '0.1.2' }, // current
|
|
13
|
+
'@astrale-os/shell': { wanted: '0.1.0', latest: '0.1.1' }, // no `current` (not installed)
|
|
14
|
+
})
|
|
15
|
+
expect(parseSdkOutdated(json)).toEqual([
|
|
16
|
+
{ pkg: '@astrale-os/sdk', current: '0.1.5', latest: '0.1.9' },
|
|
17
|
+
{ pkg: '@astrale-os/shell', current: '0.1.0', latest: '0.1.1' },
|
|
18
|
+
])
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('empty / non-JSON / no-latest input yields no upgrades (never throws)', () => {
|
|
22
|
+
expect(parseSdkOutdated('')).toEqual([])
|
|
23
|
+
expect(parseSdkOutdated('{}')).toEqual([])
|
|
24
|
+
expect(parseSdkOutdated('not json')).toEqual([])
|
|
25
|
+
expect(parseSdkOutdated(JSON.stringify({ '@astrale-os/sdk': { current: '0.1.9' } }))).toEqual(
|
|
26
|
+
[],
|
|
27
|
+
)
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
describe('inDomainProject', () => {
|
|
32
|
+
let tmp: string
|
|
33
|
+
afterEach(async () => {
|
|
34
|
+
if (tmp) await rm(tmp, { recursive: true, force: true })
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('true only where astrale.config.ts exists', async () => {
|
|
38
|
+
tmp = await mkdtemp(join(tmpdir(), 'sdk-deps-'))
|
|
39
|
+
expect(inDomainProject(tmp)).toBe(false)
|
|
40
|
+
await writeFile(join(tmp, 'astrale.config.ts'), 'export default {}\n')
|
|
41
|
+
expect(inDomainProject(tmp)).toBe(true)
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe('foreignPackageManager', () => {
|
|
46
|
+
let tmp: string
|
|
47
|
+
afterEach(async () => {
|
|
48
|
+
if (tmp) await rm(tmp, { recursive: true, force: true })
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('pnpm-lock / no lockfile → null (we own it); other lockfiles → that PM', async () => {
|
|
52
|
+
tmp = await mkdtemp(join(tmpdir(), 'sdk-deps-pm-'))
|
|
53
|
+
expect(foreignPackageManager(tmp)).toBeNull() // fresh → pnpm-first default
|
|
54
|
+
|
|
55
|
+
await writeFile(join(tmp, 'package-lock.json'), '{}')
|
|
56
|
+
expect(foreignPackageManager(tmp)).toBe('npm')
|
|
57
|
+
|
|
58
|
+
// pnpm-lock wins even alongside another lockfile (it's a pnpm project).
|
|
59
|
+
await writeFile(join(tmp, 'pnpm-lock.yaml'), "lockfileVersion: '9.0'\n")
|
|
60
|
+
expect(foreignPackageManager(tmp)).toBeNull()
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('yarn.lock → yarn', async () => {
|
|
64
|
+
tmp = await mkdtemp(join(tmpdir(), 'sdk-deps-yarn-'))
|
|
65
|
+
await writeFile(join(tmp, 'yarn.lock'), '')
|
|
66
|
+
expect(foreignPackageManager(tmp)).toBe('yarn')
|
|
67
|
+
})
|
|
68
|
+
})
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { SignJWT } from 'jose'
|
|
3
|
+
import { webcrypto } from 'node:crypto'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
containsSelfRef,
|
|
7
|
+
expandSelfReferences,
|
|
8
|
+
resolveSelfNodeId,
|
|
9
|
+
selfRefusalError,
|
|
10
|
+
type SelfResolverContext,
|
|
11
|
+
type SelfResolution,
|
|
12
|
+
} from '../self'
|
|
13
|
+
|
|
14
|
+
// ── Regex behavior ───────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
describe('containsSelfRef / expandSelfReferences', () => {
|
|
17
|
+
const ID = 'node-abc-123'
|
|
18
|
+
|
|
19
|
+
// Cases that MUST match — documented positions only.
|
|
20
|
+
test.each([
|
|
21
|
+
['@self', `@${ID}`],
|
|
22
|
+
['@self::deployFunction', `@${ID}::deployFunction`],
|
|
23
|
+
['@self/functions', `@${ID}/functions`], // path navigation
|
|
24
|
+
['@self/functions/foo', `@${ID}/functions/foo`],
|
|
25
|
+
['node=@self', `node=@${ID}`],
|
|
26
|
+
['target=@self::method', `target=@${ID}::method`],
|
|
27
|
+
['target=@self/sub', `target=@${ID}/sub`],
|
|
28
|
+
['x=@self::m y=@self', `x=@${ID}::m y=@${ID}`], // double match
|
|
29
|
+
])('expands %p → %p', (input, expected) => {
|
|
30
|
+
expect(containsSelfRef(input)).toBe(true)
|
|
31
|
+
expect(expandSelfReferences(input, ID)).toBe(expected)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
// selfId carrying `String.replace` substitution metacharacters must not
|
|
35
|
+
// be re-interpreted (function-form replacement guards this).
|
|
36
|
+
test.each([
|
|
37
|
+
['@self', 'x$&y', '@x$&y'],
|
|
38
|
+
['@self', 'x$$y', '@x$$y'],
|
|
39
|
+
['@self', 'x$1y', '@x$1y'],
|
|
40
|
+
['@self', 'x$`y', '@x$`y'],
|
|
41
|
+
['@self::m', 'x$&y', '@x$&y::m'],
|
|
42
|
+
])('selfId with `$` metachars is treated literally: %p × %p → %p', (input, id, expected) => {
|
|
43
|
+
expect(expandSelfReferences(input, id)).toBe(expected)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
// Cases that MUST NOT match — substrings, JSON-like, comma-lists.
|
|
47
|
+
test.each([
|
|
48
|
+
'prefix@self',
|
|
49
|
+
'@selfsuffix',
|
|
50
|
+
'@self.example',
|
|
51
|
+
'@self,@other',
|
|
52
|
+
'node=before@self',
|
|
53
|
+
'{"ref":"@self"}', // inside quoted JSON — lookbehind requires `=` or `^`
|
|
54
|
+
'no-self-here',
|
|
55
|
+
])('does NOT expand %p', (input) => {
|
|
56
|
+
expect(containsSelfRef(input)).toBe(false)
|
|
57
|
+
expect(expandSelfReferences(input, ID)).toBe(input)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('containsSelfRef is stateless across calls (regex.lastIndex reset)', () => {
|
|
61
|
+
// Both calls should return true even though the underlying global regex
|
|
62
|
+
// tracks lastIndex internally. The implementation must reset it.
|
|
63
|
+
expect(containsSelfRef('@self')).toBe(true)
|
|
64
|
+
expect(containsSelfRef('@self')).toBe(true)
|
|
65
|
+
expect(containsSelfRef('@self')).toBe(true)
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
// ── Resolver — refusal paths ─────────────────────────────────
|
|
70
|
+
|
|
71
|
+
describe('resolveSelfNodeId refusals', () => {
|
|
72
|
+
test('bootstrap manager identity → manager', () => {
|
|
73
|
+
const ctx: SelfResolverContext = {
|
|
74
|
+
identity: { name: 'manager', subject: 'manager', createdAt: 'x' },
|
|
75
|
+
instanceSlug: 'manager',
|
|
76
|
+
instanceSigned: false,
|
|
77
|
+
}
|
|
78
|
+
const r = resolveSelfNodeId(ctx)
|
|
79
|
+
expect(r).toEqual({ reason: 'manager' })
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('identity without registration on slug → no-registration', () => {
|
|
83
|
+
const ctx: SelfResolverContext = {
|
|
84
|
+
identity: { name: 'alice', subject: 'alice', createdAt: 'x' },
|
|
85
|
+
instanceSlug: 'kernel-aidev',
|
|
86
|
+
instanceSigned: false,
|
|
87
|
+
}
|
|
88
|
+
const r = resolveSelfNodeId(ctx)
|
|
89
|
+
expect(r).toEqual({
|
|
90
|
+
reason: 'no-registration',
|
|
91
|
+
identityName: 'alice',
|
|
92
|
+
instanceSlug: 'kernel-aidev',
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test('instance-signed path → instance-signed', () => {
|
|
97
|
+
const ctx: SelfResolverContext = {
|
|
98
|
+
identity: { name: 'manager', subject: 'manager', createdAt: 'x' },
|
|
99
|
+
instanceSlug: 'child-with-keypair',
|
|
100
|
+
instanceSigned: true,
|
|
101
|
+
}
|
|
102
|
+
const r = resolveSelfNodeId(ctx)
|
|
103
|
+
expect(r).toEqual({ reason: 'instance-signed', instanceSlug: 'child-with-keypair' })
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('no slug (--url without -i) → url-no-slug', () => {
|
|
107
|
+
const ctx: SelfResolverContext = {
|
|
108
|
+
identity: { name: 'alice', subject: 'alice', createdAt: 'x' },
|
|
109
|
+
instanceSlug: undefined,
|
|
110
|
+
instanceSigned: false,
|
|
111
|
+
}
|
|
112
|
+
const r = resolveSelfNodeId(ctx)
|
|
113
|
+
expect(r).toEqual({ reason: 'url-no-slug' })
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('--creds with malformed JWT → creds-no-sub', () => {
|
|
117
|
+
const ctx: SelfResolverContext = {
|
|
118
|
+
credsJwt: 'not-a-jwt',
|
|
119
|
+
instanceSigned: false,
|
|
120
|
+
}
|
|
121
|
+
const r = resolveSelfNodeId(ctx)
|
|
122
|
+
expect(r).toEqual({ reason: 'creds-no-sub' })
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
test('--creds with valid JWT but empty sub → creds-no-sub', async () => {
|
|
126
|
+
const key = await generateEs256()
|
|
127
|
+
const jwt = await new SignJWT({})
|
|
128
|
+
.setProtectedHeader({ alg: 'ES256' })
|
|
129
|
+
.setIssuer('test')
|
|
130
|
+
.setAudience('test')
|
|
131
|
+
.sign(key)
|
|
132
|
+
const ctx: SelfResolverContext = { credsJwt: jwt, instanceSigned: false }
|
|
133
|
+
const r = resolveSelfNodeId(ctx)
|
|
134
|
+
expect(r).toEqual({ reason: 'creds-no-sub' })
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test('--creds with whitespace-only sub → creds-no-sub', async () => {
|
|
138
|
+
const key = await generateEs256()
|
|
139
|
+
const jwt = await new SignJWT({})
|
|
140
|
+
.setProtectedHeader({ alg: 'ES256' })
|
|
141
|
+
.setSubject(' ')
|
|
142
|
+
.setIssuer('test')
|
|
143
|
+
.setAudience('test')
|
|
144
|
+
.sign(key)
|
|
145
|
+
const ctx: SelfResolverContext = { credsJwt: jwt, instanceSigned: false }
|
|
146
|
+
const r = resolveSelfNodeId(ctx)
|
|
147
|
+
expect(r).toEqual({ reason: 'creds-no-sub' })
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
// ── Resolver — happy paths ───────────────────────────────────
|
|
152
|
+
|
|
153
|
+
describe('resolveSelfNodeId happy paths', () => {
|
|
154
|
+
test('--as identity with registration → registration sub', () => {
|
|
155
|
+
const ctx: SelfResolverContext = {
|
|
156
|
+
identity: {
|
|
157
|
+
name: 'alice',
|
|
158
|
+
subject: 'alice',
|
|
159
|
+
createdAt: 'x',
|
|
160
|
+
registrations: {
|
|
161
|
+
'kernel-aidev': { iss: 'https://aidev/', sub: 'node-id-aidev', registeredAt: 'x' },
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
instanceSlug: 'kernel-aidev',
|
|
165
|
+
instanceSigned: false,
|
|
166
|
+
}
|
|
167
|
+
const r = resolveSelfNodeId(ctx)
|
|
168
|
+
expect(r).toEqual({ id: 'node-id-aidev' })
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
test('default identity with registration → registration sub', () => {
|
|
172
|
+
const ctx: SelfResolverContext = {
|
|
173
|
+
identity: {
|
|
174
|
+
name: 'computer',
|
|
175
|
+
subject: 'kernel-node-id',
|
|
176
|
+
createdAt: 'x',
|
|
177
|
+
registrations: {
|
|
178
|
+
kernel: { iss: 'https://k/', sub: 'kernel-node-id', registeredAt: 'x' },
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
instanceSlug: 'kernel',
|
|
182
|
+
instanceSigned: false,
|
|
183
|
+
}
|
|
184
|
+
const r: SelfResolution = resolveSelfNodeId(ctx)
|
|
185
|
+
expect(r).toEqual({ id: 'kernel-node-id' })
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
test('--creds with valid sub → sub wins over identity', async () => {
|
|
189
|
+
const key = await generateEs256()
|
|
190
|
+
const jwt = await new SignJWT({ sub: 'creds-derived-id' })
|
|
191
|
+
.setProtectedHeader({ alg: 'ES256' })
|
|
192
|
+
.setIssuer('test')
|
|
193
|
+
.setSubject('creds-derived-id')
|
|
194
|
+
.setAudience('test')
|
|
195
|
+
.sign(key)
|
|
196
|
+
const ctx: SelfResolverContext = {
|
|
197
|
+
identity: { name: 'alice', subject: 'alice', createdAt: 'x' },
|
|
198
|
+
instanceSlug: 'kernel',
|
|
199
|
+
credsJwt: jwt,
|
|
200
|
+
instanceSigned: false,
|
|
201
|
+
}
|
|
202
|
+
const r = resolveSelfNodeId(ctx)
|
|
203
|
+
expect(r).toEqual({ id: 'creds-derived-id' })
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
test('manager identity WITH registrations → uses registration sub (not refused)', () => {
|
|
207
|
+
const ctx: SelfResolverContext = {
|
|
208
|
+
identity: {
|
|
209
|
+
name: 'manager',
|
|
210
|
+
subject: 'manager',
|
|
211
|
+
createdAt: 'x',
|
|
212
|
+
registrations: {
|
|
213
|
+
'some-child': { iss: 'x', sub: 'mgr-node-id-on-child', registeredAt: 'x' },
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
instanceSlug: 'some-child',
|
|
217
|
+
instanceSigned: false,
|
|
218
|
+
}
|
|
219
|
+
const r = resolveSelfNodeId(ctx)
|
|
220
|
+
expect(r).toEqual({ id: 'mgr-node-id-on-child' })
|
|
221
|
+
})
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
// ── Error builder ────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
describe('selfRefusalError', () => {
|
|
227
|
+
test('manager refusal carries metadata + actionable message', () => {
|
|
228
|
+
const e = selfRefusalError({ reason: 'manager' })
|
|
229
|
+
expect(e).toBeInstanceOf(Error)
|
|
230
|
+
expect(e.name).toBe('SelfRefusalError')
|
|
231
|
+
expect(e.message).toContain('bootstrap `manager` identity')
|
|
232
|
+
expect(e.message).toContain('astrale identity create')
|
|
233
|
+
expect((e as Error & { selfRefusal?: { reason: string } }).selfRefusal?.reason).toBe('manager')
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
test('no-registration message references the specific identity + slug', () => {
|
|
237
|
+
const e = selfRefusalError({
|
|
238
|
+
reason: 'no-registration',
|
|
239
|
+
identityName: 'alice',
|
|
240
|
+
instanceSlug: 'kernel-aidev',
|
|
241
|
+
})
|
|
242
|
+
expect(e.message).toContain('alice')
|
|
243
|
+
expect(e.message).toContain('kernel-aidev')
|
|
244
|
+
expect(e.message).toContain('astrale identity register alice -i kernel-aidev')
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
test('instance-signed message hints at --as', () => {
|
|
248
|
+
const e = selfRefusalError({ reason: 'instance-signed', instanceSlug: 'child' })
|
|
249
|
+
expect(e.message).toContain('--as')
|
|
250
|
+
expect(e.message).toContain('child')
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
test('url-no-slug message hints at -i', () => {
|
|
254
|
+
const e = selfRefusalError({ reason: 'url-no-slug' })
|
|
255
|
+
expect(e.message).toContain('-i')
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
test('creds-no-sub message hints at literal @<nodeId>', () => {
|
|
259
|
+
const e = selfRefusalError({ reason: 'creds-no-sub' })
|
|
260
|
+
expect(e.message).toContain('@<nodeId>')
|
|
261
|
+
})
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
// ── Helpers ──────────────────────────────────────────────────
|
|
265
|
+
|
|
266
|
+
async function generateEs256(): Promise<CryptoKey> {
|
|
267
|
+
const pair = await webcrypto.subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, [
|
|
268
|
+
'sign',
|
|
269
|
+
'verify',
|
|
270
|
+
])
|
|
271
|
+
return pair.privateKey
|
|
272
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { Project } from 'ts-morph'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The Domain Studio SERVER ships as raw source inside the published CLI package
|
|
8
|
+
* (package.json `files`: studio/server, studio/shared) and runs UNBUNDLED under
|
|
9
|
+
* Bun — unlike the CLI itself, which is bundled into dist/astrale.js with all its
|
|
10
|
+
* deps inlined. So at `astrale studio` runtime, the server can only resolve the
|
|
11
|
+
* third-party packages that the CLI declares in its own `dependencies` (the
|
|
12
|
+
* studio's package.json deps are NOT installed for an `npm i -g @astrale-os/cli`
|
|
13
|
+
* consumer — they're client/build-only, inlined into studio/client/dist).
|
|
14
|
+
*
|
|
15
|
+
* This test enforces that invariant. If it fails, either add the package to the
|
|
16
|
+
* CLI's `dependencies` or drop the import — do NOT add it to studio/package.json
|
|
17
|
+
* (that won't ship to npm consumers).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const CLI_ROOT = new URL('../../../', import.meta.url).pathname
|
|
21
|
+
|
|
22
|
+
/** Base package of a bare specifier: `@scope/pkg/sub` → `@scope/pkg`, `pkg/sub` → `pkg`. */
|
|
23
|
+
function basePackage(spec: string): string {
|
|
24
|
+
const parts = spec.split('/')
|
|
25
|
+
return spec.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0]!
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isBuiltin(spec: string): boolean {
|
|
29
|
+
return spec.startsWith('node:') || spec === 'bun' || spec.startsWith('bun:')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('studio server runtime deps', () => {
|
|
33
|
+
const deps = new Set(
|
|
34
|
+
Object.keys(
|
|
35
|
+
(
|
|
36
|
+
JSON.parse(readFileSync(join(CLI_ROOT, 'package.json'), 'utf8')) as {
|
|
37
|
+
dependencies?: Record<string, string>
|
|
38
|
+
}
|
|
39
|
+
).dependencies ?? {},
|
|
40
|
+
),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
const project = new Project({ skipAddingFilesFromTsConfig: true })
|
|
44
|
+
project.addSourceFilesAtPaths([
|
|
45
|
+
join(CLI_ROOT, 'studio/server/**/*.ts'),
|
|
46
|
+
join(CLI_ROOT, 'studio/shared/**/*.ts'),
|
|
47
|
+
])
|
|
48
|
+
|
|
49
|
+
// getImportStringLiterals covers static imports, re-exports, and dynamic import().
|
|
50
|
+
const offenders: Record<string, string[]> = {}
|
|
51
|
+
for (const sf of project.getSourceFiles()) {
|
|
52
|
+
const specs = sf.getImportStringLiterals().map((l) => l.getLiteralValue())
|
|
53
|
+
for (const spec of specs) {
|
|
54
|
+
if (spec.startsWith('.') || spec.startsWith('/') || isBuiltin(spec)) continue
|
|
55
|
+
const pkg = basePackage(spec)
|
|
56
|
+
if (!deps.has(pkg)) (offenders[pkg] ??= []).push(sf.getFilePath().replace(CLI_ROOT, ''))
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
test('only import Node/Bun builtins or packages in the CLI dependencies', () => {
|
|
61
|
+
expect(
|
|
62
|
+
offenders,
|
|
63
|
+
`studio server imports packages NOT in the CLI's package.json dependencies — ` +
|
|
64
|
+
`they won't be installed for an \`npm i -g @astrale-os/cli\` consumer:\n` +
|
|
65
|
+
JSON.stringify(offenders, null, 2),
|
|
66
|
+
).toEqual({})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('parsed at least the known studio server files', () => {
|
|
70
|
+
// Guard against the glob silently matching nothing (which would make the
|
|
71
|
+
// invariant vacuously pass).
|
|
72
|
+
expect(project.getSourceFiles().length).toBeGreaterThan(10)
|
|
73
|
+
})
|
|
74
|
+
})
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import { renderTable } from '../table'
|
|
4
|
+
|
|
5
|
+
const ESC = String.fromCharCode(27)
|
|
6
|
+
const strip = (s: string): string => s.replace(new RegExp(`${ESC}\\[[0-9;]*m`, 'g'), '')
|
|
7
|
+
|
|
8
|
+
const COLS = [
|
|
9
|
+
{ key: 'name', header: 'NAME' },
|
|
10
|
+
{ key: 'kind', header: 'KIND' },
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
describe('renderTable', () => {
|
|
14
|
+
test('aligns the KIND column across rows of different name widths', () => {
|
|
15
|
+
const out = strip(
|
|
16
|
+
renderTable(
|
|
17
|
+
[
|
|
18
|
+
{ name: 'dist.astrale.ai', kind: 'Domain' },
|
|
19
|
+
{ name: 'workspace', kind: 'Folder' },
|
|
20
|
+
],
|
|
21
|
+
{ columns: COLS },
|
|
22
|
+
),
|
|
23
|
+
)
|
|
24
|
+
const [a, b] = out.split('\n')
|
|
25
|
+
expect(a.indexOf('Domain')).toBe(b.indexOf('Folder'))
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('empty rows render as (empty)', () => {
|
|
29
|
+
expect(strip(renderTable([], { columns: COLS }))).toContain('(empty)')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('drops a column that is empty across every row (with header)', () => {
|
|
33
|
+
const out = strip(renderTable([{ name: 'a', kind: '' }], { columns: COLS, showHeader: true }))
|
|
34
|
+
expect(out).toContain('NAME')
|
|
35
|
+
expect(out).not.toContain('KIND')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('computes width on visible length, ignoring ANSI in cells', () => {
|
|
39
|
+
const colored = `${ESC}[32mok${ESC}[39m`
|
|
40
|
+
const out = strip(
|
|
41
|
+
renderTable(
|
|
42
|
+
[
|
|
43
|
+
{ name: colored, kind: 'x' },
|
|
44
|
+
{ name: 'wider-name', kind: 'y' },
|
|
45
|
+
],
|
|
46
|
+
{ columns: COLS },
|
|
47
|
+
),
|
|
48
|
+
)
|
|
49
|
+
const [a, b] = out.split('\n')
|
|
50
|
+
// KIND aligns despite the ANSI codes that were in row 0's name cell.
|
|
51
|
+
expect(a.indexOf('x')).toBe(b.indexOf('y'))
|
|
52
|
+
})
|
|
53
|
+
})
|