proteum 2.1.0-5 → 2.1.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.
Files changed (56) hide show
  1. package/AGENTS.md +37 -49
  2. package/README.md +52 -1
  3. package/agents/framework/AGENTS.md +104 -236
  4. package/agents/project/AGENTS.md +36 -70
  5. package/cli/commands/command.ts +243 -0
  6. package/cli/commands/commandLocalRunner.js +198 -0
  7. package/cli/commands/dev.ts +95 -1
  8. package/cli/commands/doctor.ts +8 -74
  9. package/cli/commands/explain.ts +8 -194
  10. package/cli/commands/trace.ts +8 -0
  11. package/cli/compiler/artifacts/commands.ts +217 -0
  12. package/cli/compiler/artifacts/manifest.ts +17 -2
  13. package/cli/compiler/artifacts/services.ts +291 -0
  14. package/cli/compiler/client/index.ts +13 -0
  15. package/cli/compiler/common/commands.ts +175 -0
  16. package/cli/compiler/common/proteumManifest.ts +15 -124
  17. package/cli/compiler/index.ts +25 -2
  18. package/cli/compiler/server/index.ts +3 -0
  19. package/cli/presentation/commands.ts +37 -5
  20. package/cli/runtime/commands.ts +29 -1
  21. package/cli/tsconfig.json +4 -1
  22. package/cli/utils/check.ts +1 -1
  23. package/client/app/component.tsx +11 -0
  24. package/client/dev/profiler/index.tsx +1511 -0
  25. package/client/dev/profiler/noop.tsx +5 -0
  26. package/client/dev/profiler/runtime.noop.ts +116 -0
  27. package/client/dev/profiler/runtime.ts +840 -0
  28. package/client/services/router/components/router.tsx +30 -2
  29. package/client/services/router/index.tsx +25 -0
  30. package/client/services/router/request/api.ts +133 -17
  31. package/commands/proteum/diagnostics.ts +11 -0
  32. package/common/dev/commands.ts +50 -0
  33. package/common/dev/diagnostics.ts +298 -0
  34. package/common/dev/profiler.ts +91 -0
  35. package/common/dev/proteumManifest.ts +135 -0
  36. package/common/dev/requestTrace.ts +28 -1
  37. package/docs/dev-commands.md +86 -0
  38. package/docs/request-tracing.md +2 -0
  39. package/package.json +1 -2
  40. package/server/app/commands.ts +35 -370
  41. package/server/app/commandsManager.ts +393 -0
  42. package/server/app/container/console/index.ts +0 -2
  43. package/server/app/container/trace/index.ts +88 -8
  44. package/server/app/devCommands.ts +192 -0
  45. package/server/app/devDiagnostics.ts +53 -0
  46. package/server/app/index.ts +27 -4
  47. package/server/services/cron/CronTask.ts +73 -5
  48. package/server/services/cron/index.ts +34 -11
  49. package/server/services/fetch/index.ts +3 -10
  50. package/server/services/prisma/index.ts +1 -1
  51. package/server/services/router/http/index.ts +132 -21
  52. package/server/services/router/index.ts +40 -4
  53. package/server/services/router/request/api.ts +30 -1
  54. package/skills/clean-project-code/SKILL.md +7 -2
  55. package/test-results/.last-run.json +4 -0
  56. package/types/aliases.d.ts +6 -0
