@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
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test'
|
|
2
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
|
3
|
+
import { dirname, join, relative, resolve } from 'node:path'
|
|
4
|
+
import ts from 'typescript'
|
|
5
|
+
|
|
6
|
+
const SHARED_ROOT = resolve(import.meta.dir, '..')
|
|
7
|
+
const STUDIO_ROOT = resolve(SHARED_ROOT, '..')
|
|
8
|
+
|
|
9
|
+
// Frozen from HEAD's former monolithic shared/types.ts before it was split.
|
|
10
|
+
// Keeping this manifest in the test makes the compatibility proof independent
|
|
11
|
+
// of Git and catches any historical symbol silently dropped by a future move.
|
|
12
|
+
const HISTORICAL_BARREL_EXPORTS = [
|
|
13
|
+
'AGENT_ACCESS_LEVELS',
|
|
14
|
+
'AGENT_EFFORT_LEVELS',
|
|
15
|
+
'AgentAccess',
|
|
16
|
+
'AgentEffort',
|
|
17
|
+
'AgentEvent',
|
|
18
|
+
'AgentEventKind',
|
|
19
|
+
'AgentPromptSnapshot',
|
|
20
|
+
'AgentRun',
|
|
21
|
+
'AgentRunSnapshot',
|
|
22
|
+
'AgentRunStatus',
|
|
23
|
+
'AgentSessionInfo',
|
|
24
|
+
'AgentSystemPromptInfo',
|
|
25
|
+
'AnchorKind',
|
|
26
|
+
'AnchorRef',
|
|
27
|
+
'BundleError',
|
|
28
|
+
'ChangeSet',
|
|
29
|
+
'ClientFeature',
|
|
30
|
+
'ClientTree',
|
|
31
|
+
'Comment',
|
|
32
|
+
'CommentStore',
|
|
33
|
+
'ContextItem',
|
|
34
|
+
'ContextStore',
|
|
35
|
+
'ConversationInfo',
|
|
36
|
+
'CopyPayload',
|
|
37
|
+
'CrossDomainImport',
|
|
38
|
+
'DeployRecord',
|
|
39
|
+
'DeployResult',
|
|
40
|
+
'DocMeta',
|
|
41
|
+
'DomainAnatomy',
|
|
42
|
+
'DomainCatalogEntry',
|
|
43
|
+
'DomainOverview',
|
|
44
|
+
'DomainSummary',
|
|
45
|
+
'DomainUsage',
|
|
46
|
+
'EnvField',
|
|
47
|
+
'EnvFileModel',
|
|
48
|
+
'EnvName',
|
|
49
|
+
'EnvVarRow',
|
|
50
|
+
'FileChange',
|
|
51
|
+
'HandlerLink',
|
|
52
|
+
'HarnessCapabilities',
|
|
53
|
+
'HarnessGatewayAuth',
|
|
54
|
+
'HarnessGatewayConfig',
|
|
55
|
+
'HarnessGatewayState',
|
|
56
|
+
'HarnessLoadout',
|
|
57
|
+
'HarnessModelOption',
|
|
58
|
+
'HarnessStatus',
|
|
59
|
+
'InstanceInfo',
|
|
60
|
+
'InstanceStatus',
|
|
61
|
+
'InstancesState',
|
|
62
|
+
'Integration',
|
|
63
|
+
'IntegrationsState',
|
|
64
|
+
'IrCallableAuth',
|
|
65
|
+
'IrCallableOutput',
|
|
66
|
+
'IrClass',
|
|
67
|
+
'IrDefinitionKey',
|
|
68
|
+
'IrDefinitionRef',
|
|
69
|
+
'IrEndpoint',
|
|
70
|
+
'IrFunction',
|
|
71
|
+
'IrImportDescriptor',
|
|
72
|
+
'IrInterface',
|
|
73
|
+
'IrMethod',
|
|
74
|
+
'IrSchemaKey',
|
|
75
|
+
'IrSchemaRef',
|
|
76
|
+
'IrView',
|
|
77
|
+
'IrViewTarget',
|
|
78
|
+
'JsonSchema',
|
|
79
|
+
'LayoutState',
|
|
80
|
+
'LoadoutSkill',
|
|
81
|
+
'McpServerInfo',
|
|
82
|
+
'MergeResult',
|
|
83
|
+
'MethodInheritance',
|
|
84
|
+
'NodePosition',
|
|
85
|
+
'RememberedViewTarget',
|
|
86
|
+
'SchemaAnnotation',
|
|
87
|
+
'SchemaChange',
|
|
88
|
+
'SchemaChangeKind',
|
|
89
|
+
'SchemaIR',
|
|
90
|
+
'SchemaOverlay',
|
|
91
|
+
'SourceSpan',
|
|
92
|
+
'StaleReport',
|
|
93
|
+
'StudioCore',
|
|
94
|
+
'StudioCoreEdge',
|
|
95
|
+
'StudioCoreNode',
|
|
96
|
+
'StudioEvent',
|
|
97
|
+
'StudioSchemaBundle',
|
|
98
|
+
'StudioSettings',
|
|
99
|
+
'ThreadEntry',
|
|
100
|
+
'ThreadEntryType',
|
|
101
|
+
'ThreadRole',
|
|
102
|
+
'TypeDescriptor',
|
|
103
|
+
'ViewInfo',
|
|
104
|
+
'ViewRuntime',
|
|
105
|
+
'ViewSessionResult',
|
|
106
|
+
'ViewTargetCandidate',
|
|
107
|
+
'ViewTargetResult',
|
|
108
|
+
'VisibilityState',
|
|
109
|
+
] as const
|
|
110
|
+
|
|
111
|
+
// Deliberate canonical identity/revision helpers added alongside the split.
|
|
112
|
+
const INTENTIONAL_BARREL_ADDITIONS = [
|
|
113
|
+
'IR_SCHEMA_REF_KINDS',
|
|
114
|
+
'IrSchemaRefKind',
|
|
115
|
+
'STUDIO_SCHEMA_PROJECTION_VERSION',
|
|
116
|
+
'SchemaRevision',
|
|
117
|
+
'definitionRefKey',
|
|
118
|
+
'isIrDefinitionRef',
|
|
119
|
+
'isIrInterfaceRef',
|
|
120
|
+
'isIrSchemaRef',
|
|
121
|
+
'isSchemaRevision',
|
|
122
|
+
'parseDefinitionRefKey',
|
|
123
|
+
'parseSchemaRefKey',
|
|
124
|
+
'schemaRefKey',
|
|
125
|
+
] as const
|
|
126
|
+
|
|
127
|
+
function productionFiles(dir: string): string[] {
|
|
128
|
+
return readdirSync(dir).flatMap((entry) => {
|
|
129
|
+
const path = join(dir, entry)
|
|
130
|
+
if (statSync(path).isDirectory()) return productionFiles(path)
|
|
131
|
+
return /\.(?:ts|tsx)$/.test(path) && !/\.test\.(?:ts|tsx)$/.test(path) ? [path] : []
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function parseTypeScript(file: string): ts.SourceFile {
|
|
136
|
+
return ts.createSourceFile(
|
|
137
|
+
file,
|
|
138
|
+
readFileSync(file, 'utf8'),
|
|
139
|
+
ts.ScriptTarget.Latest,
|
|
140
|
+
false,
|
|
141
|
+
file.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function hasExportModifier(node: ts.Node): boolean {
|
|
146
|
+
return Boolean(
|
|
147
|
+
ts.canHaveModifiers(node) &&
|
|
148
|
+
ts.getModifiers(node)?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword),
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function bindingNames(name: ts.BindingName): string[] {
|
|
153
|
+
if (ts.isIdentifier(name)) return [name.text]
|
|
154
|
+
return name.elements.flatMap((element) =>
|
|
155
|
+
ts.isOmittedExpression(element) ? [] : bindingNames(element.name),
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function resolveTypeScriptModule(file: string, specifier: string): string {
|
|
160
|
+
const target = resolve(dirname(file), specifier)
|
|
161
|
+
const candidate = [target, `${target}.ts`, `${target}.tsx`, join(target, 'index.ts')].find(
|
|
162
|
+
existsSync,
|
|
163
|
+
)
|
|
164
|
+
if (!candidate) throw new Error(`Cannot resolve ${specifier} from ${file}`)
|
|
165
|
+
return candidate
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function namedExports(file: string, seen = new Set<string>()): string[] {
|
|
169
|
+
if (seen.has(file)) return []
|
|
170
|
+
seen.add(file)
|
|
171
|
+
const names: string[] = []
|
|
172
|
+
|
|
173
|
+
for (const statement of parseTypeScript(file).statements) {
|
|
174
|
+
if (ts.isExportDeclaration(statement)) {
|
|
175
|
+
if (statement.exportClause && ts.isNamedExports(statement.exportClause)) {
|
|
176
|
+
names.push(...statement.exportClause.elements.map((element) => element.name.text))
|
|
177
|
+
} else if (statement.exportClause && ts.isNamespaceExport(statement.exportClause)) {
|
|
178
|
+
names.push(statement.exportClause.name.text)
|
|
179
|
+
} else if (
|
|
180
|
+
statement.moduleSpecifier &&
|
|
181
|
+
ts.isStringLiteral(statement.moduleSpecifier) &&
|
|
182
|
+
statement.moduleSpecifier.text.startsWith('.')
|
|
183
|
+
) {
|
|
184
|
+
names.push(
|
|
185
|
+
...namedExports(resolveTypeScriptModule(file, statement.moduleSpecifier.text), seen),
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
continue
|
|
189
|
+
}
|
|
190
|
+
if (!hasExportModifier(statement)) continue
|
|
191
|
+
if (ts.isVariableStatement(statement)) {
|
|
192
|
+
names.push(
|
|
193
|
+
...statement.declarationList.declarations.flatMap(({ name }) => bindingNames(name)),
|
|
194
|
+
)
|
|
195
|
+
} else if (
|
|
196
|
+
(ts.isClassDeclaration(statement) ||
|
|
197
|
+
ts.isEnumDeclaration(statement) ||
|
|
198
|
+
ts.isFunctionDeclaration(statement) ||
|
|
199
|
+
ts.isInterfaceDeclaration(statement) ||
|
|
200
|
+
ts.isTypeAliasDeclaration(statement)) &&
|
|
201
|
+
statement.name
|
|
202
|
+
) {
|
|
203
|
+
names.push(statement.name.text)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return names
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function moduleSpecifiers(file: string): string[] {
|
|
210
|
+
const found: string[] = []
|
|
211
|
+
const visit = (node: ts.Node): void => {
|
|
212
|
+
if (
|
|
213
|
+
(ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) &&
|
|
214
|
+
node.moduleSpecifier &&
|
|
215
|
+
ts.isStringLiteralLike(node.moduleSpecifier)
|
|
216
|
+
) {
|
|
217
|
+
found.push(node.moduleSpecifier.text)
|
|
218
|
+
} else if (
|
|
219
|
+
ts.isImportEqualsDeclaration(node) &&
|
|
220
|
+
ts.isExternalModuleReference(node.moduleReference) &&
|
|
221
|
+
node.moduleReference.expression &&
|
|
222
|
+
ts.isStringLiteralLike(node.moduleReference.expression)
|
|
223
|
+
) {
|
|
224
|
+
found.push(node.moduleReference.expression.text)
|
|
225
|
+
} else if (
|
|
226
|
+
ts.isCallExpression(node) &&
|
|
227
|
+
node.arguments.length === 1 &&
|
|
228
|
+
ts.isStringLiteralLike(node.arguments[0]!) &&
|
|
229
|
+
(node.expression.kind === ts.SyntaxKind.ImportKeyword ||
|
|
230
|
+
(ts.isIdentifier(node.expression) && node.expression.text === 'require'))
|
|
231
|
+
) {
|
|
232
|
+
found.push(node.arguments[0]!.text)
|
|
233
|
+
}
|
|
234
|
+
ts.forEachChild(node, visit)
|
|
235
|
+
}
|
|
236
|
+
visit(parseTypeScript(file))
|
|
237
|
+
return found
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function declaredTypes(file: string, candidates: ReadonlySet<string>): string[] {
|
|
241
|
+
const text = readFileSync(file, 'utf8')
|
|
242
|
+
if (![...candidates].some((name) => text.includes(name))) return []
|
|
243
|
+
return parseTypeScript(file).statements.flatMap((statement) => {
|
|
244
|
+
if (
|
|
245
|
+
!ts.isInterfaceDeclaration(statement) &&
|
|
246
|
+
!ts.isTypeAliasDeclaration(statement) &&
|
|
247
|
+
!ts.isClassDeclaration(statement) &&
|
|
248
|
+
!ts.isEnumDeclaration(statement)
|
|
249
|
+
) {
|
|
250
|
+
return []
|
|
251
|
+
}
|
|
252
|
+
const name = statement.name?.text
|
|
253
|
+
return name && candidates.has(name) ? [name] : []
|
|
254
|
+
})
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
test('types.ts remains a re-export-only compatibility facade', () => {
|
|
258
|
+
const source = parseTypeScript(join(SHARED_ROOT, 'types.ts'))
|
|
259
|
+
const exports = source.statements.flatMap((statement) => {
|
|
260
|
+
if (
|
|
261
|
+
!ts.isExportDeclaration(statement) ||
|
|
262
|
+
statement.exportClause ||
|
|
263
|
+
!statement.moduleSpecifier ||
|
|
264
|
+
!ts.isStringLiteral(statement.moduleSpecifier)
|
|
265
|
+
) {
|
|
266
|
+
return []
|
|
267
|
+
}
|
|
268
|
+
return [statement.moduleSpecifier.text]
|
|
269
|
+
})
|
|
270
|
+
expect(exports).toEqual([
|
|
271
|
+
'./schema/identity',
|
|
272
|
+
'./contracts/schema',
|
|
273
|
+
'./contracts/workspace',
|
|
274
|
+
'./contracts/agent',
|
|
275
|
+
'./contracts/runtime',
|
|
276
|
+
])
|
|
277
|
+
expect(source.statements.every(ts.isExportDeclaration)).toBe(true)
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
test('types.ts exhaustively preserves its historical named export surface', () => {
|
|
281
|
+
const exports = namedExports(join(SHARED_ROOT, 'types.ts'))
|
|
282
|
+
const duplicates = exports.filter((name, index) => exports.indexOf(name) !== index)
|
|
283
|
+
const expected = [...HISTORICAL_BARREL_EXPORTS, ...INTENTIONAL_BARREL_ADDITIONS].sort()
|
|
284
|
+
|
|
285
|
+
expect(duplicates).toEqual([])
|
|
286
|
+
expect(exports.sort()).toEqual(expected)
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
test('persisted layout and visibility shapes each have one semantic owner', () => {
|
|
290
|
+
const files = productionFiles(STUDIO_ROOT)
|
|
291
|
+
const names = new Set(['LayoutState', 'VisibilityState'])
|
|
292
|
+
const owners = new Map([...names].map((name) => [name, [] as string[]]))
|
|
293
|
+
for (const file of files) {
|
|
294
|
+
for (const name of declaredTypes(file, names)) {
|
|
295
|
+
owners.get(name)!.push(relative(STUDIO_ROOT, file).replaceAll('\\', '/'))
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
expect(Object.fromEntries(owners)).toEqual({
|
|
299
|
+
LayoutState: ['shared/contracts/workspace.ts'],
|
|
300
|
+
VisibilityState: ['shared/contracts/workspace.ts'],
|
|
301
|
+
})
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
test('contract modules follow the documented dependency direction', () => {
|
|
305
|
+
const expectedImports = new Map([
|
|
306
|
+
['schema/identity.ts', []],
|
|
307
|
+
['contracts/schema.ts', ['../schema/identity']],
|
|
308
|
+
['contracts/workspace.ts', ['./schema']],
|
|
309
|
+
['contracts/agent.ts', ['./workspace']],
|
|
310
|
+
['contracts/runtime.ts', ['./agent']],
|
|
311
|
+
])
|
|
312
|
+
|
|
313
|
+
for (const [file, expected] of expectedImports) {
|
|
314
|
+
const imports = moduleSpecifiers(join(SHARED_ROOT, file)).filter((specifier) =>
|
|
315
|
+
specifier.startsWith('.'),
|
|
316
|
+
)
|
|
317
|
+
expect(imports).toEqual(expected)
|
|
318
|
+
}
|
|
319
|
+
})
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared workspace and persisted-state contracts.
|
|
3
|
+
*
|
|
4
|
+
* This module owns the JSON shapes written below `.domain-studio/` and the
|
|
5
|
+
* workspace summaries derived around them. Server stores consume these types;
|
|
6
|
+
* they do not redeclare the persisted state.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { SchemaRevision } from './schema'
|
|
10
|
+
|
|
11
|
+
/** A kernel instance the CLI knows about (a local bookmark or an admin-managed one). */
|
|
12
|
+
export interface InstanceInfo {
|
|
13
|
+
name: string
|
|
14
|
+
url: string
|
|
15
|
+
active: boolean
|
|
16
|
+
kind: 'bookmark' | 'managed'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Global (NOT per-domain) — the CLI's instances + which is active. Source of truth: `astrale instance list`. */
|
|
20
|
+
export interface InstancesState {
|
|
21
|
+
active: string | null
|
|
22
|
+
instances: InstanceInfo[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** What the studio recorded the last time it ran a deploy for this domain. */
|
|
26
|
+
export interface DeployRecord {
|
|
27
|
+
at: string
|
|
28
|
+
/** UI/cache fingerprint recorded by Studio; not a canonical schema revision. */
|
|
29
|
+
renderFingerprint: string
|
|
30
|
+
ok: boolean
|
|
31
|
+
url?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Per-domain deploy/install state (the active instance is global — see InstancesState). */
|
|
35
|
+
export interface InstanceStatus {
|
|
36
|
+
/** the domain's configured deploy target (astrale.config.ts prod.instance) */
|
|
37
|
+
deployTarget: string | null
|
|
38
|
+
/** has a `prod` script → deployable via `pnpm prod` */
|
|
39
|
+
deployable: boolean
|
|
40
|
+
/** GROUND TRUTH — queried from `astrale introspect <origin> --bundle`, not local state.
|
|
41
|
+
* 'unknown' = the instance couldn't be reached/queried (no target, not authed, offline). */
|
|
42
|
+
install: 'installed' | 'not-installed' | 'unknown'
|
|
43
|
+
/** local schema vs the schema actually INSTALLED on the instance (ground truth) */
|
|
44
|
+
drift: 'in-sync' | 'drifted' | 'unknown'
|
|
45
|
+
/** Canonical revisions compared only after local and installed SDK admission. */
|
|
46
|
+
localRevision: SchemaRevision | null
|
|
47
|
+
installedRevision: SchemaRevision | null
|
|
48
|
+
/** the studio's own last deploy — supplementary (service URL / when), not the source of truth */
|
|
49
|
+
lastDeploy: DeployRecord | null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface DeployResult {
|
|
53
|
+
ok: boolean
|
|
54
|
+
url: string | null
|
|
55
|
+
output: string
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type ThreadRole = 'user' | 'author'
|
|
59
|
+
export type ThreadEntryType = 'text' | 'choice'
|
|
60
|
+
|
|
61
|
+
export interface ThreadEntry {
|
|
62
|
+
id: string
|
|
63
|
+
role: ThreadRole
|
|
64
|
+
type: ThreadEntryType
|
|
65
|
+
text: string
|
|
66
|
+
options?: string[]
|
|
67
|
+
answer?: string | null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type AnchorKind = 'schema' | 'section' | 'file' | 'free'
|
|
71
|
+
|
|
72
|
+
export interface AnchorRef {
|
|
73
|
+
ref: string
|
|
74
|
+
kind: AnchorKind
|
|
75
|
+
file?: string
|
|
76
|
+
startLine?: number
|
|
77
|
+
endLine?: number
|
|
78
|
+
label?: string
|
|
79
|
+
/** Flow-canvas coordinates for section-level canvas comments. */
|
|
80
|
+
x?: number
|
|
81
|
+
y?: number
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface Comment {
|
|
85
|
+
id: string
|
|
86
|
+
/** annotate-compatible: array of string excerpts */
|
|
87
|
+
anchors: string[]
|
|
88
|
+
/** studio-only, index-aligned to `anchors` */
|
|
89
|
+
anchorRefs: AnchorRef[]
|
|
90
|
+
status: 'open' | 'closed'
|
|
91
|
+
closeNote?: string
|
|
92
|
+
thread: ThreadEntry[]
|
|
93
|
+
createdAt: string
|
|
94
|
+
/** derived: a thread whose first entry is role:'author' is a question */
|
|
95
|
+
kind: 'comment' | 'question'
|
|
96
|
+
/** set when the anchor ref no longer resolves against the current schema */
|
|
97
|
+
orphaned?: boolean
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface CommentStore {
|
|
101
|
+
/** Historical annotate wire key; contains Studio's render fingerprint. */
|
|
102
|
+
schemaVersion: string
|
|
103
|
+
comments: Comment[]
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface MergeResult {
|
|
107
|
+
merged: number
|
|
108
|
+
closed: number
|
|
109
|
+
unknownIds: string[]
|
|
110
|
+
schemaMismatch: boolean
|
|
111
|
+
pastedSchemaVersion?: string
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export type SchemaChangeKind =
|
|
115
|
+
| 'schema-metadata-changed'
|
|
116
|
+
| 'type-added'
|
|
117
|
+
| 'type-removed'
|
|
118
|
+
| 'type-changed'
|
|
119
|
+
| 'import-added'
|
|
120
|
+
| 'import-removed'
|
|
121
|
+
| 'import-changed'
|
|
122
|
+
| 'class-added'
|
|
123
|
+
| 'class-removed'
|
|
124
|
+
| 'class-contract-changed'
|
|
125
|
+
| 'interface-added'
|
|
126
|
+
| 'interface-removed'
|
|
127
|
+
| 'interface-contract-changed'
|
|
128
|
+
| 'edge-added'
|
|
129
|
+
| 'edge-removed'
|
|
130
|
+
| 'edge-contract-changed'
|
|
131
|
+
| 'definition-metadata-changed'
|
|
132
|
+
| 'prop-added'
|
|
133
|
+
| 'prop-removed'
|
|
134
|
+
| 'prop-type-changed'
|
|
135
|
+
| 'prop-schema-changed'
|
|
136
|
+
| 'prop-required-changed'
|
|
137
|
+
| 'method-added'
|
|
138
|
+
| 'method-removed'
|
|
139
|
+
| 'method-signature-changed'
|
|
140
|
+
| 'method-metadata-changed'
|
|
141
|
+
| 'function-added'
|
|
142
|
+
| 'function-removed'
|
|
143
|
+
| 'function-signature-changed'
|
|
144
|
+
| 'function-metadata-changed'
|
|
145
|
+
| 'view-added'
|
|
146
|
+
| 'view-removed'
|
|
147
|
+
| 'view-changed'
|
|
148
|
+
| 'view-metadata-changed'
|
|
149
|
+
| 'policy-added'
|
|
150
|
+
| 'policy-removed'
|
|
151
|
+
| 'policy-changed'
|
|
152
|
+
| 'dependency-added'
|
|
153
|
+
| 'dependency-removed'
|
|
154
|
+
| 'dependency-changed'
|
|
155
|
+
| 'core-changed'
|
|
156
|
+
|
|
157
|
+
export interface SchemaChange {
|
|
158
|
+
kind: SchemaChangeKind
|
|
159
|
+
target: string
|
|
160
|
+
detail?: string
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface FileChange {
|
|
164
|
+
file: string
|
|
165
|
+
status: 'added' | 'modified' | 'removed'
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export interface ChangeSet {
|
|
169
|
+
source: 'baseline' | 'git'
|
|
170
|
+
hasGit: boolean
|
|
171
|
+
hasBaseline: boolean
|
|
172
|
+
schemaChanges: SchemaChange[]
|
|
173
|
+
fileChanges: FileChange[]
|
|
174
|
+
schemaDiffText?: string
|
|
175
|
+
/** Describes Studio-observed structure only; never a Runtime migration verdict. */
|
|
176
|
+
structuralStatus: 'none' | 'changed'
|
|
177
|
+
baselineRevision?: SchemaRevision
|
|
178
|
+
currentRevision?: SchemaRevision
|
|
179
|
+
baselineCapturedAt?: string
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface ContextItem {
|
|
183
|
+
id: string
|
|
184
|
+
bucket: 'user' | 'auto'
|
|
185
|
+
title: string
|
|
186
|
+
body: string
|
|
187
|
+
source?: string
|
|
188
|
+
updatedAt: string
|
|
189
|
+
/** auto items: included in the copy payload only when true */
|
|
190
|
+
includeInHandoff?: boolean
|
|
191
|
+
freshness?: string
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export interface ContextStore {
|
|
195
|
+
user: ContextItem[]
|
|
196
|
+
auto: ContextItem[]
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** A document the user drops in as context for the AI agent. */
|
|
200
|
+
export interface DocMeta {
|
|
201
|
+
id: string
|
|
202
|
+
name: string
|
|
203
|
+
type: string
|
|
204
|
+
size: number
|
|
205
|
+
addedAt: string
|
|
206
|
+
updatedAt?: string
|
|
207
|
+
/** relative path under the dotted folder (server-side) */
|
|
208
|
+
stored: string
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export interface Integration {
|
|
212
|
+
id: string
|
|
213
|
+
name: string
|
|
214
|
+
kind: string
|
|
215
|
+
status: 'planned' | 'active' | 'deprecated' | string
|
|
216
|
+
notes?: string
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export interface IntegrationsState {
|
|
220
|
+
integrations: Integration[]
|
|
221
|
+
detectedSubfolders: string[]
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** A domain in the catalog — local to this workspace, a faked external service, or the kernel. */
|
|
225
|
+
export interface DomainCatalogEntry {
|
|
226
|
+
origin: string
|
|
227
|
+
name: string
|
|
228
|
+
kind: 'kernel' | 'local' | 'external'
|
|
229
|
+
description: string
|
|
230
|
+
/** raw lucide-style SVG icon (stroke="currentColor") */
|
|
231
|
+
icon: string
|
|
232
|
+
/** kernel is always present + required */
|
|
233
|
+
required?: boolean
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export interface DomainSummary {
|
|
237
|
+
id: string
|
|
238
|
+
origin: string
|
|
239
|
+
path: string
|
|
240
|
+
schemaDir: string
|
|
241
|
+
depsInstalled: boolean
|
|
242
|
+
hasGit: boolean
|
|
243
|
+
configFile: string
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export interface CopyPayload {
|
|
247
|
+
markdown: string
|
|
248
|
+
openComments: number
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export interface NodePosition {
|
|
252
|
+
x: number
|
|
253
|
+
y: number
|
|
254
|
+
/** persisted size — only expanded module containers carry one. */
|
|
255
|
+
w?: number
|
|
256
|
+
h?: number
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Persisted manual graph layout. This is the sole authority for its JSON shape. */
|
|
260
|
+
export interface LayoutState {
|
|
261
|
+
renderFingerprint?: string
|
|
262
|
+
positions: Record<string, NodePosition>
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Persisted per-domain canvas visibility. This is the sole authority for its JSON shape. */
|
|
266
|
+
export interface VisibilityState {
|
|
267
|
+
/** refs hidden on the canvas: `class.X` | `edge.X` | `domain.<origin>` (interfaces use
|
|
268
|
+
* `materializedInterfaces`, not this set — see below) */
|
|
269
|
+
hidden: Record<string, true>
|
|
270
|
+
showInheritedEdges: boolean
|
|
271
|
+
/** Local interfaces MATERIALIZED as canvas NODES (default = badge). Keyed by BARE interface
|
|
272
|
+
* name (e.g. `Fulfillable`) — a set distinct from `hidden`, so no ref namespace collision. */
|
|
273
|
+
materializedInterfaces: Record<string, true>
|
|
274
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test'
|
|
2
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs'
|
|
3
|
+
import { dirname, join, relative, resolve } from 'node:path'
|
|
4
|
+
|
|
5
|
+
const STUDIO_ROOT = resolve(import.meta.dir, '..')
|
|
6
|
+
const CLIENT_ROOT = join(STUDIO_ROOT, 'client', 'src')
|
|
7
|
+
const SERVER_ROOT = join(STUDIO_ROOT, 'server')
|
|
8
|
+
const SHARED_ROOT = join(STUDIO_ROOT, 'shared')
|
|
9
|
+
const STATE_ROOT = join(SERVER_ROOT, 'state')
|
|
10
|
+
|
|
11
|
+
function productionFiles(dir: string): string[] {
|
|
12
|
+
return readdirSync(dir)
|
|
13
|
+
.flatMap((entry) => {
|
|
14
|
+
const path = join(dir, entry)
|
|
15
|
+
if (statSync(path).isDirectory()) return productionFiles(path)
|
|
16
|
+
if (!/\.(?:ts|tsx)$/.test(path) || /\.test\.(?:ts|tsx)$/.test(path)) return []
|
|
17
|
+
return [path]
|
|
18
|
+
})
|
|
19
|
+
.sort()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const files = [
|
|
23
|
+
...productionFiles(CLIENT_ROOT),
|
|
24
|
+
...productionFiles(SERVER_ROOT),
|
|
25
|
+
...productionFiles(SHARED_ROOT),
|
|
26
|
+
]
|
|
27
|
+
const fileSet = new Set(files)
|
|
28
|
+
|
|
29
|
+
function resolveLocalImport(file: string, specifier: string): string | null {
|
|
30
|
+
const target = specifier.startsWith('@/')
|
|
31
|
+
? join(CLIENT_ROOT, specifier.slice(2))
|
|
32
|
+
: specifier.startsWith('@shared/')
|
|
33
|
+
? join(SHARED_ROOT, specifier.slice('@shared/'.length))
|
|
34
|
+
: specifier.startsWith('.')
|
|
35
|
+
? resolve(dirname(file), specifier)
|
|
36
|
+
: null
|
|
37
|
+
if (!target) return null
|
|
38
|
+
for (const candidate of [
|
|
39
|
+
target,
|
|
40
|
+
`${target}.ts`,
|
|
41
|
+
`${target}.tsx`,
|
|
42
|
+
join(target, 'index.ts'),
|
|
43
|
+
join(target, 'index.tsx'),
|
|
44
|
+
]) {
|
|
45
|
+
if (fileSet.has(candidate)) return candidate
|
|
46
|
+
}
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function imports(file: string): string[] {
|
|
51
|
+
const source = readFileSync(file, 'utf8')
|
|
52
|
+
const found: string[] = []
|
|
53
|
+
const pattern = /(?:from\s+|import\s*(?:\(\s*)?)['"]([^'"]+)['"]\s*\)?|import\s+['"]([^'"]+)['"]/g
|
|
54
|
+
for (const match of source.matchAll(pattern)) {
|
|
55
|
+
const target = resolveLocalImport(file, match[1] ?? match[2])
|
|
56
|
+
if (target) found.push(target)
|
|
57
|
+
}
|
|
58
|
+
return found
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const studioPath = (file: string) => relative(STUDIO_ROOT, file).replaceAll('\\', '/')
|
|
62
|
+
|
|
63
|
+
test('Studio production modules form an acyclic dependency graph', () => {
|
|
64
|
+
const graph = new Map(files.map((file) => [file, imports(file)]))
|
|
65
|
+
const visiting = new Set<string>()
|
|
66
|
+
const visited = new Set<string>()
|
|
67
|
+
const stack: string[] = []
|
|
68
|
+
const cycles: string[][] = []
|
|
69
|
+
|
|
70
|
+
const visit = (file: string) => {
|
|
71
|
+
if (visiting.has(file)) {
|
|
72
|
+
const start = stack.indexOf(file)
|
|
73
|
+
cycles.push([...stack.slice(start), file].map(studioPath))
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
if (visited.has(file)) return
|
|
77
|
+
visiting.add(file)
|
|
78
|
+
stack.push(file)
|
|
79
|
+
for (const dependency of graph.get(file) ?? []) visit(dependency)
|
|
80
|
+
stack.pop()
|
|
81
|
+
visiting.delete(file)
|
|
82
|
+
visited.add(file)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (const file of files) visit(file)
|
|
86
|
+
expect(cycles).toEqual([])
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('production boundaries keep client/shared out of server and shared out of client', () => {
|
|
90
|
+
const violations = files.flatMap((file) =>
|
|
91
|
+
imports(file)
|
|
92
|
+
.filter(
|
|
93
|
+
(dependency) =>
|
|
94
|
+
(dependency.startsWith(SERVER_ROOT) &&
|
|
95
|
+
(file.startsWith(CLIENT_ROOT) || file.startsWith(SHARED_ROOT))) ||
|
|
96
|
+
(file.startsWith(SHARED_ROOT) && dependency.startsWith(CLIENT_ROOT)),
|
|
97
|
+
)
|
|
98
|
+
.map((dependency) => `${studioPath(file)} -> ${studioPath(dependency)}`),
|
|
99
|
+
)
|
|
100
|
+
expect(violations).toEqual([])
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('state repositories do not depend on workflow modules', () => {
|
|
104
|
+
const workflowRoots = ['api', 'environment', 'handoff', 'instances', 'views', 'workspace'].map(
|
|
105
|
+
(directory) => join(SERVER_ROOT, directory),
|
|
106
|
+
)
|
|
107
|
+
const edges = files
|
|
108
|
+
.filter((file) => file.startsWith(STATE_ROOT))
|
|
109
|
+
.flatMap((file) =>
|
|
110
|
+
imports(file)
|
|
111
|
+
.filter((dependency) => workflowRoots.some((root) => dependency.startsWith(root)))
|
|
112
|
+
.map((dependency) => `${studioPath(file)} -> ${studioPath(dependency)}`),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
expect(edges).toEqual([])
|
|
116
|
+
})
|