@ankhorage/infra 3.3.0 → 4.1.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/CHANGELOG.md +12 -0
- package/README.md +6 -7
- package/dist/adapters/minikube/base/index.js +26 -10
- package/dist/adapters/minikube/base/index.js.map +1 -1
- package/dist/adapters/minikube/index.d.ts.map +1 -1
- package/dist/adapters/minikube/index.js +2 -16
- package/dist/adapters/minikube/index.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/infraValidation.d.ts +1 -0
- package/dist/infraValidation.d.ts.map +1 -1
- package/dist/infraValidation.js +6 -1
- package/dist/infraValidation.js.map +1 -1
- package/dist/project/index.d.ts +1 -1
- package/dist/project/index.d.ts.map +1 -1
- package/dist/project/index.js +1 -1
- package/dist/project/index.js.map +1 -1
- package/dist/readme-usage.js +5 -0
- package/dist/readme-usage.js.map +1 -1
- package/dist/runtime.d.ts +6 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +24 -0
- package/dist/runtime.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +7 -7
- package/src/adapters/minikube/README.md +9 -1
- package/src/adapters/minikube/base/index.test.ts +33 -0
- package/src/adapters/minikube/base/index.ts +28 -10
- package/src/adapters/minikube/base/portForwardRuntime.test.ts +288 -0
- package/src/adapters/minikube/base/twoAppIsolation.integration.test.ts +272 -9
- package/src/adapters/minikube/index.ts +2 -21
- package/src/apiInfrastructure.test.ts +69 -0
- package/src/generatedApiLedgerCleanup.test.ts +72 -0
- package/src/index.ts +0 -6
- package/src/infraApiValidation.test.ts +42 -0
- package/src/infraValidation.ts +10 -1
- package/src/project/index.test.ts +46 -0
- package/src/project/index.ts +1 -0
- package/src/readme-usage.ts +5 -0
- package/src/runtime.ts +31 -0
- package/src/types.ts +1 -1
- package/dist/generatedApiDatabase.d.ts +0 -20
- package/dist/generatedApiDatabase.d.ts.map +0 -1
- package/dist/generatedApiDatabase.js +0 -343
- package/dist/generatedApiDatabase.js.map +0 -1
- package/src/generatedApiDatabase.test.ts +0 -355
- package/src/generatedApiDatabase.ts +0 -558
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import { generateInfrastructure } from './index';
|
|
4
|
+
import { createAppManifest } from './testSupport';
|
|
5
|
+
import type { InfraManifestInput } from './types';
|
|
6
|
+
|
|
7
|
+
const DEPLOYMENT = { target: 'minikube', monitoring: false } as const;
|
|
8
|
+
|
|
9
|
+
describe('canonical API infrastructure', () => {
|
|
10
|
+
test('treats external APIs as infrastructure no-ops', () => {
|
|
11
|
+
const manifest: InfraManifestInput = {
|
|
12
|
+
deployment: DEPLOYMENT,
|
|
13
|
+
modules: [],
|
|
14
|
+
apis: [
|
|
15
|
+
{
|
|
16
|
+
id: 'nutrition',
|
|
17
|
+
origin: 'external',
|
|
18
|
+
protocol: 'rest',
|
|
19
|
+
baseUrl: 'https://api.ankhorage.com/v1/nutrition',
|
|
20
|
+
endpoints: {},
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const result = generateInfrastructure(manifest, {
|
|
26
|
+
appManifest: createAppManifest('nutrition', manifest),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
expect(result.warnings).not.toContainEqual(expect.stringContaining('infra.apis.nutrition'));
|
|
30
|
+
expect(listApiArtifactPaths(result.files)).toEqual([]);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('warns for internal APIs without generating partial API infrastructure', () => {
|
|
34
|
+
const manifest: InfraManifestInput = {
|
|
35
|
+
deployment: DEPLOYMENT,
|
|
36
|
+
modules: [],
|
|
37
|
+
apis: [
|
|
38
|
+
{
|
|
39
|
+
id: 'orders',
|
|
40
|
+
origin: 'internal',
|
|
41
|
+
protocol: 'rest',
|
|
42
|
+
basePath: '/api/orders',
|
|
43
|
+
endpoints: {},
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const result = generateInfrastructure(manifest, {
|
|
49
|
+
appManifest: createAppManifest('orders', manifest),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
expect(result.warnings).toContain(
|
|
53
|
+
'infra.apis.orders has origin "internal", but internal API provisioning is not supported in Phase 1; no API infrastructure will be generated.',
|
|
54
|
+
);
|
|
55
|
+
expect(listApiArtifactPaths(result.files)).toEqual([]);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
function listApiArtifactPaths(files: readonly { readonly path: string }[]): readonly string[] {
|
|
60
|
+
return files
|
|
61
|
+
.map((file) => file.path)
|
|
62
|
+
.filter(
|
|
63
|
+
(path) =>
|
|
64
|
+
path.includes('generated-api') ||
|
|
65
|
+
path.includes('generated_api') ||
|
|
66
|
+
path.includes('/api/deployment') ||
|
|
67
|
+
path.includes('/api/service'),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
import { syncProjectInfrastructure } from './projectInfrastructure';
|
|
7
|
+
import { createAppManifest } from './testSupport';
|
|
8
|
+
|
|
9
|
+
const OBSOLETE_FILES = [
|
|
10
|
+
'infra/minikube/db/generated-api-resources.json',
|
|
11
|
+
'infra/minikube/db/migrations/001_generated_api_resources.sql',
|
|
12
|
+
] as const;
|
|
13
|
+
|
|
14
|
+
test('removes obsolete generated API database artifacts tracked by the infra ledger', async () => {
|
|
15
|
+
const projectPath = await fs.mkdtemp(path.join(os.tmpdir(), 'infra-api-cleanup-'));
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
await writeObsoleteArtifacts(projectPath);
|
|
19
|
+
const manifest = createAppManifest('shop', {
|
|
20
|
+
deployment: { target: 'minikube', monitoring: false },
|
|
21
|
+
modules: [],
|
|
22
|
+
});
|
|
23
|
+
const result = await syncProjectInfrastructure({
|
|
24
|
+
projectId: 'shop',
|
|
25
|
+
projectPath,
|
|
26
|
+
manifest,
|
|
27
|
+
generateInfrastructureImpl: () => ({
|
|
28
|
+
files: [],
|
|
29
|
+
warnings: [],
|
|
30
|
+
meta: { target: 'minikube', providers: [] },
|
|
31
|
+
dependencies: [],
|
|
32
|
+
}),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
expect(result.removed).toBe(OBSOLETE_FILES.length);
|
|
36
|
+
for (const filePath of OBSOLETE_FILES) {
|
|
37
|
+
expect(await pathExists(path.join(projectPath, filePath))).toBe(false);
|
|
38
|
+
}
|
|
39
|
+
} finally {
|
|
40
|
+
await fs.rm(projectPath, { force: true, recursive: true });
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
async function writeObsoleteArtifacts(projectPath: string): Promise<void> {
|
|
45
|
+
for (const filePath of OBSOLETE_FILES) {
|
|
46
|
+
const absolutePath = path.join(projectPath, filePath);
|
|
47
|
+
await fs.mkdir(path.dirname(absolutePath), { recursive: true });
|
|
48
|
+
await fs.writeFile(absolutePath, 'obsolete', 'utf8');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
await fs.mkdir(path.join(projectPath, '.ankh'), { recursive: true });
|
|
52
|
+
await fs.writeFile(
|
|
53
|
+
path.join(projectPath, '.ankh/infra-ledger.json'),
|
|
54
|
+
JSON.stringify({
|
|
55
|
+
schemaVersion: 1,
|
|
56
|
+
generatedAt: new Date().toISOString(),
|
|
57
|
+
target: 'minikube',
|
|
58
|
+
files: OBSOLETE_FILES,
|
|
59
|
+
warnings: [],
|
|
60
|
+
}),
|
|
61
|
+
'utf8',
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function pathExists(filePath: string): Promise<boolean> {
|
|
66
|
+
try {
|
|
67
|
+
await fs.access(filePath);
|
|
68
|
+
return true;
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -14,12 +14,6 @@ export {
|
|
|
14
14
|
resolveAuthRedirectConfiguration,
|
|
15
15
|
type ResolveAuthRedirectConfigurationInput,
|
|
16
16
|
} from './authRedirects';
|
|
17
|
-
export {
|
|
18
|
-
type GeneratedApiDatabaseArtifacts,
|
|
19
|
-
type GeneratedApiDatabaseDiagnostic,
|
|
20
|
-
type GeneratedApiDatabaseDiagnosticCode,
|
|
21
|
-
generateGeneratedApiDatabaseArtifacts,
|
|
22
|
-
} from './generatedApiDatabase';
|
|
23
17
|
export {
|
|
24
18
|
createInfraSecretStoreAdapter,
|
|
25
19
|
type CreateInfraSecretStoreAdapterInput,
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import { validateInfraSupport } from './infraValidation';
|
|
4
|
+
import type { InfraManifestInput } from './types';
|
|
5
|
+
|
|
6
|
+
describe('canonical API support validation', () => {
|
|
7
|
+
test('does not warn for external APIs', () => {
|
|
8
|
+
const manifest: InfraManifestInput = {
|
|
9
|
+
modules: [],
|
|
10
|
+
apis: [
|
|
11
|
+
{
|
|
12
|
+
id: 'nutrition',
|
|
13
|
+
origin: 'external',
|
|
14
|
+
protocol: 'rest',
|
|
15
|
+
baseUrl: 'https://api.ankhorage.com/v1/nutrition',
|
|
16
|
+
endpoints: {},
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
expect(validateInfraSupport(manifest)).toEqual([]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('reports every internal API as unsupported without treating it as invalid', () => {
|
|
25
|
+
const manifest: InfraManifestInput = {
|
|
26
|
+
modules: [],
|
|
27
|
+
apis: [
|
|
28
|
+
{
|
|
29
|
+
id: 'orders',
|
|
30
|
+
origin: 'internal',
|
|
31
|
+
protocol: 'rest',
|
|
32
|
+
basePath: '/api/orders',
|
|
33
|
+
endpoints: {},
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
expect(validateInfraSupport(manifest)).toEqual([
|
|
39
|
+
'infra.apis.orders has origin "internal", but internal API provisioning is not supported in Phase 1; no API infrastructure will be generated.',
|
|
40
|
+
]);
|
|
41
|
+
});
|
|
42
|
+
});
|
package/src/infraValidation.ts
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
import type { InfraManifestInput } from './types.js';
|
|
10
10
|
|
|
11
11
|
export function validateInfraSupport(manifest: InfraManifestInput): readonly string[] {
|
|
12
|
-
const warnings: string[] = [];
|
|
12
|
+
const warnings: string[] = [...validateInfraApiSupport(manifest)];
|
|
13
13
|
|
|
14
14
|
const deploymentTarget = manifest.deployment?.target;
|
|
15
15
|
if (deploymentTarget !== undefined && !isSupported(deploymentTarget, DEPLOYMENT_TARGETS)) {
|
|
@@ -69,6 +69,15 @@ export function validateInfraSupport(manifest: InfraManifestInput): readonly str
|
|
|
69
69
|
return warnings;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
export function validateInfraApiSupport(manifest: InfraManifestInput): readonly string[] {
|
|
73
|
+
return (manifest.apis ?? [])
|
|
74
|
+
.filter((api) => api.origin === 'internal')
|
|
75
|
+
.map(
|
|
76
|
+
(api) =>
|
|
77
|
+
`infra.apis.${api.id} has origin "internal", but internal API provisioning is not supported in Phase 1; no API infrastructure will be generated.`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
72
81
|
function warningForUnsupportedValue(args: {
|
|
73
82
|
readonly path: string;
|
|
74
83
|
readonly value: string;
|
|
@@ -5,6 +5,7 @@ import path from 'path';
|
|
|
5
5
|
|
|
6
6
|
import { createAppManifest } from '../testSupport.js';
|
|
7
7
|
import {
|
|
8
|
+
ensureProjectInfrastructureRuntime,
|
|
8
9
|
inspectProjectInfrastructure,
|
|
9
10
|
readProjectInfrastructureEnvironment,
|
|
10
11
|
resolveProjectInfrastructureDatabaseUrl,
|
|
@@ -78,6 +79,39 @@ describe('@ankhorage/infra/project', () => {
|
|
|
78
79
|
});
|
|
79
80
|
});
|
|
80
81
|
|
|
82
|
+
test('ensures the generated runtime port-forward group through the owner API', async () => {
|
|
83
|
+
const projectPath = await createProjectFixture();
|
|
84
|
+
const scriptPath = path.join(projectPath, 'infra', 'minikube', 'scripts', 'port-forward.sh');
|
|
85
|
+
await fs.writeFile(scriptPath, '#!/usr/bin/env bash\nprintf \'%s\' "$*"\n', 'utf8');
|
|
86
|
+
|
|
87
|
+
const result = await ensureProjectInfrastructureRuntime({
|
|
88
|
+
projectId: 'example',
|
|
89
|
+
projectPath,
|
|
90
|
+
target: 'minikube',
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(result).toEqual({ stdout: 'start runtime', stderr: '' });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('reports actionable runtime-forward failures without reconciling infrastructure', async () => {
|
|
97
|
+
const projectPath = await createProjectFixture();
|
|
98
|
+
const scriptPath = path.join(projectPath, 'infra', 'minikube', 'scripts', 'port-forward.sh');
|
|
99
|
+
await fs.writeFile(
|
|
100
|
+
scriptPath,
|
|
101
|
+
'#!/usr/bin/env bash\necho "app: target app/service/app-runtime not found"\nexit 1\n',
|
|
102
|
+
'utf8',
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
await expectRejectMessage(
|
|
106
|
+
ensureProjectInfrastructureRuntime({
|
|
107
|
+
projectId: 'example',
|
|
108
|
+
projectPath,
|
|
109
|
+
target: 'minikube',
|
|
110
|
+
}),
|
|
111
|
+
"Failed to ensure infrastructure runtime for project 'example': app: target app/service/app-runtime not found",
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
81
115
|
test('reads only requested runtime environment values from the generated fallback', async () => {
|
|
82
116
|
const projectPath = await createProjectFixture();
|
|
83
117
|
const result = await readProjectInfrastructureEnvironment({
|
|
@@ -147,3 +181,15 @@ describe('@ankhorage/infra/project', () => {
|
|
|
147
181
|
});
|
|
148
182
|
});
|
|
149
183
|
});
|
|
184
|
+
|
|
185
|
+
async function expectRejectMessage(
|
|
186
|
+
promise: Promise<unknown>,
|
|
187
|
+
expectedMessage: string,
|
|
188
|
+
): Promise<void> {
|
|
189
|
+
try {
|
|
190
|
+
await promise;
|
|
191
|
+
throw new Error('Expected operation to reject.');
|
|
192
|
+
} catch (error) {
|
|
193
|
+
expect(error instanceof Error ? error.message : String(error)).toContain(expectedMessage);
|
|
194
|
+
}
|
|
195
|
+
}
|
package/src/project/index.ts
CHANGED
package/src/readme-usage.ts
CHANGED
|
@@ -29,6 +29,11 @@ import { runCli } from './cli/bin.js';
|
|
|
29
29
|
* operations accept an explicit project path and do not require Studio or a
|
|
30
30
|
* particular workspace layout.
|
|
31
31
|
*
|
|
32
|
+
* Running applications can repair their provider-aware host endpoints through
|
|
33
|
+
* `ensureProjectInfrastructureRuntime()` from `@ankhorage/infra/project`. This
|
|
34
|
+
* operation only ensures the generated runtime port-forward group; it does not
|
|
35
|
+
* deploy, bootstrap, or reconcile infrastructure.
|
|
36
|
+
*
|
|
32
37
|
* @usage
|
|
33
38
|
*/
|
|
34
39
|
await runCli(['--help']);
|
package/src/runtime.ts
CHANGED
|
@@ -85,6 +85,37 @@ export async function resolveProjectInfrastructurePortForward(args: {
|
|
|
85
85
|
};
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/** Ensures the generated host forwards required by a running application are active. */
|
|
89
|
+
export async function ensureProjectInfrastructureRuntime(args: {
|
|
90
|
+
readonly projectId: string;
|
|
91
|
+
readonly projectPath: string;
|
|
92
|
+
readonly target: string;
|
|
93
|
+
}): Promise<InfraScriptOutput> {
|
|
94
|
+
try {
|
|
95
|
+
return await runProjectInfrastructureLifecycle({
|
|
96
|
+
args: ['start', 'runtime'],
|
|
97
|
+
projectId: args.projectId,
|
|
98
|
+
projectPath: args.projectPath,
|
|
99
|
+
script: 'port-forward',
|
|
100
|
+
target: args.target,
|
|
101
|
+
});
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (!(error instanceof InfraScriptExecutionError)) {
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const detail = error.stderr.trim() || error.stdout.trim();
|
|
108
|
+
throw new InfraScriptExecutionError({
|
|
109
|
+
exitCode: error.exitCode,
|
|
110
|
+
message: `Failed to ensure infrastructure runtime for project '${args.projectId}'${
|
|
111
|
+
detail.length > 0 ? `: ${detail}` : '.'
|
|
112
|
+
}`,
|
|
113
|
+
stderr: error.stderr,
|
|
114
|
+
stdout: error.stdout,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
88
119
|
export async function runProjectInfraScript(args: {
|
|
89
120
|
readonly context: InfraCommandContext;
|
|
90
121
|
readonly projectId: string;
|
package/src/types.ts
CHANGED
|
@@ -37,7 +37,7 @@ export interface InfrastructureGenerationOptions {
|
|
|
37
37
|
*/
|
|
38
38
|
readonly appManifest?: Pick<
|
|
39
39
|
AppManifest,
|
|
40
|
-
'deploy' | '
|
|
40
|
+
'deploy' | 'infra' | 'metadata' | 'navigator' | 'screens' | 'settings'
|
|
41
41
|
>;
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import type { GeneratedApiRegistry } from '@ankhorage/contracts';
|
|
2
|
-
import type { GeneratedInfrastructureFile } from './types';
|
|
3
|
-
export type GeneratedApiDatabaseDiagnosticCode = 'duplicate-field' | 'duplicate-resource-id' | 'duplicate-target' | 'invalid-default' | 'invalid-identifier' | 'invalid-primary-key' | 'invalid-seed' | 'registry-id-mismatch' | 'unsupported-policy' | 'unsupported-provider';
|
|
4
|
-
export interface GeneratedApiDatabaseDiagnostic {
|
|
5
|
-
readonly code: GeneratedApiDatabaseDiagnosticCode;
|
|
6
|
-
readonly severity: 'error' | 'warning';
|
|
7
|
-
readonly apiId: string;
|
|
8
|
-
readonly resourceId?: string;
|
|
9
|
-
readonly path?: string;
|
|
10
|
-
readonly message: string;
|
|
11
|
-
}
|
|
12
|
-
export interface GeneratedApiDatabaseArtifacts {
|
|
13
|
-
readonly files: readonly GeneratedInfrastructureFile[];
|
|
14
|
-
readonly diagnostics: readonly GeneratedApiDatabaseDiagnostic[];
|
|
15
|
-
}
|
|
16
|
-
export declare function generateGeneratedApiDatabaseArtifacts(args: {
|
|
17
|
-
readonly generatedApis: GeneratedApiRegistry | undefined;
|
|
18
|
-
readonly databaseProvider: string | undefined;
|
|
19
|
-
}): GeneratedApiDatabaseArtifacts;
|
|
20
|
-
//# sourceMappingURL=generatedApiDatabase.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"generatedApiDatabase.d.ts","sourceRoot":"","sources":["../src/generatedApiDatabase.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAKV,oBAAoB,EAGrB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,SAAS,CAAC;AAE3D,MAAM,MAAM,kCAAkC,GAC1C,iBAAiB,GACjB,uBAAuB,GACvB,kBAAkB,GAClB,iBAAiB,GACjB,oBAAoB,GACpB,qBAAqB,GACrB,cAAc,GACd,sBAAsB,GACtB,oBAAoB,GACpB,sBAAsB,CAAC;AAE3B,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,CAAC,IAAI,EAAE,kCAAkC,CAAC;IAClD,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,KAAK,EAAE,SAAS,2BAA2B,EAAE,CAAC;IACvD,QAAQ,CAAC,WAAW,EAAE,SAAS,8BAA8B,EAAE,CAAC;CACjE;AAmBD,wBAAgB,qCAAqC,CAAC,IAAI,EAAE;IAC1D,QAAQ,CAAC,aAAa,EAAE,oBAAoB,GAAG,SAAS,CAAC;IACzD,QAAQ,CAAC,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAAC;CAC/C,GAAG,6BAA6B,CA2ChC"}
|