@astrale-os/cli 0.5.0-alpha.0 → 0.6.0-alpha.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.
- package/dist/astrale.js +12962 -487
- package/package.json +7 -3
- package/src/command.ts +2 -0
- package/src/commands/__tests__/admin-instance.test.ts +3 -2
- package/src/commands/__tests__/domain-list.test.ts +6 -2
- package/src/commands/__tests__/install-identity-override.test.ts +1 -1
- package/src/commands/__tests__/view.test.ts +100 -0
- package/src/commands/call.ts +1 -1
- package/src/commands/domain/install.ts +5 -5
- package/src/commands/domain/list.ts +2 -2
- package/src/commands/domain/publish.ts +3 -3
- package/src/commands/instance/active.ts +2 -2
- package/src/commands/instance/create.ts +1 -1
- package/src/commands/instance/delete.ts +3 -3
- package/src/commands/instance/list.ts +8 -3
- package/src/commands/instance/status.ts +4 -3
- package/src/commands/instance/use.ts +2 -2
- package/src/commands/query.ts +27 -8
- package/src/commands/session/analyze.ts +50 -0
- package/src/commands/session/list.ts +36 -0
- package/src/commands/token.ts +1 -1
- package/src/commands/view-serve.ts +26 -0
- package/src/commands/view.ts +614 -0
- package/src/connect-core.test.ts +42 -0
- package/src/connect-core.ts +53 -0
- package/src/kernel/__tests__/auth.test.ts +1 -0
- package/src/kernel/client.ts +30 -24
- package/src/kernel/expand.ts +3 -2
- package/src/kernel/index.ts +7 -1
- package/src/kernel/options.ts +1 -1
- package/src/lib/__tests__/instance-target.test.ts +34 -0
- package/src/lib/__tests__/view-open-intent.test.ts +308 -0
- package/src/lib/__tests__/view-snapshot.test.ts +77 -0
- package/src/lib/admin-domain.ts +8 -4
- package/src/lib/admin-instance.ts +5 -1
- package/src/lib/config.ts +1 -0
- package/src/lib/instance-target.ts +5 -0
- package/src/lib/instance.ts +11 -0
- package/src/lib/log.ts +12 -2
- package/src/lib/login-flow.ts +41 -4
- package/src/lib/provision-instance.ts +2 -2
- package/src/lib/view/open-intent.ts +97 -0
- package/src/lib/view/resolve.ts +104 -0
- package/src/lib/view/server.ts +294 -0
- package/src/lib/view/session.ts +123 -0
- package/src/lib/view/snapshot.ts +101 -0
- package/src/program.ts +12 -1
- package/src/registry.ts +1 -1
- package/src/setup/steps/instance.ts +2 -2
- package/src/telemetry/__tests__/gate.test.ts +63 -0
- package/src/telemetry/__tests__/recorder.test.ts +110 -0
- package/src/telemetry/__tests__/redact.test.ts +79 -0
- package/src/telemetry/__tests__/session.test.ts +166 -0
- package/src/telemetry/__tests__/trigger.test.ts +49 -0
- package/src/telemetry/adapters/__tests__/claude-code.test.ts +89 -0
- package/src/telemetry/adapters/__tests__/codex.test.ts +138 -0
- package/src/telemetry/adapters/__tests__/index.test.ts +88 -0
- package/src/telemetry/adapters/claude-code.ts +90 -0
- package/src/telemetry/adapters/codex.ts +160 -0
- package/src/telemetry/adapters/index.ts +45 -0
- package/src/telemetry/adapters/types.ts +21 -0
- package/src/telemetry/analyze.ts +227 -0
- package/src/telemetry/gate.ts +79 -0
- package/src/telemetry/recorder.ts +57 -0
- package/src/telemetry/redact.ts +53 -0
- package/src/telemetry/session.ts +88 -0
- package/src/telemetry/settings.ts +26 -0
- package/src/telemetry/store.ts +91 -0
- package/src/telemetry/trigger.ts +119 -0
- package/src/telemetry/types.ts +64 -0
- package/studio/client/dist/assets/index-CyN5G8IA.js +109 -0
- package/studio/client/dist/assets/index-DKKMHBBC.css +1 -0
- package/studio/client/dist/index.html +2 -2
- package/studio/server/agent/ask.ts +2 -1
- package/studio/server/agent/runner.ts +4 -1
- package/studio/server/agent/session-id.ts +13 -0
- package/studio/server/api.ts +43 -25
- package/studio/server/cache.ts +17 -6
- package/studio/server/client-package.test.ts +147 -0
- package/studio/server/client-package.ts +242 -0
- package/studio/server/index.ts +13 -0
- package/studio/server/introspect/anatomy-extras.test.ts +91 -0
- package/studio/server/introspect/anatomy-extras.ts +48 -49
- package/studio/server/introspect/anatomy.ts +7 -7
- package/studio/server/introspect/overlay-tsmorph.test.ts +104 -0
- package/studio/server/introspect/overlay-tsmorph.ts +122 -22
- package/studio/server/state/views.test.ts +90 -0
- package/studio/server/state/views.ts +396 -99
- package/studio/server/state/visibility.ts +5 -5
- package/studio/server/view-dev-server.test.ts +111 -0
- package/studio/server/view-dev-server.ts +372 -0
- package/studio/shared/types.ts +57 -10
- package/viewer/dist/index.html +93 -0
- package/viewer/dist/main.js +71 -0
- package/studio/client/dist/assets/index-BcejyJpa.css +0 -1
- package/studio/client/dist/assets/index-Cqz3Oy_B.js +0 -179
|
@@ -28,7 +28,7 @@ function newProject(): Project {
|
|
|
28
28
|
function tryAddFile(project: Project, file: string): SourceFile | undefined {
|
|
29
29
|
try {
|
|
30
30
|
if (!existsSync(file)) return undefined
|
|
31
|
-
return project.addSourceFileAtPath(file)
|
|
31
|
+
return project.getSourceFile(file) ?? project.addSourceFileAtPath(file)
|
|
32
32
|
} catch {
|
|
33
33
|
return undefined
|
|
34
34
|
}
|
|
@@ -163,28 +163,129 @@ interface ConfigResolution {
|
|
|
163
163
|
unlinkedReason?: 'no-config'
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
function resolveConfigObject(configNode: Node): ConfigResolution {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (
|
|
170
|
-
|
|
166
|
+
function resolveConfigObject(configNode: Node, seen = new Set<string>()): ConfigResolution {
|
|
167
|
+
const node = unwrapExpression(configNode)
|
|
168
|
+
const key = `${node.getSourceFile().getFilePath()}:${node.getStart()}`
|
|
169
|
+
if (seen.has(key)) return { implemented: false, unlinkedReason: 'no-config' }
|
|
170
|
+
seen.add(key)
|
|
171
|
+
|
|
172
|
+
if (Node.isCallExpression(node)) {
|
|
173
|
+
// Curried SDK helpers carry the handler config in their final argument:
|
|
174
|
+
// `remoteMethod()(schema, 'Issue', 'delete', { authorize, execute })`.
|
|
175
|
+
const lastArg = node.getArguments().at(-1)
|
|
176
|
+
if (lastArg) {
|
|
177
|
+
const candidate = unwrapExpression(lastArg)
|
|
178
|
+
if (Node.isObjectLiteralExpression(candidate) || Node.isIdentifier(candidate)) {
|
|
179
|
+
return resolveConfigObject(candidate, seen)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Local factories such as `todo('Owner.name')` return the config object themselves.
|
|
184
|
+
const obj = followFactoryToObject(node)
|
|
171
185
|
if (obj) return classifyObjectLiteral(obj)
|
|
172
186
|
return { implemented: false, unlinkedReason: 'no-config' }
|
|
173
187
|
}
|
|
174
|
-
if (Node.isObjectLiteralExpression(
|
|
175
|
-
return classifyObjectLiteral(
|
|
188
|
+
if (Node.isObjectLiteralExpression(node)) {
|
|
189
|
+
return classifyObjectLiteral(node)
|
|
176
190
|
}
|
|
177
|
-
//
|
|
178
|
-
if (Node.isIdentifier(
|
|
179
|
-
const
|
|
180
|
-
if (
|
|
181
|
-
const init = decl.getInitializer()
|
|
182
|
-
if (init) return resolveConfigObject(init)
|
|
183
|
-
}
|
|
191
|
+
// Handler maps normally reference a local or imported `remoteMethod` value.
|
|
192
|
+
if (Node.isIdentifier(node)) {
|
|
193
|
+
const value = valueOfIdentifier(node)
|
|
194
|
+
if (value) return resolveConfigObject(value, seen)
|
|
184
195
|
}
|
|
185
196
|
return { implemented: false, unlinkedReason: 'no-config' }
|
|
186
197
|
}
|
|
187
198
|
|
|
199
|
+
/** Remove syntax-only wrappers around a runtime value. */
|
|
200
|
+
function unwrapExpression(node: Node): Node {
|
|
201
|
+
let current = node
|
|
202
|
+
while (
|
|
203
|
+
Node.isParenthesizedExpression(current) ||
|
|
204
|
+
Node.isAsExpression(current) ||
|
|
205
|
+
Node.isSatisfiesExpression(current) ||
|
|
206
|
+
Node.isTypeAssertion(current)
|
|
207
|
+
) {
|
|
208
|
+
current = current.getExpression()
|
|
209
|
+
}
|
|
210
|
+
return current
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Resolve a local or relatively imported identifier to its runtime value. */
|
|
214
|
+
function valueOfIdentifier(id: Node): Node | undefined {
|
|
215
|
+
if (!Node.isIdentifier(id)) return undefined
|
|
216
|
+
const decl = firstValueDeclaration(id)
|
|
217
|
+
if (decl && Node.isVariableDeclaration(decl)) return decl.getInitializer()
|
|
218
|
+
return resolveImportedValue(id)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Follow a named relative import to the exported value that defines it. */
|
|
222
|
+
function resolveImportedValue(id: Node): Node | undefined {
|
|
223
|
+
if (!Node.isIdentifier(id)) return undefined
|
|
224
|
+
const localName = id.getText()
|
|
225
|
+
const sourceFile = id.getSourceFile()
|
|
226
|
+
|
|
227
|
+
for (const imp of sourceFile.getImportDeclarations()) {
|
|
228
|
+
for (const named of imp.getNamedImports()) {
|
|
229
|
+
const local = named.getAliasNode()?.getText() ?? named.getName()
|
|
230
|
+
if (local !== localName) continue
|
|
231
|
+
const target = resolveModuleFile(sourceFile, imp.getModuleSpecifierValue())
|
|
232
|
+
if (!target) return undefined
|
|
233
|
+
return findExportedValue(id.getProject(), target, named.getName(), new Set<string>())
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return undefined
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Find an exported variable/function value, following local and barrel exports. */
|
|
240
|
+
function findExportedValue(
|
|
241
|
+
project: Project,
|
|
242
|
+
file: string,
|
|
243
|
+
exportName: string,
|
|
244
|
+
seen: Set<string>,
|
|
245
|
+
): Node | undefined {
|
|
246
|
+
const key = `${file}:${exportName}`
|
|
247
|
+
if (seen.has(key)) return undefined
|
|
248
|
+
seen.add(key)
|
|
249
|
+
const sf = tryAddFile(project, file)
|
|
250
|
+
if (!sf) return undefined
|
|
251
|
+
|
|
252
|
+
const localValue = (name: string, exportedOnly: boolean): Node | undefined => {
|
|
253
|
+
const variable = sf
|
|
254
|
+
.getVariableDeclarations()
|
|
255
|
+
.find(
|
|
256
|
+
(candidate) => candidate.getName() === name && (!exportedOnly || candidate.isExported()),
|
|
257
|
+
)
|
|
258
|
+
if (variable) return variable.getInitializer()
|
|
259
|
+
return sf
|
|
260
|
+
.getFunctions()
|
|
261
|
+
.find(
|
|
262
|
+
(candidate) => candidate.getName() === name && (!exportedOnly || candidate.isExported()),
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const direct = localValue(exportName, true)
|
|
267
|
+
if (direct) return direct
|
|
268
|
+
|
|
269
|
+
for (const ex of sf.getExportDeclarations()) {
|
|
270
|
+
const modSpec = ex.getModuleSpecifierValue()
|
|
271
|
+
const target = modSpec ? resolveModuleFile(sf, modSpec) : undefined
|
|
272
|
+
const named = ex.getNamedExports()
|
|
273
|
+
if (named.length > 0) {
|
|
274
|
+
for (const specifier of named) {
|
|
275
|
+
const exposed = specifier.getAliasNode()?.getText() ?? specifier.getName()
|
|
276
|
+
if (exposed !== exportName) continue
|
|
277
|
+
const original = specifier.getName()
|
|
278
|
+
if (target) return findExportedValue(project, target, original, seen)
|
|
279
|
+
return localValue(original, false)
|
|
280
|
+
}
|
|
281
|
+
} else if (target) {
|
|
282
|
+
const found = findExportedValue(project, target, exportName, seen)
|
|
283
|
+
if (found) return found
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return undefined
|
|
287
|
+
}
|
|
288
|
+
|
|
188
289
|
/** Given `todo('x')`, find the factory's `return { … }` object literal. */
|
|
189
290
|
function followFactoryToObject(call: CallExpression): Node | undefined {
|
|
190
291
|
const expr = call.getExpression()
|
|
@@ -287,11 +388,8 @@ function functionBodyOf(node: Node): Node | null | undefined {
|
|
|
287
388
|
if (Node.isArrowFunction(node) || Node.isFunctionExpression(node)) fn = node
|
|
288
389
|
else if (Node.isMethodDeclaration(node)) fn = node
|
|
289
390
|
else if (Node.isIdentifier(node)) {
|
|
290
|
-
const
|
|
291
|
-
if (
|
|
292
|
-
const init = decl.getInitializer()
|
|
293
|
-
if (init) return functionBodyOf(init)
|
|
294
|
-
}
|
|
391
|
+
const value = valueOfIdentifier(node)
|
|
392
|
+
if (value) return functionBodyOf(value)
|
|
295
393
|
return undefined
|
|
296
394
|
}
|
|
297
395
|
if (!fn) return undefined
|
|
@@ -434,9 +532,11 @@ function resolveModuleFile(from: SourceFile, spec: string): string | undefined {
|
|
|
434
532
|
if (!spec.startsWith('.')) return undefined
|
|
435
533
|
const baseDir = dirname(from.getFilePath())
|
|
436
534
|
const base = resolvePath(baseDir, spec)
|
|
535
|
+
const sourceBase = base.replace(/\.(?:[cm]?js|jsx)$/, '')
|
|
437
536
|
const candidates = [
|
|
438
|
-
base
|
|
439
|
-
|
|
537
|
+
base,
|
|
538
|
+
sourceBase.endsWith('.ts') ? sourceBase : `${sourceBase}.ts`,
|
|
539
|
+
sourceBase.endsWith('.tsx') ? sourceBase : `${sourceBase}.tsx`,
|
|
440
540
|
`${base}/index.ts`,
|
|
441
541
|
`${base}/index.tsx`,
|
|
442
542
|
]
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import type { RememberedViewTarget, ViewTargetCandidate } from '../../shared/types'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
conciseCliFailure,
|
|
7
|
+
localViewUrl,
|
|
8
|
+
reconcileRememberedTarget,
|
|
9
|
+
targetFromRow,
|
|
10
|
+
targetQuery,
|
|
11
|
+
} from './views'
|
|
12
|
+
|
|
13
|
+
describe('managed local view URLs', () => {
|
|
14
|
+
test('mounts each view beneath the Studio-assigned server origin', () => {
|
|
15
|
+
expect(localViewUrl('http://127.0.0.1:5173', '/ui/issues')).toBe(
|
|
16
|
+
'http://127.0.0.1:5173/ui/issues',
|
|
17
|
+
)
|
|
18
|
+
expect(localViewUrl('http://127.0.0.1:61001/ignored', 'ui/issue')).toBe(
|
|
19
|
+
'http://127.0.0.1:61001/ui/issue',
|
|
20
|
+
)
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('reduces CLI stack output to the actionable kernel failure', () => {
|
|
25
|
+
expect(
|
|
26
|
+
conciseCliFailure(
|
|
27
|
+
`275 | if (code === KERNEL_ERROR_CODES.INVALID_REQUEST)\n276 |\nPermissionDeniedError: Permission denied: READ on /:issues.astrale.ai:view.issue\n details: {\n method: \"View:resolve\"\n}\n at mapServerError (errors.ts:280:61)\nBun v1.3.14`,
|
|
28
|
+
),
|
|
29
|
+
).toBe('PermissionDeniedError: Permission denied: READ on /:issues.astrale.ai:view.issue')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
describe('target candidates', () => {
|
|
33
|
+
const remembered: RememberedViewTarget = {
|
|
34
|
+
id: 'gone-id',
|
|
35
|
+
className: 'Issue',
|
|
36
|
+
classOrigin: 'issues.astrale.ai',
|
|
37
|
+
label: 'Deleted issue',
|
|
38
|
+
}
|
|
39
|
+
const candidate: ViewTargetCandidate = {
|
|
40
|
+
id: 'live-id',
|
|
41
|
+
ref: '@live-id',
|
|
42
|
+
className: 'Issue',
|
|
43
|
+
classOrigin: 'issues.astrale.ai',
|
|
44
|
+
label: 'Live issue',
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
test('turns a missing remembered node into an explicit stale selection', () => {
|
|
48
|
+
expect(reconcileRememberedTarget(remembered, [candidate])).toEqual({
|
|
49
|
+
selected: null,
|
|
50
|
+
stale: remembered,
|
|
51
|
+
})
|
|
52
|
+
expect(reconcileRememberedTarget({ ...remembered, id: 'live-id' }, [candidate])).toEqual({
|
|
53
|
+
selected: candidate,
|
|
54
|
+
stale: null,
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('builds friendly candidates from generic qualified node properties', () => {
|
|
59
|
+
expect(
|
|
60
|
+
targetFromRow(
|
|
61
|
+
{
|
|
62
|
+
id: 'iss-1',
|
|
63
|
+
props: {
|
|
64
|
+
'kernel.astrale.ai:interface.Named.property.name': 'Broken authorization badge',
|
|
65
|
+
'kernel.astrale.ai:interface.Descriptable.property.description': 'Studio regression',
|
|
66
|
+
'kernel.astrale.ai:interface.Statused.property.status': 'open',
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
'Issue',
|
|
70
|
+
'issues.astrale.ai',
|
|
71
|
+
),
|
|
72
|
+
).toEqual({
|
|
73
|
+
id: 'iss-1',
|
|
74
|
+
ref: '@iss-1',
|
|
75
|
+
className: 'Issue',
|
|
76
|
+
classOrigin: 'issues.astrale.ai',
|
|
77
|
+
label: 'Broken authorization badge',
|
|
78
|
+
description: 'Studio regression',
|
|
79
|
+
status: 'open',
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('queries exact class instances through instance_of', () => {
|
|
84
|
+
const query = targetQuery('Issue', 'issues.astrale.ai')
|
|
85
|
+
expect(query).toContain('(n:Node)-[:instance_of]->(c:Class)-[:of_domain]->(d:Domain)')
|
|
86
|
+
expect(query).toContain('"Issue"')
|
|
87
|
+
expect(query).toContain('"issues.astrale.ai"')
|
|
88
|
+
expect(query).toContain('LIMIT 201')
|
|
89
|
+
})
|
|
90
|
+
})
|