@@ -0,0 +1,175 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import ts from 'typescript';
4
+
5
+ export type TCommandSourceLocation = { line: number; column: number };
6
+
7
+ export type TCommandMethodMeta = {
8
+ name: string;
9
+ path: string;
10
+ sourceLocation: TCommandSourceLocation;
11
+ };
12
+
13
+ export type TCommandFileMeta = {
14
+ importPath: string;
15
+ filepath: string;
16
+ className: string;
17
+ commandBasePath: string;
18
+ methods: TCommandMethodMeta[];
19
+ };
20
+
21
+ type TCommandSearchDir = { importPrefix: string; root: string };
22
+
23
+ const getCommandSegments = (relativePath: string) => {
24
+ const segments = relativePath
25
+ .replace(/\.ts$/, '')
26
+ .split('/')
27
+ .filter(Boolean);
28
+
29
+ if (segments[segments.length - 1] === 'index') {
30
+ segments.pop();
31
+ }
32
+
33
+ return segments;
34
+ };
35
+
36
+ const getCommandBasePathFromFilepath = (filepath: string, root: string) =>
37
+ getCommandSegments(path.relative(root, filepath).replace(/\\/g, '/')).join('/');
38
+
39
+ const getGeneratedClassName = (filepath: string) => {
40
+ const filename = path.basename(filepath, '.ts').replace(/[^A-Za-z0-9_$]+/g, '_');
41
+ const normalized = filename.length ? filename : 'Commands';
42
+
43
+ return normalized[0].toUpperCase() + normalized.substring(1);
44
+ };
45
+
46
+ const buildImportPath = (searchDir: TCommandSearchDir, filepath: string) =>
47
+ searchDir.importPrefix + path.relative(searchDir.root, filepath).replace(/\\/g, '/').replace(/\.ts$/, '');
48
+
49
+ const findCommandFiles = (dir: string): string[] => {
50
+ if (!fs.existsSync(dir)) return [];
51
+
52
+ const files: string[] = [];
53
+
54
+ for (const dirent of fs.readdirSync(dir, { withFileTypes: true })) {
55
+ const filepath = path.join(dir, dirent.name);
56
+
57
+ if (dirent.isDirectory()) {
58
+ files.push(...findCommandFiles(filepath));
59
+ continue;
60
+ }
61
+
62
+ if (!dirent.isFile()) continue;
63
+ if (!dirent.name.endsWith('.ts')) continue;
64
+ if (dirent.name.endsWith('.d.ts')) continue;
65
+
66
+ files.push(filepath);
67
+ }
68
+
69
+ return files;
70
+ };
71
+
72
+ const parseSourceFile = (filepath: string, code: string) =>
73
+ ts.createSourceFile(
74
+ filepath,
75
+ code,
76
+ ts.ScriptTarget.Latest,
77
+ true,
78
+ filepath.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
79
+ );
80
+
81
+ const getNodeLocation = (sourceFile: ts.SourceFile, node: ts.Node): TCommandSourceLocation => {
82
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
83
+
84
+ return { line: line + 1, column: character + 1 };
85
+ };
86
+
87
+ const hasModifier = (node: ts.Node, kind: ts.SyntaxKind) =>
88
+ !!node.modifiers?.some((modifier) => modifier.kind === kind);
89
+
90
+ const getDefaultExportClass = (sourceFile: ts.SourceFile) => {
91
+ const classes = new Map<string, ts.ClassDeclaration>();
92
+
93
+ for (const statement of sourceFile.statements) {
94
+ if (ts.isClassDeclaration(statement) && statement.name) {
95
+ classes.set(statement.name.text, statement);
96
+
97
+ if (hasModifier(statement, ts.SyntaxKind.DefaultKeyword)) {
98
+ return statement;
99
+ }
100
+ }
101
+ }
102
+
103
+ for (const statement of sourceFile.statements) {
104
+ if (!ts.isExportAssignment(statement) || statement.isExportEquals) continue;
105
+
106
+ if (ts.isIdentifier(statement.expression)) {
107
+ return classes.get(statement.expression.text);
108
+ }
109
+ }
110
+
111
+ return undefined;
112
+ };
113
+
114
+ const getExportedString = (sourceFile: ts.SourceFile, exportName: string) => {
115
+ for (const statement of sourceFile.statements) {
116
+ if (!ts.isVariableStatement(statement)) continue;
117
+ if (!hasModifier(statement, ts.SyntaxKind.ExportKeyword)) continue;
118
+
119
+ for (const declaration of statement.declarationList.declarations) {
120
+ if (!ts.isIdentifier(declaration.name)) continue;
121
+ if (declaration.name.text !== exportName) continue;
122
+ if (!declaration.initializer || !ts.isStringLiteral(declaration.initializer)) continue;
123
+
124
+ return declaration.initializer.text;
125
+ }
126
+ }
127
+
128
+ return undefined;
129
+ };
130
+
131
+ export const indexCommands = (searchDirs: TCommandSearchDir[]) => {
132
+ const commands: TCommandFileMeta[] = [];
133
+
134
+ for (const searchDir of searchDirs) {
135
+ const commandFiles = findCommandFiles(searchDir.root);
136
+
137
+ for (const filepath of commandFiles.sort((a, b) => a.localeCompare(b))) {
138
+ const code = fs.readFileSync(filepath, 'utf8');
139
+ const sourceFile = parseSourceFile(filepath, code);
140
+
141
+ const commandPathOverride = getExportedString(sourceFile, 'commandPath');
142
+ const defaultClass = getDefaultExportClass(sourceFile);
143
+
144
+ if (!defaultClass) continue;
145
+
146
+ const className = defaultClass.name?.text || getGeneratedClassName(filepath);
147
+ const commandBasePath = commandPathOverride || getCommandBasePathFromFilepath(filepath, searchDir.root);
148
+ const methods: TCommandMethodMeta[] = [];
149
+
150
+ for (const member of defaultClass.members) {
151
+ if (!ts.isMethodDeclaration(member)) continue;
152
+ if (!member.body) continue;
153
+ if (!member.name || !ts.isIdentifier(member.name)) continue;
154
+
155
+ methods.push({
156
+ name: member.name.text,
157
+ path: [commandBasePath, member.name.text].filter(Boolean).join('/'),
158
+ sourceLocation: getNodeLocation(sourceFile, member.name),
159
+ });
160
+ }
161
+
162
+ if (!methods.length) continue;
163
+
164
+ commands.push({
165
+ filepath,
166
+ importPath: buildImportPath(searchDir, filepath),
167
+ className,
168
+ commandBasePath,
169
+ methods,
170
+ });
171
+ }
172
+ }
173
+
174
+ return commands.sort((a, b) => a.filepath.localeCompare(b.filepath));
175
+ };
@@ -2,130 +2,21 @@ import path from 'path';
2
2
  import fs from 'fs-extra';
