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.
Files changed (95) hide show
  1. package/AGENTS.md +22 -14
  2. package/README.md +109 -17
  3. package/agents/project/AGENTS.md +188 -25
  4. package/agents/project/CODING_STYLE.md +1 -0
  5. package/agents/project/client/AGENTS.md +13 -8
  6. package/agents/project/client/pages/AGENTS.md +17 -9
  7. package/agents/project/diagnostics.md +52 -0
  8. package/agents/project/optimizations.md +48 -0
  9. package/agents/project/server/routes/AGENTS.md +9 -6
  10. package/agents/project/server/services/AGENTS.md +10 -6
  11. package/agents/project/tests/AGENTS.md +11 -5
  12. package/cli/app/config.ts +13 -14
  13. package/cli/app/index.ts +58 -0
  14. package/cli/commands/connect.ts +45 -0
  15. package/cli/commands/dev.ts +37 -13
  16. package/cli/commands/diagnose.ts +286 -0
  17. package/cli/commands/doctor.ts +18 -5
  18. package/cli/commands/explain.ts +25 -0
  19. package/cli/commands/perf.ts +243 -0
  20. package/cli/commands/trace.ts +9 -1
  21. package/cli/commands/verify.ts +281 -0
  22. package/cli/compiler/artifacts/connectedProjects.ts +453 -0
  23. package/cli/compiler/artifacts/controllers.ts +198 -49
  24. package/cli/compiler/artifacts/discovery.ts +0 -34
  25. package/cli/compiler/artifacts/manifest.ts +95 -6
  26. package/cli/compiler/artifacts/routing.ts +2 -2
  27. package/cli/compiler/artifacts/services.ts +277 -130
  28. package/cli/compiler/client/index.ts +3 -0
  29. package/cli/compiler/common/files/style.ts +52 -0
  30. package/cli/compiler/common/generatedRouteModules.ts +34 -5
  31. package/cli/compiler/common/scripts.ts +11 -5
  32. package/cli/compiler/index.ts +2 -1
  33. package/cli/compiler/server/index.ts +3 -0
  34. package/cli/presentation/commands.ts +110 -7
  35. package/cli/presentation/devSession.ts +32 -7
  36. package/cli/runtime/commands.ts +165 -6
  37. package/cli/scaffold/index.ts +18 -27
  38. package/cli/scaffold/templates.ts +48 -28
  39. package/cli/utils/agents.ts +106 -13
  40. package/cli/utils/keyboard.ts +8 -0
  41. package/client/dev/profiler/ApexChart.tsx +66 -0
  42. package/client/dev/profiler/index.tsx +2508 -302
  43. package/client/dev/profiler/runtime.noop.ts +12 -0
  44. package/client/dev/profiler/runtime.ts +195 -4
  45. package/client/services/router/request/api.ts +6 -1
  46. package/common/applicationConfig.ts +173 -0
  47. package/common/applicationConfigLoader.ts +102 -0
  48. package/common/connectedProjects.ts +113 -0
  49. package/common/dev/connect.ts +267 -0
  50. package/common/dev/console.ts +31 -0
  51. package/common/dev/contractsDoctor.ts +128 -0
  52. package/common/dev/diagnostics.ts +59 -15
  53. package/common/dev/inspection.ts +491 -0
  54. package/common/dev/performance.ts +809 -0
  55. package/common/dev/profiler.ts +3 -0
  56. package/common/dev/proteumManifest.ts +31 -6
  57. package/common/dev/requestTrace.ts +52 -1
  58. package/common/env/proteumEnv.ts +176 -50
  59. package/common/router/index.ts +1 -0
  60. package/common/router/request/api.ts +2 -0
  61. package/config.ts +5 -0
  62. package/docs/dev-commands.md +5 -1
  63. package/docs/dev-sessions.md +90 -0
  64. package/docs/diagnostics.md +74 -11
  65. package/docs/request-tracing.md +50 -3
  66. package/package.json +1 -1
  67. package/server/app/container/config.ts +16 -87
  68. package/server/app/container/console/index.ts +42 -8
  69. package/server/app/container/index.ts +10 -2
  70. package/server/app/container/trace/index.ts +105 -0
  71. package/server/app/devDiagnostics.ts +138 -0
  72. package/server/app/index.ts +18 -8
  73. package/server/app/service/container.ts +0 -12
  74. package/server/app/service/index.ts +0 -2
  75. package/server/services/prisma/index.ts +121 -4
  76. package/server/services/router/http/index.ts +305 -11
  77. package/server/services/router/index.ts +116 -57
  78. package/server/services/router/request/api.ts +160 -19
  79. package/server/services/router/request/index.ts +8 -0
  80. package/server/services/router/response/index.ts +23 -1
  81. package/server/services/router/response/page/document.tsx +31 -14
  82. package/server/services/router/response/page/index.tsx +10 -0
  83. package/agents/framework/AGENTS.md +0 -177
  84. package/server/services/auth/router/service.json +0 -6
  85. package/server/services/auth/service.json +0 -6
  86. package/server/services/cron/service.json +0 -6
  87. package/server/services/disks/drivers/local/service.json +0 -6
  88. package/server/services/disks/drivers/s3/service.json +0 -6
  89. package/server/services/disks/service.json +0 -6
  90. package/server/services/fetch/service.json +0 -7
  91. package/server/services/prisma/service.json +0 -6
  92. package/server/services/router/service.json +0 -6
  93. package/server/services/schema/router/service.json +0 -6
  94. package/server/services/schema/service.json +0 -6
  95. package/server/services/security/encrypt/aes/service.json +0 -6
