proteum 2.1.3-1 → 2.1.7
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/AGENTS.md +22 -14
- package/README.md +109 -17
- package/agents/project/AGENTS.md +188 -25
- package/agents/project/CODING_STYLE.md +1 -0
- package/agents/project/client/AGENTS.md +13 -8
- package/agents/project/client/pages/AGENTS.md +17 -9
- package/agents/project/diagnostics.md +52 -0
- package/agents/project/optimizations.md +48 -0
- package/agents/project/server/routes/AGENTS.md +9 -6
- package/agents/project/server/services/AGENTS.md +10 -6
- package/agents/project/tests/AGENTS.md +11 -5
- package/cli/app/config.ts +13 -14
- package/cli/app/index.ts +58 -0
- package/cli/commands/connect.ts +45 -0
- package/cli/commands/dev.ts +37 -13
- package/cli/commands/diagnose.ts +286 -0
- package/cli/commands/doctor.ts +18 -5
- package/cli/commands/explain.ts +25 -0
- package/cli/commands/perf.ts +243 -0
- package/cli/commands/trace.ts +9 -1
- package/cli/commands/verify.ts +281 -0
- package/cli/compiler/artifacts/connectedProjects.ts +453 -0
- package/cli/compiler/artifacts/controllers.ts +198 -49
- package/cli/compiler/artifacts/discovery.ts +0 -34
- package/cli/compiler/artifacts/manifest.ts +95 -6
- package/cli/compiler/artifacts/routing.ts +2 -2
- package/cli/compiler/artifacts/services.ts +277 -130
- package/cli/compiler/client/index.ts +3 -0
- package/cli/compiler/common/files/style.ts +52 -0
- package/cli/compiler/common/generatedRouteModules.ts +34 -5
- package/cli/compiler/common/scripts.ts +11 -5
- package/cli/compiler/index.ts +2 -1
- package/cli/compiler/server/index.ts +3 -0
- package/cli/presentation/commands.ts +110 -7
- package/cli/presentation/devSession.ts +32 -7
- package/cli/runtime/commands.ts +165 -6
- package/cli/scaffold/index.ts +18 -27
- package/cli/scaffold/templates.ts +48 -28
- package/cli/utils/agents.ts +106 -13
- package/cli/utils/keyboard.ts +8 -0
- package/client/dev/profiler/ApexChart.tsx +66 -0
- package/client/dev/profiler/index.tsx +2508 -302
- package/client/dev/profiler/runtime.noop.ts +12 -0
- package/client/dev/profiler/runtime.ts +195 -4
- package/client/services/router/request/api.ts +6 -1
- package/common/applicationConfig.ts +173 -0
- package/common/applicationConfigLoader.ts +102 -0
- package/common/connectedProjects.ts +113 -0
- package/common/dev/connect.ts +267 -0
- package/common/dev/console.ts +31 -0
- package/common/dev/contractsDoctor.ts +128 -0
- package/common/dev/diagnostics.ts +59 -15
- package/common/dev/inspection.ts +491 -0
- package/common/dev/performance.ts +809 -0
- package/common/dev/profiler.ts +3 -0
- package/common/dev/proteumManifest.ts +31 -6
- package/common/dev/requestTrace.ts +52 -1
- package/common/env/proteumEnv.ts +176 -50
- package/common/router/index.ts +1 -0
- package/common/router/request/api.ts +2 -0
- package/config.ts +5 -0
- package/docs/dev-commands.md +5 -1
- package/docs/dev-sessions.md +90 -0
- package/docs/diagnostics.md +74 -11
- package/docs/request-tracing.md +50 -3
- package/package.json +1 -1
- package/server/app/container/config.ts +16 -87
- package/server/app/container/console/index.ts +42 -8
- package/server/app/container/index.ts +10 -2
- package/server/app/container/trace/index.ts +105 -0
- package/server/app/devDiagnostics.ts +138 -0
- package/server/app/index.ts +18 -8
- package/server/app/service/container.ts +0 -12
- package/server/app/service/index.ts +0 -2
- package/server/services/prisma/index.ts +121 -4
- package/server/services/router/http/index.ts +305 -11
- package/server/services/router/index.ts +116 -57
- package/server/services/router/request/api.ts +160 -19
- package/server/services/router/request/index.ts +8 -0
- package/server/services/router/response/index.ts +23 -1
- package/server/services/router/response/page/document.tsx +31 -14
- package/server/services/router/response/page/index.tsx +10 -0
- package/agents/framework/AGENTS.md +0 -177
- package/server/services/auth/router/service.json +0 -6
- package/server/services/auth/service.json +0 -6
- package/server/services/cron/service.json +0 -6
- package/server/services/disks/drivers/local/service.json +0 -6
- package/server/services/disks/drivers/s3/service.json +0 -6
- package/server/services/disks/service.json +0 -6
- package/server/services/fetch/service.json +0 -7
- package/server/services/prisma/service.json +0 -6
- package/server/services/router/service.json +0 -6
- package/server/services/schema/router/service.json +0 -6
- package/server/services/schema/service.json +0 -6
- package/server/services/security/encrypt/aes/service.json +0 -6
package/cli/runtime/commands.ts
CHANGED
|
@@ -10,7 +10,7 @@ class InitCommand extends ProteumCommand {
|
|
|
10
10
|
public static usage = buildUsage('init');
|
|
11
11
|
|
|
12
12
|
public name = Option.String('--name', { description: 'Human-readable app name.' });
|
|
13
|
-
public description = Option.String('--description', { description: 'App description used in identity.
|
|
13
|
+
public description = Option.String('--description', { description: 'App description used in identity.config.ts and package.json.' });
|
|
14
14
|
public identifier = Option.String('--identifier', { description: 'Application class and identity identifier.' });
|
|
15
15
|
public port = Option.String('--port', { description: 'Default local router port used in .env.' });
|
|
16
16
|
public url = Option.String('--url', { description: 'Default absolute URL used in .env.' });
|
|
@@ -179,19 +179,43 @@ class CheckCommand extends ProteumCommand {
|
|
|
179
179
|
}
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
class ConnectCommand extends ProteumCommand {
|
|
183
|
+
public static paths = [['connect']];
|
|
184
|
+
|
|
185
|
+
public static usage = buildUsage('connect');
|
|
186
|
+
|
|
187
|
+
public controllers = Option.Boolean('--controllers', false, {
|
|
188
|
+
description: 'Include imported connected controllers in the output.',
|
|
189
|
+
});
|
|
190
|
+
public json = Option.Boolean('--json', false, { description: 'Print JSON output.' });
|
|
191
|
+
public strict = Option.Boolean('--strict', false, { description: 'Exit with failure if any connect diagnostics exist.' });
|
|
192
|
+
public legacyArgs = Option.Rest();
|
|
193
|
+
|
|
194
|
+
public async execute() {
|
|
195
|
+
const args = { controllers: this.controllers, json: this.json, strict: this.strict } satisfies TArgsObject;
|
|
196
|
+
|
|
197
|
+
applyLegacyBooleanArgs('connect', this.legacyArgs, ['controllers', 'json', 'strict'], args);
|
|
198
|
+
this.setCliArgs(args);
|
|
199
|
+
await runCommandModule(() => import('../commands/connect'));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
182
203
|
class DoctorCommand extends ProteumCommand {
|
|
183
204
|
public static paths = [['doctor']];
|
|
184
205
|
|
|
185
206
|
public static usage = buildUsage('doctor');
|
|
186
207
|
|
|
208
|
+
public contracts = Option.Boolean('--contracts', false, {
|
|
209
|
+
description: 'Run contract-focused diagnostics for generated artifacts and manifest-owned source files.',
|
|
210
|
+
});
|
|
187
211
|
public json = Option.Boolean('--json', false, { description: 'Print JSON output.' });
|
|
188
212
|
public strict = Option.Boolean('--strict', false, { description: 'Exit with failure if any diagnostics exist.' });
|
|
189
213
|
public legacyArgs = Option.Rest();
|
|
190
214
|
|
|
191
215
|
public async execute() {
|
|
192
|
-
const args = { json: this.json, strict: this.strict } satisfies TArgsObject;
|
|
216
|
+
const args = { contracts: this.contracts, json: this.json, strict: this.strict } satisfies TArgsObject;
|
|
193
217
|
|
|
194
|
-
applyLegacyBooleanArgs('doctor', this.legacyArgs, ['json', 'strict'], args);
|
|
218
|
+
applyLegacyBooleanArgs('doctor', this.legacyArgs, ['contracts', 'json', 'strict'], args);
|
|
195
219
|
this.setCliArgs(args);
|
|
196
220
|
await runCommandModule(() => import('../commands/doctor'));
|
|
197
221
|
}
|
|
@@ -207,6 +231,7 @@ class ExplainCommand extends ProteumCommand {
|
|
|
207
231
|
public app = Option.Boolean('--app', false, { description: 'Include the app section.' });
|
|
208
232
|
public conventions = Option.Boolean('--conventions', false, { description: 'Include the conventions section.' });
|
|
209
233
|
public env = Option.Boolean('--env', false, { description: 'Include the env section.' });
|
|
234
|
+
public connected = Option.Boolean('--connected', false, { description: 'Include the connected-projects section.' });
|
|
210
235
|
public services = Option.Boolean('--services', false, { description: 'Include the services section.' });
|
|
211
236
|
public controllers = Option.Boolean('--controllers', false, { description: 'Include the controllers section.' });
|
|
212
237
|
public commands = Option.Boolean('--commands', false, { description: 'Include the commands section.' });
|
|
@@ -215,14 +240,25 @@ class ExplainCommand extends ProteumCommand {
|
|
|
215
240
|
public diagnostics = Option.Boolean('--diagnostics', false, {
|
|
216
241
|
description: 'Include the diagnostics section.',
|
|
217
242
|
});
|
|
218
|
-
public
|
|
243
|
+
public args = Option.Rest();
|
|
219
244
|
|
|
220
245
|
public async execute() {
|
|
246
|
+
const [mode = '', ...restArgs] = this.args;
|
|
247
|
+
if (mode === 'owner') {
|
|
248
|
+
this.setCliArgs({
|
|
249
|
+
json: this.json,
|
|
250
|
+
ownerQuery: restArgs.join(' ').trim(),
|
|
251
|
+
});
|
|
252
|
+
await runCommandModule(() => import('../commands/explain'));
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
221
256
|
const args = {
|
|
222
257
|
json: this.json,
|
|
223
258
|
all: this.all,
|
|
224
259
|
app: this.app,
|
|
225
260
|
conventions: this.conventions,
|
|
261
|
+
connected: this.connected,
|
|
226
262
|
env: this.env,
|
|
227
263
|
services: this.services,
|
|
228
264
|
controllers: this.controllers,
|
|
@@ -234,8 +270,8 @@ class ExplainCommand extends ProteumCommand {
|
|
|
234
270
|
|
|
235
271
|
applyLegacyBooleanArgs(
|
|
236
272
|
'explain',
|
|
237
|
-
this.
|
|
238
|
-
['json', 'all', 'app', 'conventions', 'env', 'services', 'controllers', 'commands', 'routes', 'layouts', 'diagnostics'],
|
|
273
|
+
this.args,
|
|
274
|
+
['json', 'all', 'app', 'conventions', 'env', 'connected', 'services', 'controllers', 'commands', 'routes', 'layouts', 'diagnostics'],
|
|
239
275
|
args,
|
|
240
276
|
);
|
|
241
277
|
this.setCliArgs(args);
|
|
@@ -322,6 +358,121 @@ class SessionCommand extends ProteumCommand {
|
|
|
322
358
|
}
|
|
323
359
|
}
|
|
324
360
|
|
|
361
|
+
class DiagnoseCommand extends ProteumCommand {
|
|
362
|
+
public static paths = [['diagnose']];
|
|
363
|
+
|
|
364
|
+
public static usage = buildUsage('diagnose');
|
|
365
|
+
|
|
366
|
+
public port = Option.String('--port', { description: 'Target an existing dev server on the given port.' });
|
|
367
|
+
public url = Option.String('--url', { description: 'Target an existing dev server at the given base URL.' });
|
|
368
|
+
public json = Option.Boolean('--json', false, { description: 'Print JSON output.' });
|
|
369
|
+
public hit = Option.String('--hit', { description: 'Issue one HTTP request before diagnosing. Defaults to the target path when it starts with /.' });
|
|
370
|
+
public method = Option.String('--method', { description: 'HTTP method used with `--hit`.' });
|
|
371
|
+
public dataJson = Option.String('--data-json', { description: 'JSON request body used with `--hit`.' });
|
|
372
|
+
public sessionEmail = Option.String('--session-email', {
|
|
373
|
+
description: 'Mint a dev session before `--hit` and attach the returned cookie.',
|
|
374
|
+
});
|
|
375
|
+
public sessionRole = Option.String('--session-role', { description: 'Require the dev session user to have this role.' });
|
|
376
|
+
public capture = Option.String('--capture', { description: 'Trace capture mode armed before `--hit`.' });
|
|
377
|
+
public logsLevel = Option.String('--logs-level', { description: 'Minimum server log level included in the diagnose response.' });
|
|
378
|
+
public logsLimit = Option.String('--logs-limit', { description: 'Maximum number of server log lines included in the diagnose response.' });
|
|
379
|
+
public args = Option.Rest();
|
|
380
|
+
|
|
381
|
+
public async execute() {
|
|
382
|
+
const [target = ''] = this.args;
|
|
383
|
+
|
|
384
|
+
this.setCliArgs({
|
|
385
|
+
capture: this.capture ?? '',
|
|
386
|
+
dataJson: this.dataJson ?? '',
|
|
387
|
+
hit: this.hit ?? '',
|
|
388
|
+
json: this.json,
|
|
389
|
+
logsLevel: this.logsLevel ?? '',
|
|
390
|
+
logsLimit: this.logsLimit ?? '',
|
|
391
|
+
method: this.method ?? '',
|
|
392
|
+
port: this.port ?? '',
|
|
393
|
+
sessionEmail: this.sessionEmail ?? '',
|
|
394
|
+
sessionRole: this.sessionRole ?? '',
|
|
395
|
+
target,
|
|
396
|
+
url: this.url ?? '',
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
await runCommandModule(() => import('../commands/diagnose'));
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
class PerfCommand extends ProteumCommand {
|
|
404
|
+
public static paths = [['perf']];
|
|
405
|
+
|
|
406
|
+
public static usage = buildUsage('perf');
|
|
407
|
+
|
|
408
|
+
public port = Option.String('--port', { description: 'Target an existing dev server on the given port.' });
|
|
409
|
+
public url = Option.String('--url', { description: 'Target an existing dev server at the given base URL.' });
|
|
410
|
+
public json = Option.Boolean('--json', false, { description: 'Print JSON output.' });
|
|
411
|
+
public since = Option.String('--since', { description: 'Window used by `top` and `memory`, for example `today`, `yesterday`, or `1h`.' });
|
|
412
|
+
public baseline = Option.String('--baseline', { description: 'Baseline window used by `compare`.' });
|
|
413
|
+
public target = Option.String('--target', { description: 'Target window used by `compare`.' });
|
|
414
|
+
public groupBy = Option.String('--group-by', { description: 'Aggregate by `path`, `route`, or `controller`.' });
|
|
415
|
+
public limit = Option.String('--limit', { description: 'Maximum number of rows to print.' });
|
|
416
|
+
public args = Option.Rest();
|
|
417
|
+
|
|
418
|
+
public async execute() {
|
|
419
|
+
const [action = 'top', target = ''] = this.args;
|
|
420
|
+
|
|
421
|
+
this.setCliArgs({
|
|
422
|
+
action,
|
|
423
|
+
baseline: this.baseline ?? '',
|
|
424
|
+
groupBy: this.groupBy ?? '',
|
|
425
|
+
json: this.json,
|
|
426
|
+
limit: this.limit ?? '',
|
|
427
|
+
port: this.port ?? '',
|
|
428
|
+
since: this.since ?? '',
|
|
429
|
+
target,
|
|
430
|
+
targetWindow: this.target ?? '',
|
|
431
|
+
url: this.url ?? '',
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
await runCommandModule(() => import('../commands/perf'));
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
class VerifyCommand extends ProteumCommand {
|
|
439
|
+
public static paths = [['verify']];
|
|
440
|
+
|
|
441
|
+
public static usage = buildUsage('verify');
|
|
442
|
+
|
|
443
|
+
public json = Option.Boolean('--json', false, { description: 'Print JSON output.' });
|
|
444
|
+
public crosspath = Option.String('--crosspath', { description: 'Override the CrossPath reference app path.' });
|
|
445
|
+
public product = Option.String('--product', { description: 'Override the Unique Domains Product reference app path.' });
|
|
446
|
+
public website = Option.String('--website', { description: 'Override the Unique Domains Website reference app path.' });
|
|
447
|
+
public crosspathPort = Option.String('--crosspath-port', { description: 'Port used for the CrossPath validation server.' });
|
|
448
|
+
public productPort = Option.String('--product-port', {
|
|
449
|
+
description: 'Port used for the Unique Domains Product validation server.',
|
|
450
|
+
});
|
|
451
|
+
public websitePort = Option.String('--website-port', {
|
|
452
|
+
description: 'Port used for the Unique Domains Website validation server.',
|
|
453
|
+
});
|
|
454
|
+
public route = Option.String('--route', { description: 'Route loaded in both apps during validation.' });
|
|
455
|
+
public args = Option.Rest();
|
|
456
|
+
|
|
457
|
+
public async execute() {
|
|
458
|
+
const [action = 'framework-change'] = this.args;
|
|
459
|
+
|
|
460
|
+
this.setCliArgs({
|
|
461
|
+
action,
|
|
462
|
+
crosspath: this.crosspath ?? '',
|
|
463
|
+
crosspathPort: this.crosspathPort ?? '',
|
|
464
|
+
json: this.json,
|
|
465
|
+
product: this.product ?? '',
|
|
466
|
+
productPort: this.productPort ?? '',
|
|
467
|
+
route: this.route ?? '',
|
|
468
|
+
website: this.website ?? '',
|
|
469
|
+
websitePort: this.websitePort ?? '',
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
await runCommandModule(() => import('../commands/verify'));
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
325
476
|
export const registeredCommands = {
|
|
326
477
|
init: InitCommand,
|
|
327
478
|
create: CreateCommand,
|
|
@@ -331,11 +482,15 @@ export const registeredCommands = {
|
|
|
331
482
|
typecheck: TypecheckCommand,
|
|
332
483
|
lint: LintCommand,
|
|
333
484
|
check: CheckCommand,
|
|
485
|
+
connect: ConnectCommand,
|
|
334
486
|
doctor: DoctorCommand,
|
|
335
487
|
explain: ExplainCommand,
|
|
488
|
+
diagnose: DiagnoseCommand,
|
|
489
|
+
perf: PerfCommand,
|
|
336
490
|
trace: TraceCommand,
|
|
337
491
|
command: CommandCommand,
|
|
338
492
|
session: SessionCommand,
|
|
493
|
+
verify: VerifyCommand,
|
|
339
494
|
} as const;
|
|
340
495
|
|
|
341
496
|
export const createCli = (version: string) => {
|
|
@@ -356,11 +511,15 @@ export const createCli = (version: string) => {
|
|
|
356
511
|
clipanion.register(TypecheckCommand);
|
|
357
512
|
clipanion.register(LintCommand);
|
|
358
513
|
clipanion.register(CheckCommand);
|
|
514
|
+
clipanion.register(ConnectCommand);
|
|
359
515
|
clipanion.register(DoctorCommand);
|
|
360
516
|
clipanion.register(ExplainCommand);
|
|
517
|
+
clipanion.register(DiagnoseCommand);
|
|
518
|
+
clipanion.register(PerfCommand);
|
|
361
519
|
clipanion.register(TraceCommand);
|
|
362
520
|
clipanion.register(CommandCommand);
|
|
363
521
|
clipanion.register(SessionCommand);
|
|
522
|
+
clipanion.register(VerifyCommand);
|
|
364
523
|
|
|
365
524
|
return clipanion;
|
|
366
525
|
};
|
package/cli/scaffold/index.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import slugify from 'slugify';
|
|
4
|
-
import yaml from 'yaml';
|
|
5
4
|
import { UsageError } from 'clipanion';
|
|
6
5
|
|
|
7
6
|
import cli from '..';
|
|
8
|
-
import {
|
|
7
|
+
import { loadApplicationIdentityConfig } from '../../common/applicationConfigLoader';
|
|
8
|
+
import { ensureProjectAgentSymlinks, renderProjectInstructionGitignoreBlock } from '../utils/agents';
|
|
9
9
|
import { runProcess } from '../utils/runProcess';
|
|
10
10
|
import {
|
|
11
11
|
createClientTsconfigTemplate,
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
createInitSummary,
|
|
19
19
|
createPackageJsonTemplate,
|
|
20
20
|
createPageTemplate,
|
|
21
|
+
createProteumConfigTemplate,
|
|
21
22
|
createRouteTemplate,
|
|
22
23
|
createRouterConfigTemplate,
|
|
23
24
|
createServerIndexTemplate,
|
|
@@ -133,16 +134,16 @@ const defaultRouteFromSegments = (segments: string[]) => {
|
|
|
133
134
|
const resolveRootServiceLeaf = (segments: string[]) => segments[segments.length - 1];
|
|
134
135
|
|
|
135
136
|
const readIdentityConfig = (appRoot: string): TIdentityConfig => {
|
|
136
|
-
const identityFilepath = path.join(appRoot, 'identity.
|
|
137
|
+
const identityFilepath = path.join(appRoot, 'identity.config.ts');
|
|
137
138
|
if (!fs.existsSync(identityFilepath)) {
|
|
138
|
-
throw new UsageError(`Missing identity.
|
|
139
|
+
throw new UsageError(`Missing identity.config.ts in ${appRoot}. Run \`proteum init\` first or target a Proteum app root.`);
|
|
139
140
|
}
|
|
140
141
|
|
|
141
|
-
const parsed =
|
|
142
|
-
const identifier = typeof parsed
|
|
143
|
-
const name = typeof parsed
|
|
142
|
+
const parsed = loadApplicationIdentityConfig(appRoot);
|
|
143
|
+
const identifier = typeof parsed.identifier === 'string' ? parsed.identifier.trim() : '';
|
|
144
|
+
const name = typeof parsed.name === 'string' ? parsed.name.trim() : '';
|
|
144
145
|
|
|
145
|
-
if (!identifier) throw new UsageError(`identity.
|
|
146
|
+
if (!identifier) throw new UsageError(`identity.config.ts in ${appRoot} is missing a valid "identifier" field.`);
|
|
146
147
|
|
|
147
148
|
return {
|
|
148
149
|
identifier,
|
|
@@ -151,7 +152,7 @@ const readIdentityConfig = (appRoot: string): TIdentityConfig => {
|
|
|
151
152
|
};
|
|
152
153
|
|
|
153
154
|
const assertProteumAppRoot = (appRoot: string) => {
|
|
154
|
-
const expectedEntries = ['package.json', 'identity.
|
|
155
|
+
const expectedEntries = ['package.json', 'identity.config.ts', 'proteum.config.ts', 'client', 'server'];
|
|
155
156
|
const missing = expectedEntries.filter((entry) => !fs.existsSync(path.join(appRoot, entry)));
|
|
156
157
|
if (missing.length > 0) {
|
|
157
158
|
throw new UsageError(
|
|
@@ -459,21 +460,9 @@ const createServicePlan = ({
|
|
|
459
460
|
const configExportName = `${configFileBase}Config`;
|
|
460
461
|
const relativeServiceDir = path.join('server', 'services', ...segments);
|
|
461
462
|
const relativeServiceFilepath = path.join(relativeServiceDir, 'index.ts');
|
|
462
|
-
const relativeServiceConfigFilepath = path.join(relativeServiceDir, 'service.json');
|
|
463
463
|
const relativeConfigFilepath = path.join('server', 'config', `${configFileBase}.ts`);
|
|
464
464
|
const propertyName = serviceImportName;
|
|
465
465
|
|
|
466
|
-
const serviceJson = JSON.stringify(
|
|
467
|
-
{
|
|
468
|
-
id: `${appIdentifier}/${serviceImportName}`,
|
|
469
|
-
name: `${appIdentifier}${serviceImportName}`,
|
|
470
|
-
parent: 'app',
|
|
471
|
-
dependences: [],
|
|
472
|
-
},
|
|
473
|
-
null,
|
|
474
|
-
4,
|
|
475
|
-
) + '\n';
|
|
476
|
-
|
|
477
466
|
return {
|
|
478
467
|
files: [
|
|
479
468
|
{
|
|
@@ -483,10 +472,6 @@ const createServicePlan = ({
|
|
|
483
472
|
className,
|
|
484
473
|
}),
|
|
485
474
|
},
|
|
486
|
-
{
|
|
487
|
-
relativePath: relativeServiceConfigFilepath,
|
|
488
|
-
content: serviceJson,
|
|
489
|
-
},
|
|
490
475
|
{
|
|
491
476
|
relativePath: relativeConfigFilepath,
|
|
492
477
|
content: createServiceConfigTemplate({
|
|
@@ -628,13 +613,17 @@ const createInitFilePlans = (config: TScaffoldInitConfig): TScaffoldFilePlan[] =
|
|
|
628
613
|
}),
|
|
629
614
|
},
|
|
630
615
|
{
|
|
631
|
-
relativePath: 'identity.
|
|
616
|
+
relativePath: 'identity.config.ts',
|
|
632
617
|
content: createIdentityTemplate({
|
|
633
618
|
appName: config.name,
|
|
634
619
|
appIdentifier: config.identifier,
|
|
635
620
|
appDescription: config.description,
|
|
636
621
|
}),
|
|
637
622
|
},
|
|
623
|
+
{
|
|
624
|
+
relativePath: 'proteum.config.ts',
|
|
625
|
+
content: createProteumConfigTemplate(),
|
|
626
|
+
},
|
|
638
627
|
{
|
|
639
628
|
relativePath: '.env',
|
|
640
629
|
content: createEnvTemplate({
|
|
@@ -644,7 +633,9 @@ const createInitFilePlans = (config: TScaffoldInitConfig): TScaffoldFilePlan[] =
|
|
|
644
633
|
},
|
|
645
634
|
{
|
|
646
635
|
relativePath: '.gitignore',
|
|
647
|
-
content: createGitignoreTemplate(
|
|
636
|
+
content: createGitignoreTemplate({
|
|
637
|
+
projectInstructionGitignoreBlock: renderProjectInstructionGitignoreBlock({ coreRoot: cli.paths.core.root }),
|
|
638
|
+
}),
|
|
648
639
|
},
|
|
649
640
|
{
|
|
650
641
|
relativePath: 'eslint.config.mjs',
|
|
@@ -227,6 +227,8 @@ export const createServerTsconfigTemplate = () => `{
|
|
|
227
227
|
},
|
|
228
228
|
"include": [
|
|
229
229
|
".",
|
|
230
|
+
"../identity.config.ts",
|
|
231
|
+
"../proteum.config.ts",
|
|
230
232
|
"../var/typings",
|
|
231
233
|
"../node_modules/proteum/types/global",
|
|
232
234
|
"../.proteum/server/services.d.ts",
|
|
@@ -235,19 +237,27 @@ export const createServerTsconfigTemplate = () => `{
|
|
|
235
237
|
}
|
|
236
238
|
`;
|
|
237
239
|
|
|
238
|
-
export const createGitignoreTemplate = (
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
240
|
+
export const createGitignoreTemplate = ({
|
|
241
|
+
projectInstructionGitignoreBlock,
|
|
242
|
+
}: {
|
|
243
|
+
projectInstructionGitignoreBlock: string;
|
|
244
|
+
}) => `node_modules
|
|
245
|
+
/.proteum
|
|
246
|
+
/.cache
|
|
247
|
+
/bin
|
|
248
|
+
/dev
|
|
249
|
+
/var
|
|
250
|
+
/proteum.connected.json
|
|
244
251
|
.env
|
|
252
|
+
|
|
253
|
+
${projectInstructionGitignoreBlock}
|
|
245
254
|
`;
|
|
246
255
|
|
|
247
256
|
export const createEnvTemplate = ({ port, url }: { port: number; url: string }) => `ENV_NAME=local
|
|
248
257
|
ENV_PROFILE=dev
|
|
249
258
|
PORT=${port}
|
|
250
259
|
URL=${url}
|
|
260
|
+
URL_INTERNAL=${url}
|
|
251
261
|
|
|
252
262
|
# Optional trace settings
|
|
253
263
|
# TRACE_ENABLE=true
|
|
@@ -306,28 +316,38 @@ export const createIdentityTemplate = ({
|
|
|
306
316
|
appName: string;
|
|
307
317
|
appIdentifier: string;
|
|
308
318
|
appDescription: string;
|
|
309
|
-
}) => `
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
web:
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
319
|
+
}) => `import { Application } from 'proteum/config';
|
|
320
|
+
|
|
321
|
+
export default Application.identity({
|
|
322
|
+
name: ${JSON.stringify(appName)},
|
|
323
|
+
identifier: ${JSON.stringify(appIdentifier)},
|
|
324
|
+
description: ${JSON.stringify(appDescription)},
|
|
325
|
+
author: {
|
|
326
|
+
name: ${JSON.stringify(appName)},
|
|
327
|
+
url: 'localhost',
|
|
328
|
+
email: 'team@example.com',
|
|
329
|
+
},
|
|
330
|
+
social: {},
|
|
331
|
+
language: 'en',
|
|
332
|
+
locale: 'en-US',
|
|
333
|
+
maincolor: 'white',
|
|
334
|
+
iconsPack: 'light',
|
|
335
|
+
web: {
|
|
336
|
+
title: ${JSON.stringify(appName)},
|
|
337
|
+
titleSuffix: ${JSON.stringify(appName)},
|
|
338
|
+
fullTitle: ${JSON.stringify(appName)},
|
|
339
|
+
description: ${JSON.stringify(appDescription)},
|
|
340
|
+
version: '0.0.1',
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
`;
|
|
344
|
+
|
|
345
|
+
export const createProteumConfigTemplate = () => `import { Application } from 'proteum/config';
|
|
346
|
+
|
|
347
|
+
export default Application.setup({
|
|
348
|
+
transpile: [],
|
|
349
|
+
connect: {},
|
|
350
|
+
});
|
|
331
351
|
`;
|
|
332
352
|
|
|
333
353
|
export const createInitSummary = (result: TScaffoldResult, config: TScaffoldInitConfig) => ({
|
package/cli/utils/agents.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { logVerbose } from '../runtime/verbose';
|
|
|
11
11
|
- TYPES
|
|
12
12
|
----------------------------------*/
|
|
13
13
|
|
|
14
|
+
type TProjectInstructionArgs = { coreRoot: string };
|
|
14
15
|
type TEnsureProjectAgentSymlinksArgs = { appRoot: string; coreRoot: string };
|
|
15
16
|
|
|
16
17
|
type TAgentLinkDefinition = { projectPath: string; sourcePath: string; ensureParentDir?: boolean };
|
|
@@ -19,10 +20,12 @@ type TAgentLinkDefinition = { projectPath: string; sourcePath: string; ensurePar
|
|
|
19
20
|
- CONSTANTS
|
|
20
21
|
----------------------------------*/
|
|
21
22
|
|
|
22
|
-
// Project-local
|
|
23
|
+
// Project-local instruction entrypoints mapped to their canonical shipped source files.
|
|
23
24
|
const projectAgentLinkDefinitions: TAgentLinkDefinition[] = [
|
|
24
25
|
{ projectPath: 'AGENTS.md', sourcePath: 'AGENTS.md' },
|
|
25
26
|
{ projectPath: 'CODING_STYLE.md', sourcePath: 'CODING_STYLE.md' },
|
|
27
|
+
{ projectPath: 'diagnostics.md', sourcePath: 'diagnostics.md' },
|
|
28
|
+
{ projectPath: 'optimizations.md', sourcePath: 'optimizations.md' },
|
|
26
29
|
{ projectPath: path.join('client', 'AGENTS.md'), sourcePath: path.join('client', 'AGENTS.md') },
|
|
27
30
|
{ projectPath: path.join('client', 'pages', 'AGENTS.md'), sourcePath: path.join('client', 'pages', 'AGENTS.md') },
|
|
28
31
|
{
|
|
@@ -32,41 +35,107 @@ const projectAgentLinkDefinitions: TAgentLinkDefinition[] = [
|
|
|
32
35
|
{ projectPath: path.join('server', 'routes', 'AGENTS.md'), sourcePath: path.join('server', 'routes', 'AGENTS.md') },
|
|
33
36
|
{ projectPath: path.join('tests', 'e2e', 'AGENTS.md'), sourcePath: path.join('tests', 'AGENTS.md') },
|
|
34
37
|
];
|
|
38
|
+
const projectInstructionGitignoreBlockStart = '# Proteum-managed instruction symlinks';
|
|
39
|
+
const projectInstructionGitignoreBlockEnd = '# End Proteum-managed instruction symlinks';
|
|
35
40
|
|
|
36
41
|
/*----------------------------------
|
|
37
42
|
- PUBLIC API
|
|
38
43
|
----------------------------------*/
|
|
39
44
|
|
|
40
45
|
export function ensureProjectAgentSymlinks({ appRoot, coreRoot }: TEnsureProjectAgentSymlinksArgs) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
ensureSymlinks(appRoot, getProjectAgentLinkDefinitions({ coreRoot }), '[agents]');
|
|
47
|
+
ensureSymlinks(appRoot, getProjectSkillLinkDefinitions({ coreRoot }), '[skills]');
|
|
48
|
+
ensureProjectInstructionGitignoreEntries({ appRoot, coreRoot });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getProjectInstructionGitignoreEntries({ coreRoot }: TProjectInstructionArgs) {
|
|
52
|
+
const entries = new Set<string>();
|
|
46
53
|
|
|
47
|
-
|
|
48
|
-
|
|
54
|
+
for (const linkDefinition of [
|
|
55
|
+
...getProjectAgentLinkDefinitions({ coreRoot }),
|
|
56
|
+
...getProjectSkillLinkDefinitions({ coreRoot }),
|
|
57
|
+
]) {
|
|
58
|
+
entries.add(`/${normalizeProjectPathForGitignore(linkDefinition.projectPath)}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return Array.from(entries);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function renderProjectInstructionGitignoreBlock({ coreRoot }: TProjectInstructionArgs) {
|
|
65
|
+
return [
|
|
66
|
+
projectInstructionGitignoreBlockStart,
|
|
67
|
+
...getProjectInstructionGitignoreEntries({ coreRoot }),
|
|
68
|
+
projectInstructionGitignoreBlockEnd,
|
|
69
|
+
].join('\n');
|
|
49
70
|
}
|
|
50
71
|
|
|
51
72
|
/*----------------------------------
|
|
52
73
|
- HELPERS
|
|
53
74
|
----------------------------------*/
|
|
54
75
|
|
|
55
|
-
function
|
|
76
|
+
function getProjectAgentLinkDefinitions({ coreRoot }: TProjectInstructionArgs) {
|
|
77
|
+
const agentSourceRoot = path.join(coreRoot, 'agents', 'project');
|
|
78
|
+
|
|
79
|
+
return projectAgentLinkDefinitions.map((linkDefinition) => ({
|
|
80
|
+
...linkDefinition,
|
|
81
|
+
sourcePath: path.join(agentSourceRoot, linkDefinition.sourcePath),
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getProjectSkillLinkDefinitions({ coreRoot }: TProjectInstructionArgs) {
|
|
56
86
|
const frameworkSkillsRoot = path.join(coreRoot, 'skills');
|
|
57
|
-
if (!fs.existsSync(frameworkSkillsRoot)) return;
|
|
87
|
+
if (!fs.existsSync(frameworkSkillsRoot)) return [];
|
|
58
88
|
|
|
59
|
-
|
|
89
|
+
return fs
|
|
60
90
|
.readdirSync(frameworkSkillsRoot, { withFileTypes: true })
|
|
61
91
|
.filter((dirent) => dirent.isDirectory())
|
|
92
|
+
.sort((left, right) => left.name.localeCompare(right.name))
|
|
62
93
|
.map((dirent) => ({
|
|
63
94
|
projectPath: path.join('skills', dirent.name),
|
|
64
95
|
sourcePath: path.join(frameworkSkillsRoot, dirent.name),
|
|
65
96
|
ensureParentDir: true,
|
|
66
97
|
}))
|
|
67
98
|
.filter((linkDefinition) => pathEntryExists(path.join(linkDefinition.sourcePath, 'SKILL.md')));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function ensureProjectInstructionGitignoreEntries({ appRoot, coreRoot }: TEnsureProjectAgentSymlinksArgs) {
|
|
102
|
+
const gitignoreFilepath = path.join(appRoot, '.gitignore');
|
|
103
|
+
if (!pathEntryExists(gitignoreFilepath)) return;
|
|
104
|
+
|
|
105
|
+
const managedEntries = getProjectInstructionGitignoreEntries({ coreRoot });
|
|
106
|
+
const managedNormalizedEntries = new Set(managedEntries.map(normalizeGitignoreEntry));
|
|
107
|
+
const lines = fs.readFileSync(gitignoreFilepath, 'utf8').split(/\r?\n/);
|
|
108
|
+
const filteredLines: string[] = [];
|
|
109
|
+
|
|
110
|
+
let insideManagedBlock = false;
|
|
68
111
|
|
|
69
|
-
|
|
112
|
+
for (const line of lines) {
|
|
113
|
+
const trimmedLine = line.trim();
|
|
114
|
+
|
|
115
|
+
if (trimmedLine === projectInstructionGitignoreBlockStart) {
|
|
116
|
+
insideManagedBlock = true;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (trimmedLine === projectInstructionGitignoreBlockEnd) {
|
|
121
|
+
insideManagedBlock = false;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (insideManagedBlock) continue;
|
|
126
|
+
if (shouldSkipLegacyManagedGitignoreLine(line, managedNormalizedEntries)) continue;
|
|
127
|
+
|
|
128
|
+
filteredLines.push(line);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const baseContent = trimTrailingBlankLines(filteredLines).join('\n');
|
|
132
|
+
const managedBlock = renderProjectInstructionGitignoreBlock({ coreRoot });
|
|
133
|
+
const nextContent = baseContent ? `${baseContent}\n\n${managedBlock}\n` : `${managedBlock}\n`;
|
|
134
|
+
|
|
135
|
+
if (nextContent === fs.readFileSync(gitignoreFilepath, 'utf8')) return;
|
|
136
|
+
|
|
137
|
+
fs.writeFileSync(gitignoreFilepath, nextContent);
|
|
138
|
+
logVerbose(`[agents] Updated ${path.relative(appRoot, gitignoreFilepath) || '.gitignore'} with Proteum-managed instruction ignore entries.`);
|
|
70
139
|
}
|
|
71
140
|
|
|
72
141
|
function ensureSymlinks(appRoot: string, linkDefinitions: TAgentLinkDefinition[], logPrefix: string) {
|
|
@@ -81,7 +150,7 @@ function ensureSymlinks(appRoot: string, linkDefinitions: TAgentLinkDefinition[]
|
|
|
81
150
|
|
|
82
151
|
const sourceFilepath = linkDefinition.sourcePath;
|
|
83
152
|
if (!fs.existsSync(sourceFilepath)) {
|
|
84
|
-
throw new Error(`Missing
|
|
153
|
+
throw new Error(`Missing project instruction asset: ${sourceFilepath}`);
|
|
85
154
|
}
|
|
86
155
|
|
|
87
156
|
const symlinkTarget = path.relative(projectParentDir, sourceFilepath);
|
|
@@ -91,6 +160,30 @@ function ensureSymlinks(appRoot: string, linkDefinitions: TAgentLinkDefinition[]
|
|
|
91
160
|
}
|
|
92
161
|
}
|
|
93
162
|
|
|
163
|
+
function normalizeProjectPathForGitignore(projectPath: string) {
|
|
164
|
+
return projectPath.replace(/\\/g, '/');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function normalizeGitignoreEntry(value: string) {
|
|
168
|
+
return value.trim().replace(/#.*/, '').replace(/\\/g, '/').replace(/^\/+/, '').replace(/\/+$/, '');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function shouldSkipLegacyManagedGitignoreLine(line: string, managedNormalizedEntries: Set<string>) {
|
|
172
|
+
const normalizedLine = normalizeGitignoreEntry(line);
|
|
173
|
+
if (!normalizedLine) return false;
|
|
174
|
+
if (line.trim().startsWith('#')) return false;
|
|
175
|
+
|
|
176
|
+
return managedNormalizedEntries.has(normalizedLine);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function trimTrailingBlankLines(lines: string[]) {
|
|
180
|
+
const trimmedLines = [...lines];
|
|
181
|
+
|
|
182
|
+
while (trimmedLines.length > 0 && trimmedLines[trimmedLines.length - 1].trim() === '') trimmedLines.pop();
|
|
183
|
+
|
|
184
|
+
return trimmedLines;
|
|
185
|
+
}
|
|
186
|
+
|
|
94
187
|
function pathEntryExists(filepath: string) {
|
|
95
188
|
try {
|
|
96
189
|
fs.lstatSync(filepath);
|
package/cli/utils/keyboard.ts
CHANGED
|
@@ -23,7 +23,15 @@ class KeyboardCommands {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
private listen() {
|
|
26
|
+
if (!process.stdin) return;
|
|
27
|
+
|
|
26
28
|
readline.emitKeypressEvents(process.stdin);
|
|
29
|
+
|
|
30
|
+
if (!process.stdin.isTTY || typeof process.stdin.setRawMode !== 'function') {
|
|
31
|
+
logVerbose('Keyboard shortcuts disabled because stdin is not an interactive TTY.');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
27
35
|
process.stdin.setRawMode(true);
|
|
28
36
|
process.stdin.on('keypress', async (chunk: string, key: Key) => {
|
|
29
37
|
let str = key.name;
|