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.
- package/AGENTS.md +37 -49
- package/README.md +52 -1
- package/agents/framework/AGENTS.md +104 -236
- package/agents/project/AGENTS.md +36 -70
- package/cli/commands/command.ts +243 -0
- package/cli/commands/commandLocalRunner.js +198 -0
- package/cli/commands/dev.ts +95 -1
- package/cli/commands/doctor.ts +8 -74
- package/cli/commands/explain.ts +8 -194
- package/cli/commands/trace.ts +8 -0
- package/cli/compiler/artifacts/commands.ts +217 -0
- package/cli/compiler/artifacts/manifest.ts +17 -2
- package/cli/compiler/artifacts/services.ts +291 -0
- package/cli/compiler/client/index.ts +13 -0
- package/cli/compiler/common/commands.ts +175 -0
- package/cli/compiler/common/proteumManifest.ts +15 -124
- package/cli/compiler/index.ts +25 -2
- package/cli/compiler/server/index.ts +3 -0
- package/cli/presentation/commands.ts +37 -5
- package/cli/runtime/commands.ts +29 -1
- package/cli/tsconfig.json +4 -1
- package/cli/utils/check.ts +1 -1
- package/client/app/component.tsx +11 -0
- package/client/dev/profiler/index.tsx +1511 -0
- package/client/dev/profiler/noop.tsx +5 -0
- package/client/dev/profiler/runtime.noop.ts +116 -0
- package/client/dev/profiler/runtime.ts +840 -0
- package/client/services/router/components/router.tsx +30 -2
- package/client/services/router/index.tsx +25 -0
- package/client/services/router/request/api.ts +133 -17
- package/commands/proteum/diagnostics.ts +11 -0
- package/common/dev/commands.ts +50 -0
- package/common/dev/diagnostics.ts +298 -0
- package/common/dev/profiler.ts +91 -0
- package/common/dev/proteumManifest.ts +135 -0
- package/common/dev/requestTrace.ts +28 -1
- package/docs/dev-commands.md +86 -0
- package/docs/request-tracing.md +2 -0
- package/package.json +1 -2
- package/server/app/commands.ts +35 -370
- package/server/app/commandsManager.ts +393 -0
- package/server/app/container/console/index.ts +0 -2
- package/server/app/container/trace/index.ts +88 -8
- package/server/app/devCommands.ts +192 -0
- package/server/app/devDiagnostics.ts +53 -0
- package/server/app/index.ts +27 -4
- package/server/services/cron/CronTask.ts +73 -5
- package/server/services/cron/index.ts +34 -11
- package/server/services/fetch/index.ts +3 -10
- package/server/services/prisma/index.ts +1 -1
- package/server/services/router/http/index.ts +132 -21
- package/server/services/router/index.ts +40 -4
- package/server/services/router/request/api.ts +30 -1
- package/skills/clean-project-code/SKILL.md +7 -2
- package/test-results/.last-run.json +4 -0
- package/types/aliases.d.ts +6 -0
|
@@ -28,6 +28,11 @@ export type TProps = { service?: ClientRouter; loaderComponent?: React.Component
|
|
|
28
28
|
----------------------------------*/
|
|
29
29
|
|
|
30
30
|
const LogPrefix = `[router][component]`;
|
|
31
|
+
const withProfiler = <T,>(callback: (runtime: (typeof import('@client/dev/profiler/runtime'))['profilerRuntime']) => T) => {
|
|
32
|
+
if (!__DEV__) return undefined as T | undefined;
|
|
33
|
+
const profilerModule = require('@client/dev/profiler/runtime') as typeof import('@client/dev/profiler/runtime');
|
|
34
|
+
return callback(profilerModule.profilerRuntime);
|
|
35
|
+
};
|
|
31
36
|
|
|
32
37
|
const PageLoading = ({
|
|
33
38
|
clientRouter,
|
|
@@ -91,6 +96,12 @@ export default ({ service: clientRouter, loaderComponent }: TProps) => {
|
|
|
91
96
|
}
|
|
92
97
|
|
|
93
98
|
// Set loading state
|
|
99
|
+
const sessionId = withProfiler((runtime) =>
|
|
100
|
+
runtime.startNavigationSession({
|
|
101
|
+
path: request.path,
|
|
102
|
+
url: request.url,
|
|
103
|
+
}),
|
|
104
|
+
);
|
|
94
105
|
clientRouter.runHook('page.change', request);
|
|
95
106
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
96
107
|
clientRouter.setLoading(true);
|
|
@@ -99,21 +110,25 @@ export default ({ service: clientRouter, loaderComponent }: TProps) => {
|
|
|
99
110
|
// Unable to load (no connection, server error, ....)
|
|
100
111
|
if (newpage === null) return;
|
|
101
112
|
|
|
102
|
-
return await changePage(newpage, data, request);
|
|
113
|
+
return await changePage(newpage, data, request, sessionId);
|
|
103
114
|
};
|
|
104
115
|
|
|
105
|
-
async function changePage(newpage: Page, data?: {}, request?: ClientRequest) {
|
|
116
|
+
async function changePage(newpage: Page, data?: {}, request?: ClientRequest, sessionId?: string) {
|
|
106
117
|
// Fetch API data to hydrate the page
|
|
107
118
|
try {
|
|
108
119
|
await newpage.preRender();
|
|
109
120
|
} catch (error) {
|
|
110
121
|
console.error(LogPrefix, 'Unable to fetch data:', error);
|
|
122
|
+
withProfiler((runtime) =>
|
|
123
|
+
runtime.failNavigation(error instanceof Error ? error.message : String(error), sessionId),
|
|
124
|
+
);
|
|
111
125
|
clientRouter?.setLoading(false);
|
|
112
126
|
return;
|
|
113
127
|
}
|
|
114
128
|
|
|
115
129
|
// Add additional data
|
|
116
130
|
if (data) newpage.data = { ...newpage.data, ...data };
|
|
131
|
+
withProfiler((runtime) => runtime.startRenderStep(sessionId));
|
|
117
132
|
|
|
118
133
|
// Add page container
|
|
119
134
|
setCurrentPage((page) => {
|
|
@@ -172,6 +187,19 @@ export default ({ service: clientRouter, loaderComponent }: TProps) => {
|
|
|
172
187
|
currentPage?.updateClient();
|
|
173
188
|
// Scroll to the selected content via url hash
|
|
174
189
|
restoreScroll(currentPage);
|
|
190
|
+
const routeLabel =
|
|
191
|
+
currentPage && 'path' in currentPage.route && currentPage.route.path
|
|
192
|
+
? currentPage.route.path
|
|
193
|
+
: currentPage && 'code' in currentPage.route
|
|
194
|
+
? String(currentPage.route.code)
|
|
195
|
+
: undefined;
|
|
196
|
+
withProfiler((runtime) =>
|
|
197
|
+
runtime.finishNavigation({
|
|
198
|
+
chunkId: currentPage?.chunkId,
|
|
199
|
+
routeLabel,
|
|
200
|
+
title: currentPage?.title,
|
|
201
|
+
}),
|
|
202
|
+
);
|
|
175
203
|
|
|
176
204
|
// Hooks
|
|
177
205
|
clientRouter.runHook('page.changed', (currentPage?.context.request || context.request) as ClientRequest);
|
|
@@ -52,6 +52,11 @@ import appRoutes from '@generated/client/routes';
|
|
|
52
52
|
const debug = false;
|
|
53
53
|
const LogPrefix = '[router]';
|
|
54
54
|
const browserWindow = window as Window & { routes?: TSsrUnresolvedRoute[]; ssr?: TBasicSSrData };
|
|
55
|
+
const withProfiler = <T,>(callback: (runtime: (typeof import('@client/dev/profiler/runtime'))['profilerRuntime']) => T) => {
|
|
56
|
+
if (!__DEV__) return undefined as T | undefined;
|
|
57
|
+
const profilerModule = require('@client/dev/profiler/runtime') as typeof import('@client/dev/profiler/runtime');
|
|
58
|
+
return callback(profilerModule.profilerRuntime);
|
|
59
|
+
};
|
|
55
60
|
|
|
56
61
|
/*----------------------------------
|
|
57
62
|
- TYPES
|
|
@@ -320,12 +325,19 @@ export default class ClientRouter<
|
|
|
320
325
|
|
|
321
326
|
// Create response
|
|
322
327
|
debug && console.log(LogPrefix, 'Resolved request', request.path, '| Route:', route);
|
|
328
|
+
withProfiler((runtime) =>
|
|
329
|
+
runtime.completeResolveStep({
|
|
330
|
+
chunkId: 'chunk' in route ? route.chunk : route.options.id,
|
|
331
|
+
routeLabel: request.path,
|
|
332
|
+
}),
|
|
333
|
+
);
|
|
323
334
|
const page = await this.createResponse(route, request);
|
|
324
335
|
|
|
325
336
|
return page;
|
|
326
337
|
}
|
|
327
338
|
|
|
328
339
|
const notFoundRoute = this.errors[404];
|
|
340
|
+
withProfiler((runtime) => runtime.completeResolveStep({ routeLabel: '404' }));
|
|
329
341
|
return await this.createResponse(notFoundRoute, request, { error: new Error('Page not found') });
|
|
330
342
|
}
|
|
331
343
|
|
|
@@ -337,16 +349,21 @@ export default class ClientRouter<
|
|
|
337
349
|
//throw new Error(`Failed to load route: ${route.chunk}`);
|
|
338
350
|
|
|
339
351
|
debug && console.log(`Fetching route ${route.chunk} ...`, route);
|
|
352
|
+
const stepId = withProfiler((runtime) => runtime.startChunkStep(route.chunk));
|
|
340
353
|
try {
|
|
341
354
|
const loaded = await route.load();
|
|
342
355
|
const fetched = loaded.__register(this.app);
|
|
343
356
|
|
|
344
357
|
debug && console.log(`Route fetched: ${route.chunk}`, fetched);
|
|
358
|
+
withProfiler((runtime) => runtime.finishStep(stepId));
|
|
345
359
|
|
|
346
360
|
if ('code' in route) return fetched as TClientPageErrorRoute<this>;
|
|
347
361
|
|
|
348
362
|
return { ...(fetched as TClientPageRoute<this>), regex: route.regex, keys: route.keys };
|
|
349
363
|
} catch (e) {
|
|
364
|
+
withProfiler((runtime) =>
|
|
365
|
+
runtime.finishStep(stepId, 'error', e instanceof Error ? e.message : String(e)),
|
|
366
|
+
);
|
|
350
367
|
console.error(`Failed to fetch the route ${route.chunk}`, e);
|
|
351
368
|
try {
|
|
352
369
|
this.app.handleUpdate();
|
|
@@ -367,6 +384,13 @@ export default class ClientRouter<
|
|
|
367
384
|
if (!route) throw new Error(`Unable to resolve route.`);
|
|
368
385
|
|
|
369
386
|
const request = new ClientRequest(location, this);
|
|
387
|
+
withProfiler((runtime) =>
|
|
388
|
+
runtime.ensureInitialSession({
|
|
389
|
+
path: request.path,
|
|
390
|
+
requestId: this.ssrContext?.request.id,
|
|
391
|
+
url: request.url,
|
|
392
|
+
}),
|
|
393
|
+
);
|
|
370
394
|
|
|
371
395
|
// Restituate SSR response
|
|
372
396
|
let apiData: {} = {};
|
|
@@ -384,6 +408,7 @@ export default class ClientRouter<
|
|
|
384
408
|
|
|
385
409
|
ReactDOM.hydrate(<App context={response.context as AppPropsContext} />, document.body, () => {
|
|
386
410
|
console.log(`Render complete`);
|
|
411
|
+
withProfiler((runtime) => runtime.markInitialHydrated({ chunkId: response.chunkId, title: response.title }));
|
|
387
412
|
|
|
388
413
|
this.runHook('page.rendered', request);
|
|
389
414
|
});
|
|
@@ -24,6 +24,16 @@ import { toMultipart } from './multipart';
|
|
|
24
24
|
----------------------------------*/
|
|
25
25
|
|
|
26
26
|
const debug = false;
|
|
27
|
+
const getProfilerModule = () => {
|
|
28
|
+
if (!__DEV__) return undefined;
|
|
29
|
+
return require('@client/dev/profiler/runtime') as typeof import('@client/dev/profiler/runtime');
|
|
30
|
+
};
|
|
31
|
+
const withProfiler = <T>(callback: (runtime: (typeof import('@client/dev/profiler/runtime'))['profilerRuntime']) => T) => {
|
|
32
|
+
const profilerModule = getProfilerModule();
|
|
33
|
+
return profilerModule ? callback(profilerModule.profilerRuntime) : undefined;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type TExecuteResult<TData> = { data: TData; durationMs: number; response: Response };
|
|
27
37
|
|
|
28
38
|
export type Config = {};
|
|
29
39
|
|
|
@@ -126,8 +136,54 @@ export default class ApiClient implements ApiClientService {
|
|
|
126
136
|
): Promise<TData> {
|
|
127
137
|
/*if (options?.captcha !== undefined)
|
|
128
138
|
await this.gui.captcha.check(options?.captcha);*/
|
|
139
|
+
const pendingTrace = withProfiler((runtime) =>
|
|
140
|
+
runtime.startTrace('async', {
|
|
141
|
+
label: `${method} ${path}`,
|
|
142
|
+
method,
|
|
143
|
+
path,
|
|
144
|
+
}),
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const result = await this.executeDetailed<TData>('client-async', method, path, data, options);
|
|
149
|
+
const profilerModule = getProfilerModule();
|
|
150
|
+
const traceRequestId = profilerModule?.readProfilerTraceRequestId(result.response);
|
|
151
|
+
|
|
152
|
+
if (pendingTrace && traceRequestId) {
|
|
153
|
+
await profilerModule?.profilerRuntime.attachTraceByRequestId(
|
|
154
|
+
pendingTrace.sessionId,
|
|
155
|
+
pendingTrace.traceId,
|
|
156
|
+
traceRequestId,
|
|
157
|
+
);
|
|
158
|
+
} else if (pendingTrace) {
|
|
159
|
+
withProfiler((runtime) =>
|
|
160
|
+
runtime.completeTrace(pendingTrace.traceId, {
|
|
161
|
+
durationMs: result.durationMs,
|
|
162
|
+
status: 'completed',
|
|
163
|
+
}),
|
|
164
|
+
);
|
|
165
|
+
}
|
|
129
166
|
|
|
130
|
-
|
|
167
|
+
return result.data;
|
|
168
|
+
} catch (error) {
|
|
169
|
+
const profilerModule = getProfilerModule();
|
|
170
|
+
const errorResponse = (error as Error & { response?: Response }).response;
|
|
171
|
+
const traceRequestId = errorResponse ? profilerModule?.readProfilerTraceRequestId(errorResponse) : undefined;
|
|
172
|
+
if (pendingTrace && traceRequestId) {
|
|
173
|
+
await profilerModule?.profilerRuntime.attachTraceByRequestId(
|
|
174
|
+
pendingTrace.sessionId,
|
|
175
|
+
pendingTrace.traceId,
|
|
176
|
+
traceRequestId,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
withProfiler((runtime) =>
|
|
180
|
+
runtime.completeTrace(pendingTrace?.traceId, {
|
|
181
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
182
|
+
status: 'error',
|
|
183
|
+
}),
|
|
184
|
+
);
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
131
187
|
}
|
|
132
188
|
|
|
133
189
|
public async fetchSync(fetchers: TFetcherList, alreadyLoadedData: {}): Promise<TObjetDonnees> {
|
|
@@ -145,23 +201,68 @@ export default class ApiClient implements ApiClientService {
|
|
|
145
201
|
const fetchedData =
|
|
146
202
|
fetchersCount === 0
|
|
147
203
|
? {}
|
|
148
|
-
: await
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
204
|
+
: await (async () => {
|
|
205
|
+
const pendingTrace = withProfiler((runtime) =>
|
|
206
|
+
runtime.startTrace('navigation-data', {
|
|
207
|
+
fetcherIds: Object.keys(fetchersToRun),
|
|
208
|
+
label: 'Navigation data',
|
|
209
|
+
method: 'POST',
|
|
210
|
+
path: '/api',
|
|
211
|
+
}),
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
const result = await this.executeDetailed<TObjetDonnees>(
|
|
216
|
+
'client-navigation',
|
|
217
|
+
'POST',
|
|
218
|
+
'/api',
|
|
219
|
+
({ fetchers: fetchersToRun } as unknown) as TPostData,
|
|
220
|
+
);
|
|
221
|
+
const profilerModule = getProfilerModule();
|
|
222
|
+
const traceRequestId = profilerModule?.readProfilerTraceRequestId(result.response);
|
|
223
|
+
|
|
224
|
+
if (pendingTrace && traceRequestId) {
|
|
225
|
+
await profilerModule?.profilerRuntime.attachTraceByRequestId(
|
|
226
|
+
pendingTrace.sessionId,
|
|
227
|
+
pendingTrace.traceId,
|
|
228
|
+
traceRequestId,
|
|
229
|
+
);
|
|
230
|
+
} else if (pendingTrace) {
|
|
231
|
+
withProfiler((runtime) =>
|
|
232
|
+
runtime.completeTrace(pendingTrace.traceId, {
|
|
233
|
+
durationMs: result.durationMs,
|
|
234
|
+
status: 'completed',
|
|
235
|
+
}),
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const responseData: TObjetDonnees = {};
|
|
240
|
+
for (const id in result.data) responseData[id] = result.data[id];
|
|
241
|
+
return responseData;
|
|
242
|
+
} catch (e) {
|
|
243
|
+
const profilerModule = getProfilerModule();
|
|
244
|
+
const errorResponse = (e as Error & { response?: Response }).response;
|
|
245
|
+
const traceRequestId = errorResponse ? profilerModule?.readProfilerTraceRequestId(errorResponse) : undefined;
|
|
246
|
+
if (pendingTrace && traceRequestId) {
|
|
247
|
+
await profilerModule?.profilerRuntime.attachTraceByRequestId(
|
|
248
|
+
pendingTrace.sessionId,
|
|
249
|
+
pendingTrace.traceId,
|
|
250
|
+
traceRequestId,
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
withProfiler((runtime) =>
|
|
254
|
+
runtime.completeTrace(pendingTrace?.traceId, {
|
|
255
|
+
errorMessage: e instanceof Error ? e.message : String(e),
|
|
256
|
+
status: 'error',
|
|
257
|
+
}),
|
|
258
|
+
);
|
|
259
|
+
|
|
160
260
|
// API Error hook
|
|
161
|
-
this.app.handleError(e);
|
|
261
|
+
this.app.handleError(e as Error);
|
|
162
262
|
|
|
163
263
|
throw e;
|
|
164
|
-
}
|
|
264
|
+
}
|
|
265
|
+
})();
|
|
165
266
|
|
|
166
267
|
// Errors will be catched in the caller
|
|
167
268
|
|
|
@@ -214,21 +315,36 @@ export default class ApiClient implements ApiClientService {
|
|
|
214
315
|
};
|
|
215
316
|
|
|
216
317
|
public execute<TData = unknown>(...args: TFetcherArgs): Promise<TData> {
|
|
318
|
+
return this.executeDetailed<TData>('client-async', ...args).then((result) => result.data);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
private async executeDetailed<TData = unknown>(
|
|
322
|
+
profilerOrigin: string,
|
|
323
|
+
...args: TFetcherArgs
|
|
324
|
+
): Promise<TExecuteResult<TData>> {
|
|
217
325
|
const { url, config } = this.configure(...args);
|
|
326
|
+
const startedAt = Date.now();
|
|
327
|
+
const headers = config.headers instanceof Headers ? config.headers : new Headers(config.headers as HeadersInit);
|
|
328
|
+
const profilerHeaders = withProfiler((runtime) => runtime.getRequestHeaders(profilerOrigin)) || {};
|
|
329
|
+
for (const [key, value] of Object.entries(profilerHeaders)) headers.set(key, value);
|
|
330
|
+
config.headers = headers;
|
|
218
331
|
|
|
219
332
|
console.log(`[api] Fetching`, url, config);
|
|
220
333
|
|
|
221
334
|
return fetch(url, config)
|
|
222
335
|
.then(async (response) => {
|
|
336
|
+
const requestDurationMs = Math.max(0, Date.now() - startedAt);
|
|
223
337
|
if (!response.ok) {
|
|
224
338
|
const errorData = await response.json();
|
|
225
339
|
console.warn(`[api] Failure:`, response.status, errorData);
|
|
226
|
-
const error = errorFromJson(errorData);
|
|
340
|
+
const error = errorFromJson(errorData) as Error & { durationMs?: number; response?: Response };
|
|
341
|
+
error.durationMs = requestDurationMs;
|
|
342
|
+
error.response = response;
|
|
227
343
|
throw error;
|
|
228
344
|
}
|
|
229
345
|
const json = (await response.json()) as TData;
|
|
230
346
|
debug && console.log(`[api] Success:`, json);
|
|
231
|
-
return json;
|
|
347
|
+
return { data: json, durationMs: requestDurationMs, response };
|
|
232
348
|
})
|
|
233
349
|
.catch((error) => {
|
|
234
350
|
if (error instanceof TypeError) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Commands } from '@server/app/commands';
|
|
2
|
+
|
|
3
|
+
export default class ProteumDiagnosticsCommands extends Commands {
|
|
4
|
+
public async ping() {
|
|
5
|
+
return {
|
|
6
|
+
app: this.app.identity.identifier,
|
|
7
|
+
envProfile: this.app.env.profile,
|
|
8
|
+
services: Object.keys(this.app.getRootServices()),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { TTraceSummaryValue } from './requestTrace';
|
|
2
|
+
|
|
3
|
+
export type TDevCommandScope = 'app' | 'framework';
|
|
4
|
+
export type TDevCommandSourceLocation = { line: number; column: number };
|
|
5
|
+
export type TDevCommandExecutionStatus = 'completed' | 'error';
|
|
6
|
+
|
|
7
|
+
export type TDevCommandDefinition = {
|
|
8
|
+
path: string;
|
|
9
|
+
className: string;
|
|
10
|
+
methodName: string;
|
|
11
|
+
importPath: string;
|
|
12
|
+
filepath: string;
|
|
13
|
+
sourceLocation: TDevCommandSourceLocation;
|
|
14
|
+
scope: TDevCommandScope;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type TDevCommandSerializedResult = {
|
|
18
|
+
json?: unknown;
|
|
19
|
+
summary: TTraceSummaryValue;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type TDevCommandExecution = {
|
|
23
|
+
command: TDevCommandDefinition;
|
|
24
|
+
startedAt: string;
|
|
25
|
+
finishedAt: string;
|
|
26
|
+
durationMs: number;
|
|
27
|
+
status: TDevCommandExecutionStatus;
|
|
28
|
+
result?: TDevCommandSerializedResult;
|
|
29
|
+
errorMessage?: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type TDevCommandListResponse = {
|
|
33
|
+
commands: TDevCommandDefinition[];
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type TDevCommandRunResponse = {
|
|
37
|
+
execution: TDevCommandExecution;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type TDevCommandErrorResponse = {
|
|
41
|
+
error: string;
|
|
42
|
+
execution?: TDevCommandExecution;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const normalizeDevCommandPath = (value: string) =>
|
|
46
|
+
value
|
|
47
|
+
.trim()
|
|
48
|
+
.replace(/^\/+/, '')
|
|
49
|
+
.replace(/\/+$/, '')
|
|
50
|
+
.replace(/\/{2,}/g, '/');
|