3
3
 
4
4
  import writeIfChanged from '../writeIfChanged';
5
-
6
- export type TProteumManifestScope = 'app' | 'framework';
7
- export type TProteumManifestSourceLocation = { line: number; column: number };
8
- export type TProteumManifestRouteTargetResolution = 'literal' | 'static-expression' | 'dynamic-expression';
9
- export type TProteumManifestDiagnosticLevel = 'warning' | 'error';
10
-
11
- export type TProteumManifestDiagnostic = {
12
- level: TProteumManifestDiagnosticLevel;
13
- code: string;
14
- message: string;
15
- filepath: string;
16
- sourceLocation?: TProteumManifestSourceLocation;
17
- relatedFilepaths?: string[];
18
- };
19
-
20
- export type TProteumManifestService = {
21
- kind: 'service' | 'ref';
22
- id?: string;
23
- registeredName: string;
24
- metaName?: string;
25
- parent: string;
26
- priority: number;
27
- importPath?: string;
28
- sourceDir?: string;
29
- metasFilepath?: string;
30
- refTo?: string;
31
- scope: TProteumManifestScope;
32
- };
33
-
34
- export type TProteumManifestController = {
35
- className: string;
36
- importPath: string;
37
- filepath: string;
38
- sourceLocation: TProteumManifestSourceLocation;
39
- routeBasePath: string;
40
- methodName: string;
41
- inputCallsCount: number;
42
- hasInput: boolean;
43
- routePath: string;
44
- httpPath: string;
45
- clientAccessor: string;
46
- scope: TProteumManifestScope;
47
- };
48
-
49
- export type TProteumManifestRoute = {
50
- kind: 'client-page' | 'client-error' | 'server-route';
51
- methodName: string;
52
- serviceLocalName: string;
53
- filepath: string;
54
- sourceLocation: TProteumManifestSourceLocation;
55
- targetResolution: TProteumManifestRouteTargetResolution;
56
- path?: string;
57
- pathRaw?: string;
58
- code?: number;
59
- codeRaw?: string;
60
- optionKeys: string[];
61
- normalizedOptionKeys: string[];
62
- invalidOptionKeys: string[];
63
- reservedOptionKeys: string[];
64
- optionsRaw?: string;
65
- hasSetup: boolean;
66
- chunkId?: string;
67
- chunkFilepath?: string;
68
- scope: TProteumManifestScope;
69
- };
70
-
71
- export type TProteumManifestLayout = {
72
- chunkId: string;
73
- filepath: string;
74
- importPath: string;
75
- depth: number;
76
- scope: TProteumManifestScope;
77
- };
78
-
79
- export type TProteumManifest = {
80
- version: 1;
81
- app: {
82
- root: string;
83
- coreRoot: string;
84
- identityFilepath: string;
85
- identity: {
86
- name: string;
87
- identifier: string;
88
- description: string;
89
- language?: string;
90
- locale?: string;
91
- title?: string;
92
- titleSuffix?: string;
93
- fullTitle?: string;
94
- webDescription?: string;
95
- version?: string;
96
- };
97
- };
98
- conventions: {
99
- routeSetupOptionKeys: string[];
100
- reservedRouteSetupKeys: string[];
101
- };
102
- env: {
103
- source: string;
104
- loadedVariableKeys: string[];
105
- requiredVariables: {
106
- key: string;
107
- possibleValues: string[];
108
- provided: boolean;
109
- }[];
110
- resolved: {
111
- name: string;
112
- profile: string;
113
- routerPort: number;
114
- routerCurrentDomain: string;
115
- };
116
- };
117
- services: {
118
- app: TProteumManifestService[];
119
- routerPlugins: TProteumManifestService[];
120
- };
121
- controllers: TProteumManifestController[];
122
- routes: {
123
- client: TProteumManifestRoute[];
124
- server: TProteumManifestRoute[];
125
- };
126
- layouts: TProteumManifestLayout[];
127
- diagnostics: TProteumManifestDiagnostic[];
128
- };
5
+ import type { TProteumManifest } from '@common/dev/proteumManifest';
6
+
7
+ export type {
8
+ TProteumManifest,
9
+ TProteumManifestCommand,
10
+ TProteumManifestController,
11
+ TProteumManifestDiagnostic,
12
+ TProteumManifestDiagnosticLevel,
13
+ TProteumManifestLayout,
14
+ TProteumManifestRoute,
15
+ TProteumManifestRouteTargetResolution,
16
+ TProteumManifestScope,
17
+ TProteumManifestService,
18
+ TProteumManifestSourceLocation,
19
+ } from '@common/dev/proteumManifest';
129
20
 