@@ -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.yaml and package.json.' });
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 legacyArgs = Option.Rest();
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.legacyArgs,
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
  };
@@ -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 { ensureProjectAgentSymlinks } from '../utils/agents';
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.yaml');
137
+ const identityFilepath = path.join(appRoot, 'identity.config.ts');
137
138
  if (!fs.existsSync(identityFilepath)) {
138
- throw new UsageError(`Missing identity.yaml in ${appRoot}. Run \`proteum init\` first or target a Proteum app root.`);
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 = yaml.parse(fs.readFileSync(identityFilepath, 'utf8')) as Partial<TIdentityConfig> | null;
142
- const identifier = typeof parsed?.identifier === 'string' ? parsed.identifier.trim() : '';
143
- const name = typeof parsed?.name === 'string' ? parsed.name.trim() : '';
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.yaml in ${appRoot} is missing a valid "identifier" field.`);
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.yaml', 'client', 'server'];
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.yaml',
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 = () => `node_modules
239
- .proteum
240
- .cache
241
- bin
242
- dev
243
- var
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
- }) => `name: ${appName}
310
- identifier: ${appIdentifier}
311
- description: ${JSON.stringify(appDescription)}
312
-
313
- author:
314
- name: ${appName}
315
- url: localhost
316
- email: team@example.com
317
-
318
- social:
319
-
320
- language: en
321
- locale: en-US
322
- maincolor: white
323
- iconsPack: light
324
-
325
- web:
326
- title: ${JSON.stringify(appName)}
327
- titleSuffix: ${JSON.stringify(appName)}
328
- fullTitle: ${JSON.stringify(appName)}
329
- description: ${JSON.stringify(appDescription)}
330
- version: 0.0.1
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) => ({
@@ -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 AGENTS entrypoints mapped to their framework-owned source files.
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
- const agentSourceRoot = path.join(coreRoot, 'agents', 'project');
42
- const agentLinks = projectAgentLinkDefinitions.map((linkDefinition) => ({
43
- ...linkDefinition,
44
- sourcePath: path.join(agentSourceRoot, linkDefinition.sourcePath),
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
- ensureSymlinks(appRoot, agentLinks, '[agents]');
48
- ensureProjectSkillSymlinks({ appRoot, coreRoot });
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 ensureProjectSkillSymlinks({ appRoot, coreRoot }: TEnsureProjectAgentSymlinksArgs) {
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
- const skillLinks: TAgentLinkDefinition[] = fs
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
- ensureSymlinks(appRoot, skillLinks, '[skills]');
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 framework asset: ${sourceFilepath}`);
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);
@@ -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;