proteum 2.4.4 → 2.5.1
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 +81 -52
- package/agents/project/AGENTS.md +112 -31
- package/agents/project/CODING_STYLE.md +2 -2
- package/agents/project/app-root/AGENTS.md +1 -3
- package/agents/project/client/AGENTS.md +5 -1
- package/agents/project/client/pages/AGENTS.md +21 -9
- package/agents/project/diagnostics.md +2 -2
- package/agents/project/optimizations.md +1 -1
- package/agents/project/root/AGENTS.md +105 -22
- package/agents/project/server/routes/AGENTS.md +30 -1
- package/agents/project/server/services/AGENTS.md +4 -0
- package/agents/project/tests/AGENTS.md +1 -1
- package/cli/commands/doctor.ts +54 -3
- package/cli/commands/runtime.ts +6 -0
- package/cli/commands/worktree.ts +116 -0
- package/cli/compiler/artifacts/controllers.ts +16 -15
- package/cli/compiler/artifacts/discovery.ts +129 -17
- package/cli/compiler/artifacts/routing.ts +0 -5
- package/cli/compiler/artifacts/services.ts +253 -76
- package/cli/compiler/common/controllers.ts +159 -57
- package/cli/compiler/common/generatedRouteModules.ts +457 -363
- package/cli/mcp/router.ts +47 -3
- package/cli/presentation/commands.ts +25 -15
- package/cli/runtime/commands.ts +39 -12
- package/cli/runtime/worktreeBootstrap.ts +608 -0
- package/cli/scaffold/index.ts +28 -18
- package/cli/scaffold/templates.ts +44 -33
- package/cli/utils/agents.ts +14 -1
- package/client/app/index.ts +22 -5
- package/client/services/router/index.tsx +23 -3
- package/client/services/router/request/api.ts +16 -6
- package/common/dev/contractsDoctor.ts +1 -1
- package/common/dev/mcpPayloads.ts +8 -1
- package/common/env/proteumEnv.ts +14 -2
- package/common/router/contracts.ts +1 -1
- package/common/router/definitions.ts +177 -0
- package/common/router/index.ts +23 -12
- package/common/router/pageData.ts +5 -5
- package/common/router/register.ts +2 -2
- package/common/router/request/api.ts +12 -2
- package/docs/agent-routing.md +5 -2
- package/docs/diagnostics.md +2 -0
- package/docs/mcp.md +6 -3
- package/docs/migration-2.5.md +226 -0
- package/eslint.js +89 -42
- package/package.json +1 -1
- package/server/app/commands.ts +5 -1
- package/server/app/container/console/index.ts +1 -1
- package/server/app/controller/index.ts +98 -40
- package/server/app/index.ts +120 -3
- package/server/app/service/index.ts +5 -1
- package/server/index.ts +6 -2
- package/server/services/router/index.ts +50 -41
- package/server/services/router/response/index.ts +2 -2
- package/tests/agents-utils.test.cjs +14 -1
- package/tests/cli-mcp-command.test.cjs +84 -0
- package/tests/client-app-error-handling.test.cjs +100 -0
- package/tests/definition-contracts.test.cjs +453 -0
- package/tests/dev-transpile-watch.test.cjs +37 -31
- package/tests/eslint-rules.test.cjs +185 -8
- package/tests/mcp.test.cjs +90 -0
- package/tests/scaffold-templates.test.cjs +18 -0
- package/tests/server-app-report-error.test.cjs +135 -0
- package/tests/worktree-bootstrap.test.cjs +206 -0
- package/types/aliases.d.ts +0 -5
- package/types/controller-input.test.ts +23 -17
- package/types/controller-request-context.test.ts +10 -11
- package/cli/commands/migrate.ts +0 -51
- package/cli/migrate/pageContract.ts +0 -516
- package/docs/migrate-from-2.1.3.md +0 -396
- package/scripts/cleanup-generated-controllers.ts +0 -62
- package/scripts/fix-reference-app-typing.ts +0 -490
- package/scripts/format-router-registrations.ts +0 -119
- package/scripts/migrate-explicit-controllers-and-request.ts +0 -423
- package/scripts/refactor-client-app-imports.ts +0 -244
- package/scripts/refactor-client-pages.ts +0 -587
- package/scripts/refactor-server-controllers.ts +0 -471
- package/scripts/refactor-server-runtime-aliases.ts +0 -360
- package/scripts/restore-client-app-import-files.ts +0 -41
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
const assert = require('node:assert/strict');
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const os = require('node:os');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const coreRoot = path.resolve(__dirname, '..');
|
|
7
|
+
process.env.TS_NODE_PROJECT = path.join(coreRoot, 'cli', 'tsconfig.json');
|
|
8
|
+
process.env.TS_NODE_TRANSPILE_ONLY = '1';
|
|
9
|
+
require('ts-node/register/transpile-only');
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
createWorktreeBootstrapDiagnostics,
|
|
13
|
+
getWorktreeBootstrapStatus,
|
|
14
|
+
runWorktreeBootstrapInit,
|
|
15
|
+
worktreeBootstrapMarkerRelativePath,
|
|
16
|
+
} = require('../cli/runtime/worktreeBootstrap.ts');
|
|
17
|
+
|
|
18
|
+
const writeFile = (filepath, content) => {
|
|
19
|
+
fs.mkdirSync(path.dirname(filepath), { recursive: true });
|
|
20
|
+
fs.writeFileSync(filepath, content);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const createCodexAppRoot = () => {
|
|
24
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-worktree-bootstrap-'));
|
|
25
|
+
const appRoot = path.join(root, '.codex', 'worktrees', 'fixture-app');
|
|
26
|
+
|
|
27
|
+
fs.mkdirSync(appRoot, { recursive: true });
|
|
28
|
+
return appRoot;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const writeBootstrapFixture = (appRoot, { env = true, manifest = true, nodeModules = true } = {}) => {
|
|
32
|
+
writeFile(path.join(appRoot, 'package.json'), '{"name":"fixture"}\n');
|
|
33
|
+
writeFile(path.join(appRoot, 'package-lock.json'), '{"lockfileVersion":3}\n');
|
|
34
|
+
writeFile(path.join(appRoot, 'proteum.config.ts'), 'export default {};\n');
|
|
35
|
+
writeFile(path.join(appRoot, 'AGENTS.md'), '# Agents\n');
|
|
36
|
+
if (env) writeFile(path.join(appRoot, '.env'), 'PORT=3020\n');
|
|
37
|
+
if (manifest) writeFile(path.join(appRoot, '.proteum', 'manifest.json'), '{"version":10}\n');
|
|
38
|
+
if (nodeModules) fs.mkdirSync(path.join(appRoot, 'node_modules'), { recursive: true });
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const noOpRefresh = async () => ({ stdout: '', stderr: '', summary: 'refresh ok' });
|
|
42
|
+
const noOpRuntime = async () => ({ stdout: '', stderr: '', summary: 'runtime ok' });
|
|
43
|
+
const noOpDeps = async () => {};
|
|
44
|
+
|
|
45
|
+
test('worktree bootstrap status blocks Codex worktrees without a marker', () => {
|
|
46
|
+
const appRoot = createCodexAppRoot();
|
|
47
|
+
writeBootstrapFixture(appRoot);
|
|
48
|
+
|
|
49
|
+
const status = getWorktreeBootstrapStatus({ appRoot, proteumVersion: 'test' });
|
|
50
|
+
|
|
51
|
+
assert.equal(status.guarded, true);
|
|
52
|
+
assert.equal(status.blocking, true);
|
|
53
|
+
assert.equal(status.state, 'missing');
|
|
54
|
+
assert.equal(status.staleReasons[0].code, 'worktree-bootstrap/missing-marker');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('worktree bootstrap init writes a fresh marker with hashes and runtime status', async () => {
|
|
58
|
+
const appRoot = createCodexAppRoot();
|
|
59
|
+
writeBootstrapFixture(appRoot);
|
|
60
|
+
|
|
61
|
+
const result = await runWorktreeBootstrapInit({
|
|
62
|
+
appRoot,
|
|
63
|
+
coreRoot,
|
|
64
|
+
proteumVersion: 'test',
|
|
65
|
+
runDependencies: noOpDeps,
|
|
66
|
+
runRefresh: noOpRefresh,
|
|
67
|
+
runRuntimeStatus: noOpRuntime,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
assert.equal(fs.existsSync(path.join(appRoot, worktreeBootstrapMarkerRelativePath)), true);
|
|
71
|
+
assert.equal(result.status.blocking, false);
|
|
72
|
+
assert.equal(result.marker.proteumVersion, 'test');
|
|
73
|
+
assert.equal(result.marker.refresh.status, 'ok');
|
|
74
|
+
assert.equal(result.marker.runtimeStatus.summary, 'runtime ok');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('worktree bootstrap becomes stale when tracked inputs change', async () => {
|
|
78
|
+
const appRoot = createCodexAppRoot();
|
|
79
|
+
writeBootstrapFixture(appRoot);
|
|
80
|
+
|
|
81
|
+
await runWorktreeBootstrapInit({
|
|
82
|
+
appRoot,
|
|
83
|
+
coreRoot,
|
|
84
|
+
proteumVersion: 'test',
|
|
85
|
+
runDependencies: noOpDeps,
|
|
86
|
+
runRefresh: noOpRefresh,
|
|
87
|
+
runRuntimeStatus: noOpRuntime,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
writeFile(path.join(appRoot, 'package-lock.json'), '{"lockfileVersion":3,"changed":true}\n');
|
|
91
|
+
|
|
92
|
+
const status = getWorktreeBootstrapStatus({ appRoot, proteumVersion: 'test' });
|
|
93
|
+
|
|
94
|
+
assert.equal(status.blocking, true);
|
|
95
|
+
assert.equal(
|
|
96
|
+
status.staleReasons.some((reason) => reason.code === 'worktree-bootstrap/package-lock-changed'),
|
|
97
|
+
true,
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('worktree bootstrap accepts intentional dependency skips with a reason', async () => {
|
|
102
|
+
const appRoot = createCodexAppRoot();
|
|
103
|
+
writeBootstrapFixture(appRoot, { nodeModules: false });
|
|
104
|
+
|
|
105
|
+
const result = await runWorktreeBootstrapInit({
|
|
106
|
+
appRoot,
|
|
107
|
+
coreRoot,
|
|
108
|
+
proteumVersion: 'test',
|
|
109
|
+
reason: 'dependencies are shared by the parent workspace',
|
|
110
|
+
runDependencies: noOpDeps,
|
|
111
|
+
runRefresh: noOpRefresh,
|
|
112
|
+
runRuntimeStatus: noOpRuntime,
|
|
113
|
+
skipDeps: true,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
assert.equal(result.status.blocking, false);
|
|
117
|
+
assert.equal(result.marker.dependencies.status, 'skipped');
|
|
118
|
+
assert.equal(result.marker.skips.dependencies.reason, 'dependencies are shared by the parent workspace');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('worktree bootstrap requires a source env only when .env is missing', async () => {
|
|
122
|
+
const appRoot = createCodexAppRoot();
|
|
123
|
+
writeBootstrapFixture(appRoot, { env: false });
|
|
124
|
+
|
|
125
|
+
await assert.rejects(
|
|
126
|
+
() =>
|
|
127
|
+
runWorktreeBootstrapInit({
|
|
128
|
+
appRoot,
|
|
129
|
+
coreRoot,
|
|
130
|
+
proteumVersion: 'test',
|
|
131
|
+
runDependencies: noOpDeps,
|
|
132
|
+
runRefresh: noOpRefresh,
|
|
133
|
+
runRuntimeStatus: noOpRuntime,
|
|
134
|
+
}),
|
|
135
|
+
/missing \.env/,
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const sourceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-worktree-source-'));
|
|
139
|
+
writeFile(path.join(sourceRoot, '.env'), 'PORT=3021\n');
|
|
140
|
+
|
|
141
|
+
const result = await runWorktreeBootstrapInit({
|
|
142
|
+
appRoot,
|
|
143
|
+
coreRoot,
|
|
144
|
+
proteumVersion: 'test',
|
|
145
|
+
runDependencies: noOpDeps,
|
|
146
|
+
runRefresh: noOpRefresh,
|
|
147
|
+
runRuntimeStatus: noOpRuntime,
|
|
148
|
+
source: sourceRoot,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
assert.equal(fs.readFileSync(path.join(appRoot, '.env'), 'utf8'), 'PORT=3021\n');
|
|
152
|
+
assert.equal(result.marker.env.copied, true);
|
|
153
|
+
assert.equal(result.status.blocking, false);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('worktree bootstrap detects missing env manifest node_modules and version changes', async () => {
|
|
157
|
+
const appRoot = createCodexAppRoot();
|
|
158
|
+
writeBootstrapFixture(appRoot);
|
|
159
|
+
|
|
160
|
+
await runWorktreeBootstrapInit({
|
|
161
|
+
appRoot,
|
|
162
|
+
coreRoot,
|
|
163
|
+
proteumVersion: 'test',
|
|
164
|
+
runDependencies: noOpDeps,
|
|
165
|
+
runRefresh: noOpRefresh,
|
|
166
|
+
runRuntimeStatus: noOpRuntime,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
fs.rmSync(path.join(appRoot, '.env'));
|
|
170
|
+
fs.rmSync(path.join(appRoot, '.proteum', 'manifest.json'));
|
|
171
|
+
fs.rmSync(path.join(appRoot, 'node_modules'), { force: true, recursive: true });
|
|
172
|
+
|
|
173
|
+
const status = getWorktreeBootstrapStatus({ appRoot, proteumVersion: 'next' });
|
|
174
|
+
const codes = status.staleReasons.map((reason) => reason.code);
|
|
175
|
+
|
|
176
|
+
assert.equal(status.blocking, true);
|
|
177
|
+
assert.equal(codes.includes('worktree-bootstrap/env-missing'), true);
|
|
178
|
+
assert.equal(codes.includes('worktree-bootstrap/manifest-missing'), true);
|
|
179
|
+
assert.equal(codes.includes('worktree-bootstrap/node-modules-missing'), true);
|
|
180
|
+
assert.equal(codes.includes('worktree-bootstrap/proteum-version-changed'), true);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('worktree bootstrap bypass keeps status visible without blocking', () => {
|
|
184
|
+
const appRoot = createCodexAppRoot();
|
|
185
|
+
const previous = process.env.PROTEUM_ALLOW_UNBOOTSTRAPPED_WORKTREE;
|
|
186
|
+
process.env.PROTEUM_ALLOW_UNBOOTSTRAPPED_WORKTREE = '1';
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
writeBootstrapFixture(appRoot);
|
|
190
|
+
const status = getWorktreeBootstrapStatus({ appRoot, proteumVersion: 'test' });
|
|
191
|
+
|
|
192
|
+
assert.equal(status.bypassed, true);
|
|
193
|
+
assert.equal(status.blocking, false);
|
|
194
|
+
assert.equal(status.state, 'bypassed');
|
|
195
|
+
assert.equal(status.staleReasons.length > 0, true);
|
|
196
|
+
assert.equal(
|
|
197
|
+
createWorktreeBootstrapDiagnostics({ appRoot, status }).some(
|
|
198
|
+
(diagnostic) => diagnostic.code === 'worktree-bootstrap/bypassed',
|
|
199
|
+
),
|
|
200
|
+
true,
|
|
201
|
+
);
|
|
202
|
+
} finally {
|
|
203
|
+
if (previous === undefined) delete process.env.PROTEUM_ALLOW_UNBOOTSTRAPPED_WORKTREE;
|
|
204
|
+
else process.env.PROTEUM_ALLOW_UNBOOTSTRAPPED_WORKTREE = previous;
|
|
205
|
+
}
|
|
206
|
+
});
|
package/types/aliases.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
defineAction,
|
|
3
|
+
defineController,
|
|
4
|
+
schema,
|
|
5
|
+
type TControllerActionInput,
|
|
6
|
+
} from '@server/app/controller';
|
|
2
7
|
|
|
3
8
|
type Assert<T extends true> = T;
|
|
4
9
|
|
|
@@ -8,25 +13,26 @@ type Equals<TLeft, TRight> = (<T>() => T extends TLeft ? 1 : 2) extends <T>() =>
|
|
|
8
13
|
: false
|
|
9
14
|
: false;
|
|
10
15
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
schema.object({
|
|
16
|
+
const controller = defineController({
|
|
17
|
+
actions: {
|
|
18
|
+
fromShape: defineAction({
|
|
19
|
+
input: {
|
|
20
|
+
name: schema.string(),
|
|
21
|
+
age: schema.number().optional(),
|
|
22
|
+
},
|
|
23
|
+
handler: ({ input }) => input,
|
|
24
|
+
}),
|
|
25
|
+
fromSchema: defineAction({
|
|
26
|
+
input: schema.object({
|
|
22
27
|
slug: schema.string(),
|
|
23
28
|
}),
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
29
|
+
handler: ({ input }) => input,
|
|
30
|
+
}),
|
|
31
|
+
},
|
|
32
|
+
});
|
|
27
33
|
|
|
28
|
-
type TFromShape =
|
|
29
|
-
type TFromSchema =
|
|
34
|
+
type TFromShape = TControllerActionInput<typeof controller, 'fromShape'>;
|
|
35
|
+
type TFromSchema = TControllerActionInput<typeof controller, 'fromSchema'>;
|
|
30
36
|
|
|
31
37
|
type _AssertShapeInference = Assert<
|
|
32
38
|
Equals<
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Application } from '@server/app';
|
|
2
|
-
import
|
|
2
|
+
import type { TControllerActionContext } from '@server/app/controller';
|
|
3
3
|
import type { TServerRouter } from '@server/services/router';
|
|
4
4
|
import type { TServiceModelsClient, TServiceRequestContext } from '@server/app/service';
|
|
5
5
|
|
|
@@ -38,18 +38,17 @@ interface TTestApp extends Application {
|
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
type TRequestAuth = TypedRequestController['request']['auth'];
|
|
48
|
-
type TAuthCheckResult = ReturnType<TypedRequestController['useAuth']>;
|
|
41
|
+
type TControllerContext = TControllerActionContext<undefined, TTestApp>;
|
|
42
|
+
type TControllerContextAuth = TControllerContext['auth'];
|
|
43
|
+
type TControllerModelsClient = TControllerContext['models'];
|
|
49
44
|
type TServiceContextAuth = TServiceRequestContext<TTestApp>['auth'];
|
|
50
45
|
type TModelsClient = TServiceModelsClient<TTestApp>;
|
|
51
46
|
|
|
52
|
-
type
|
|
53
|
-
|
|
47
|
+
type _AssertTypedControllerRequestService = Assert<
|
|
48
|
+
Equals<TControllerContextAuth['check'], TTypedAuthRequestService['check']>
|
|
49
|
+
>;
|
|
54
50
|
type _AssertTypedServiceRequestContext = Assert<Equals<TServiceContextAuth['check'], TTypedAuthRequestService['check']>>;
|
|
51
|
+
type _AssertTypedControllerModelsClient = Assert<
|
|
52
|
+
Equals<ReturnType<TControllerModelsClient['post']['findMany']>, 'posts'>
|
|
53
|
+
>;
|
|
55
54
|
type _AssertTypedModelsClient = Assert<Equals<ReturnType<TModelsClient['post']['findMany']>, 'posts'>>;
|
package/cli/commands/migrate.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import path from 'path';
|
|
2
|
-
|
|
3
|
-
import cli from '..';
|
|
4
|
-
import { runPageContractMigration } from '../migrate/pageContract';
|
|
5
|
-
|
|
6
|
-
const printHuman = (summary: ReturnType<typeof runPageContractMigration>) => {
|
|
7
|
-
console.log(`Proteum migrate page-contract`);
|
|
8
|
-
console.log(`App root: ${summary.appRoot}`);
|
|
9
|
-
console.log(`Scanned files: ${summary.scannedFiles}`);
|
|
10
|
-
console.log(`Changed files: ${summary.changedFiles.length}${summary.dryRun ? ' (dry run)' : ''}`);
|
|
11
|
-
|
|
12
|
-
if (summary.changedFiles.length > 0) {
|
|
13
|
-
console.log('');
|
|
14
|
-
console.log('Rewritten files:');
|
|
15
|
-
for (const filepath of summary.changedFiles) {
|
|
16
|
-
console.log(`- ${path.relative(summary.appRoot, filepath)}`);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
if (summary.manualFixes.length > 0) {
|
|
21
|
-
console.log('');
|
|
22
|
-
console.log('Manual fixes required:');
|
|
23
|
-
for (const fix of summary.manualFixes) {
|
|
24
|
-
console.log(
|
|
25
|
-
`- ${path.relative(summary.appRoot, fix.filepath)}:${fix.line}:${fix.column} ${fix.routeLabel} :: ${fix.reason}`,
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
export const run = async (): Promise<void> => {
|
|
32
|
-
const action = String(cli.args.action || '').trim();
|
|
33
|
-
if (action !== 'page-contract') {
|
|
34
|
-
throw new Error(`Unsupported migrate action "${action}". Expected "page-contract".`);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const summary = runPageContractMigration({
|
|
38
|
-
appRoot: String(cli.args.workdir || process.cwd()),
|
|
39
|
-
dryRun: cli.args.dryRun === true,
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
if (cli.args.json === true) {
|
|
43
|
-
console.log(JSON.stringify(summary, null, 2));
|
|
44
|
-
} else {
|
|
45
|
-
printHuman(summary);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (summary.manualFixes.length > 0) {
|
|
49
|
-
throw new Error(`Page-contract migration requires manual fixes in ${summary.manualFixes.length} location(s).`);
|
|
50
|
-
}
|
|
51
|
-
};
|