130
21
  export const getProteumManifestPath = (appRoot: string) => path.join(appRoot, '.proteum', 'manifest.json');
131
22
 
@@ -16,6 +16,7 @@ import { writeClientManifest } from './common/clientManifest';
16
16
  import { logVerbose } from '../runtime/verbose';
17
17
  import { createCompileReporter, type TCompileReporter } from '../presentation/compileReporter';
18
18
  import { generateRoutingArtifacts } from './artifacts/routing';
19
+ import { generateCommandArtifacts } from './artifacts/commands';
19
20
  import { generateControllerArtifacts } from './artifacts/controllers';
20
21
  import { generateServiceArtifacts } from './artifacts/services';
21
22
  import { writeCurrentProteumManifest } from './artifacts/manifest';
@@ -32,6 +33,7 @@ export default class Compiler {
32
33
  public compiling: { [compiler: string]: Promise<void> } = {};
33
34
  private recentCompilationResults: { [compiler: string]: TRecentCompilationResult } = {};
34
35
  private recentModifiedFiles: { [compiler: string]: string[] } = {};
36
+ private manualModifiedFiles: { [compiler: string]: Set<string> } = {};
35
37
  private refreshingGeneratedArtifacts?: Promise<void>;
36
38
  private compileReporter?: TCompileReporter;
37
39
 
@@ -140,11 +142,13 @@ export default class Compiler {
140
142
  this.refreshingGeneratedArtifacts = (async () => {
141
143
  const services = generateServiceArtifacts();
142
144
  const controllers = generateControllerArtifacts();
145
+ const commands = generateCommandArtifacts();
143
146
  const { clientRoutes, serverRoutes, layouts } = generateRoutingArtifacts();
144
147
 
145
148
  writeCurrentProteumManifest({
146
149
  services,
147
150
  controllers,
151
+ commands,
148
152
  routes: { client: clientRoutes, server: serverRoutes },
149
153
  layouts,
150
154
  });
@@ -172,6 +176,21 @@ export default class Compiler {
172
176
  return recentCompilationResults;
173
177
  }
174
178
 
179
+ public noteManualModifiedFiles(compilerNames: string[] | string, filepaths: string[]) {
180
+ const normalizedCompilerNames = Array.isArray(compilerNames) ? compilerNames : [compilerNames];
181
+ const normalizedFilepaths = filepaths.map((filepath) => normalizePath(path.resolve(filepath)));
182
+
183
+ for (const compilerName of normalizedCompilerNames) {
184
+ const pendingFiles = this.manualModifiedFiles[compilerName] || new Set<string>();
185
+
186
+ for (const filepath of normalizedFilepaths) {
187
+ pendingFiles.add(filepath);
188
+ }
189
+
190
+ this.manualModifiedFiles[compilerName] = pendingFiles;
191
+ }
192
+ }
193
+
175
194
  public async create() {
176
195
  await this.warmupApp();
177
196
 
@@ -204,9 +223,13 @@ export default class Compiler {
204
223
  compiler.hooks.compile.tap(name, (compilation) => {
205
224
  this.callbacks.before && this.callbacks.before(compiler);
206
225
 
207
- this.recentModifiedFiles[name] = [...(compiler.modifiedFiles ? [...compiler.modifiedFiles] : [])].map(
208
- (filepath) => normalizePath(path.resolve(filepath)),
226
+ const compilerModifiedFiles = [...(compiler.modifiedFiles ? [...compiler.modifiedFiles] : [])].map((filepath) =>
227
+ normalizePath(path.resolve(filepath)),
209
228
  );
229
+ const manualModifiedFiles = [...(this.manualModifiedFiles[name] || [])];
230
+ delete this.manualModifiedFiles[name];
231
+
232
+ this.recentModifiedFiles[name] = [...new Set([...compilerModifiedFiles, ...manualModifiedFiles])];
210
233
 
211
234
  this.compiling[name] = new Promise((resolve) => (finished = resolve));
212
235
 
@@ -184,9 +184,12 @@ export default function createCompiler(
184
184
  // Prisma 7 generates TypeScript entrypoints under var/prisma.
185
185
  app.paths.root + '/var/prisma',
186
186
 
187
+ app.paths.root + '/commands',
188
+
187
189
  // Dossiers server uniquement pour le bundle server
188
190
  app.paths.root + '/server',
189
191
  app.paths.server.generated,
192
+ ...frameworkRoots.map((rootPath) => rootPath + '/commands'),
190
193
  ...frameworkRoots.map((rootPath) => rootPath + '/client'),
191
194
  ...frameworkRoots.map((rootPath) => rootPath + '/common'),
192
195
  ...frameworkRoots.map((rootPath) => rootPath + '/server'),
@@ -14,6 +14,7 @@ export const proteumCommandNames = [
14
14
  'doctor',
15
15
  'explain',
16
16
  'trace',
17
+ 'command',
17
18
  ] as const;
18
19
 
19
20
  export type TProteumCommandName = (typeof proteumCommandNames)[number];
@@ -46,7 +47,7 @@ export const proteumRecommendedFlow: TRow[] = [
46
47
  export const proteumCommandGroups: Array<{ title: string; names: TProteumCommandName[] }> = [
47
48
  { title: 'Daily workflow', names: ['dev', 'refresh', 'build'] },
48
49
  { title: 'Quality gates', names: ['typecheck', 'lint', 'check'] },
49
- { title: 'Manifest and contracts', names: ['doctor', 'explain', 'trace'] },
50
+ { title: 'Manifest and contracts', names: ['doctor', 'explain', 'trace', 'command'] },
50
51
  { title: 'Project scaffolding', names: ['init'] },
51
52
  ];
52
53
 
@@ -171,14 +172,14 @@ export const proteumCommands: Record<TProteumCommandName, TProteumCommandDoc> =
171
172
  name: 'explain',
172
173
  category: 'Manifest and contracts',
173
174
  summary: 'Explain the generated Proteum manifest.',
174
- usage: 'proteum explain [--all|--app|--conventions|--env|--services|--controllers|--routes|--layouts|--diagnostics] [--json]',
175
+ usage: 'proteum explain [--all|--app|--conventions|--env|--services|--controllers|--commands|--routes|--layouts|--diagnostics] [--json]',
175
176
  bestFor:
176
- 'Inspecting how source files became generated routes, controllers, layouts, services, and diagnostics without reading compiler internals.',
177
+ 'Inspecting how source files became generated routes, controllers, commands, layouts, services, and diagnostics without reading compiler internals.',
177
178
  examples: [
178
179
  { description: 'Show the default human summary', command: 'proteum explain' },
179
180
  {
180
- description: 'Inspect generated routes and controllers together',
181
- command: 'proteum explain --routes --controllers',
181
+ description: 'Inspect generated routes, controllers, and commands together',
182
+ command: 'proteum explain --routes --controllers --commands',
182
183
  },
183
184
  { description: 'Emit the selected manifest sections as JSON', command: 'proteum explain --routes --json' },
184
185
  ],
@@ -206,6 +207,37 @@ export const proteumCommands: Record<TProteumCommandName, TProteumCommandDoc> =
206
207
  ],
207
208
  status: 'experimental',
208
209
  },
210
+ command: {
211
+ name: 'command',
212
+ category: 'Manifest and contracts',
213
+ summary: 'Run a dev-only app command from /commands or against an existing dev instance.',
214
+ usage: 'proteum command <path> [--port <port>|--url <baseUrl>] [--json]',
215
+ bestFor:
216
+ 'Internal testing, debugging, and one-off service execution that should not be exposed as a normal controller or route.',
217
+ examples: [
218
+ {
219
+ description: 'Run a local command through a temporary bundled dev server',
220
+ command: 'proteum command proteum/diagnostics/ping',
221
+ },
222
+ {
223
+ description: 'Run a command against an existing dev server',
224
+ command: 'proteum command proteum/diagnostics/ping --port 3101',
225
+ },
226
+ {
227
+ description: 'Emit the command execution as JSON',
228
+ command: 'proteum command proteum/diagnostics/ping --json',
229
+ },
230
+ ],
231
+ notes: [
232
+ 'Commands live under `./commands/**/*.ts` and default-export a class that extends `{ Commands }` from `@server/app/commands`.',
233
+ 'Methods are addressed by file path plus method name, mirroring controller path generation.',
234
+ 'Proteum creates `./commands/tsconfig.json` and `.proteum/server/commands.d.ts` so `/commands` gets a command-specific alias/type project.',
235
+ 'Prefer `extends Commands` directly inside `/commands`; importing the app class is still supported through a generated command-only `@/server/index` type alias.',
236
+ 'Without `--port` or `--url`, Proteum refreshes generated artifacts, builds the dev output, starts a temporary local dev server, runs the command, and exits.',
237
+ 'With `--port` or `--url`, Proteum talks to the running app over the dev-only `__proteum/commands` HTTP endpoints.',
238
+ ],
239
+ status: 'experimental',
240
+ },
209
241
  };
210
242
 
211
243
  export const isLikelyProteumAppRoot = (workdir: string) =>
@@ -153,6 +153,7 @@ class ExplainCommand extends ProteumCommand {
153
153
  public env = Option.Boolean('--env', false, { description: 'Include the env section.' });
154
154
  public services = Option.Boolean('--services', false, { description: 'Include the services section.' });
155
155
  public controllers = Option.Boolean('--controllers', false, { description: 'Include the controllers section.' });
156
+ public commands = Option.Boolean('--commands', false, { description: 'Include the commands section.' });
156
157
  public routes = Option.Boolean('--routes', false, { description: 'Include the routes section.' });
157
158
  public layouts = Option.Boolean('--layouts', false, { description: 'Include the layouts section.' });
158
159
  public diagnostics = Option.Boolean('--diagnostics', false, {
@@ -169,6 +170,7 @@ class ExplainCommand extends ProteumCommand {
169
170
  env: this.env,
170
171
  services: this.services,
171
172
  controllers: this.controllers,
173
+ commands: this.commands,
172
174
  routes: this.routes,
173
175
  layouts: this.layouts,
174
176
  diagnostics: this.diagnostics,
@@ -177,7 +179,7 @@ class ExplainCommand extends ProteumCommand {
177
179
  applyLegacyBooleanArgs(
178
180
  'explain',
179
181
  this.legacyArgs,
180
- ['json', 'all', 'app', 'conventions', 'env', 'services', 'controllers', 'routes', 'layouts', 'diagnostics'],
182
+ ['json', 'all', 'app', 'conventions', 'env', 'services', 'controllers', 'commands', 'routes', 'layouts', 'diagnostics'],
181
183
  args,
182
184
  );
183
185
  this.setCliArgs(args);
@@ -214,6 +216,30 @@ class TraceCommand extends ProteumCommand {
214
216
  }
215
217
  }
216
218
 
219
+ class CommandCommand extends ProteumCommand {
220
+ public static paths = [['command']];
221
+
222
+ public static usage = buildUsage('command');
223
+
224
+ public port = Option.String('--port', { description: 'Target an existing dev server on the given port.' });
225
+ public url = Option.String('--url', { description: 'Target an existing dev server at the given base URL.' });
226
+ public json = Option.Boolean('--json', false, { description: 'Print JSON output.' });
227
+ public args = Option.Rest();
228
+
229
+ public async execute() {
230
+ const [path = ''] = this.args;
231
+
232
+ this.setCliArgs({
233
+ path,
234
+ port: this.port ?? '',
235
+ url: this.url ?? '',
236
+ json: this.json,
237
+ });
238
+
239
+ await runCommandModule(() => import('../commands/command'));
240
+ }
241
+ }
242
+
217
243
  export const registeredCommands = {
218
244
  init: InitCommand,
219
245
  dev: DevCommand,
@@ -225,6 +251,7 @@ export const registeredCommands = {
225
251
  doctor: DoctorCommand,
226
252
  explain: ExplainCommand,
227
253
  trace: TraceCommand,
254
+ command: CommandCommand,
228
255
  } as const;
229
256
 
230
257
  export const createCli = (version: string) => {
@@ -247,6 +274,7 @@ export const createCli = (version: string) => {
247
274
  clipanion.register(DoctorCommand);
248
275
  clipanion.register(ExplainCommand);
249
276
  clipanion.register(TraceCommand);
277
+ clipanion.register(CommandCommand);
250
278
 
251
279
  return clipanion;
252
280
  };
package/cli/tsconfig.json CHANGED
@@ -29,9 +29,12 @@
29
29
  "outDir": "./bin",
30
30
 
31
31
  "paths": {
32
+ "@client/*": [ "../client/*" ],
32
33
  "@cli/*": [ "./*" ],
33
34
  "@cli/app": [ "./app" ],
34
- "@cli": [ "./" ]
35
+ "@cli": [ "./" ],
36
+ "@common/*": [ "../common/*" ],
37
+ "@server/*": [ "../server/*" ]
35
38
  },
36
39
  },
37
40
 
@@ -5,7 +5,7 @@ import cli from '..';
5
5
  import Compiler from '../compiler';
6
6
  import { runProcess } from './runProcess';
7
7
 
8
- const tsconfigPaths = ['client/tsconfig.json', 'server/tsconfig.json'];
8
+ const tsconfigPaths = ['client/tsconfig.json', 'server/tsconfig.json', 'commands/tsconfig.json'];
9
9
  const eslintConfigPaths = ['eslint.config.mjs', 'eslint.config.js', 'eslint.config.cjs'];
10
10
 
11
11
  const resolveInstalledBinary = (name: string) => {
@@ -21,10 +21,17 @@ export default function App({ context }: { context: ClientContext }) {
21
21
  const curLayout = context.page?.layout;
22
22
  const [layout, setLayout] = React.useState<Layout | false | undefined>(curLayout);
23
23
  const [apiData, setApiData] = React.useState<{ [k: string]: any } | null>(context.page?.data || {});
24
+ const shouldEnableDevProfiler = __DEV__ && typeof window !== 'undefined' && window.dev;
25
+ const [isDevProfilerMounted, setDevProfilerMounted] = React.useState(false);
24
26
 
25
27
  // TODO: context.page is always provided in the context on the client side
26
28
  if (context.app.side === 'client') context.app.setLayout = setLayout;
27
29
 
30
+ React.useEffect(() => {
31
+ if (!shouldEnableDevProfiler) return;
32
+ setDevProfilerMounted(true);
33
+ }, [shouldEnableDevProfiler]);
34
+
28
35
  const layoutProps: LayoutProps = {
29
36
  ...context,
30
37
  context,
@@ -32,10 +39,14 @@ export default function App({ context }: { context: ClientContext }) {
32
39
  menu: undefined,
33
40
  children: undefined,
34
41
  };
42
+ const DevProfiler = shouldEnableDevProfiler
43
+ ? ((require('@client/dev/profiler') as typeof import('@client/dev/profiler')).default as React.ComponentType)
44
+ : null;
35
45
 
36
46
  return (
37
47
  <ReactClientContext.Provider value={context}>
38
48
  <DialogManager />
49
+ {DevProfiler && isDevProfilerMounted ? <DevProfiler /> : null}
39
50
 
40
51
  {!layout ? (
41
52
  <RouterComponent service={context.Router} />