@callidescope/cli 0.0.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/LICENSE +21 -0
- package/README.md +1763 -0
- package/dist/main.module-tKMvFmcY.js +1476 -0
- package/dist/src/index.d.ts +649 -0
- package/dist/src/index.js +2 -0
- package/dist/src/main.d.ts +1 -0
- package/dist/src/main.js +17 -0
- package/package.json +80 -0
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
import { AddressCommandOptions } from '@callidescope/configuration';
|
|
2
|
+
import { AddressService } from '@callidescope/graph';
|
|
3
|
+
import { CallableAddressResolution } from '@callidescope/graph';
|
|
4
|
+
import { CallableId } from '@callidescope/core';
|
|
5
|
+
import { CallablesService } from '@callidescope/graph';
|
|
6
|
+
import { CallGraph } from '@callidescope/graph';
|
|
7
|
+
import { CallGraphResult } from '@callidescope/core';
|
|
8
|
+
import { CallidescopeLimitOverrides } from '@callidescope/configuration';
|
|
9
|
+
import { CallidescopeLimits } from '@callidescope/configuration';
|
|
10
|
+
import { CallidescopeOutputFormat } from '@callidescope/configuration';
|
|
11
|
+
import { ClassesService } from '@callidescope/graph';
|
|
12
|
+
import { ConfigurationService } from '@callidescope/configuration';
|
|
13
|
+
import { ConsoleLogger } from '@nestjs/common';
|
|
14
|
+
import { DiscoveredCallable } from '@callidescope/graph';
|
|
15
|
+
import { EntriesService } from '@callidescope/graph';
|
|
16
|
+
import { ExternalService } from '@callidescope/graph';
|
|
17
|
+
import { FileFilterService } from '@callidescope/graph';
|
|
18
|
+
import { GraphAssemblyService } from '@callidescope/graph';
|
|
19
|
+
import pino from 'pino';
|
|
20
|
+
import { ProgramService } from '@callidescope/graph';
|
|
21
|
+
import { ProjectLimitsLookup } from '@callidescope/configuration';
|
|
22
|
+
import { ProjectReportsService } from '@callidescope/output';
|
|
23
|
+
import { ResolvedCallidescopeConfiguration } from '@callidescope/configuration';
|
|
24
|
+
import { ResolvedCallidescopeEntryPoints } from '@callidescope/configuration';
|
|
25
|
+
import { ResolvedCallidescopeWriteConfiguration } from '@callidescope/configuration';
|
|
26
|
+
import { UnresolvedEntryPointAddress } from '@callidescope/graph';
|
|
27
|
+
import { WorkspaceService } from '@callidescope/graph';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* NestJS module that wires `depth` and `breadth`'s shared address resolution.
|
|
31
|
+
*/
|
|
32
|
+
export declare class AddressLookupModule {
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Resolves `depth` and `breadth`'s callable address into a callable, sharing
|
|
37
|
+
* every step the two commands would otherwise duplicate: loading the
|
|
38
|
+
* configuration, tracing the workspace, and matching the address against
|
|
39
|
+
* what was found.
|
|
40
|
+
*/
|
|
41
|
+
export declare class AddressLookupService {
|
|
42
|
+
private readonly addressService;
|
|
43
|
+
private readonly callidescopeService;
|
|
44
|
+
private readonly configurationService;
|
|
45
|
+
constructor(addressService: AddressService, callidescopeService: CallidescopeService, configurationService: ConfigurationService);
|
|
46
|
+
/**
|
|
47
|
+
* States why a resolution cannot be acted on, or nothing when it can.
|
|
48
|
+
*
|
|
49
|
+
* One message per failure kind rather than a generic "not found": an
|
|
50
|
+
* invalid address, an address matching nothing, and an address matching
|
|
51
|
+
* several declarations are each fixed a different way, and only the message
|
|
52
|
+
* for the one that actually happened tells the caller which.
|
|
53
|
+
*
|
|
54
|
+
* The candidates of an ambiguous address are rendered by `AddressService`,
|
|
55
|
+
* the same renderer the workspace run's own refusal prints, so one concept
|
|
56
|
+
* reaches a reader one way — as an address they can paste back, rather than
|
|
57
|
+
* a file location they cannot.
|
|
58
|
+
*/
|
|
59
|
+
describeProblem(args: {
|
|
60
|
+
address: string;
|
|
61
|
+
resolution: CallableAddressResolution;
|
|
62
|
+
}): string | undefined;
|
|
63
|
+
/**
|
|
64
|
+
* Every callable the trace found, written as an address that resolves to it.
|
|
65
|
+
*
|
|
66
|
+
* What a prompt completes against, so a caller picks from what is actually
|
|
67
|
+
* there rather than recalling a qualified name it has no way to look up.
|
|
68
|
+
*/
|
|
69
|
+
listAddresses(workspace: LocatedWorkspace): string[];
|
|
70
|
+
/** Loads the configuration and traces the workspace, matching nothing yet. */
|
|
71
|
+
locate(options: AddressCommandOptions): Promise<LocatedWorkspace>;
|
|
72
|
+
/** Matches an address against a workspace already traced. */
|
|
73
|
+
resolve(args: ResolveAddressArguments): CallableAddressResolution;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** What analyzing one run's callables produced. */
|
|
77
|
+
declare interface AnalyzeOutcome {
|
|
78
|
+
/**
|
|
79
|
+
* The depth and breadth limits each traced project is judged against.
|
|
80
|
+
*
|
|
81
|
+
* Held beside the result rather than inside it, the same as
|
|
82
|
+
* `unresolvedAddresses`: whether `--check breadth` even has a limit to gate
|
|
83
|
+
* on is a fact about the projects a run reached, not part of the report
|
|
84
|
+
* every destination writes.
|
|
85
|
+
*/
|
|
86
|
+
readonly projectLimits: ProjectLimitsLookup;
|
|
87
|
+
readonly result: CallGraphResult;
|
|
88
|
+
/**
|
|
89
|
+
* Declared entry-point addresses that named no callable, or more than one.
|
|
90
|
+
*
|
|
91
|
+
* Held beside the result rather than inside it: the report shape is what
|
|
92
|
+
* gets written to every destination, and an address that stopped resolving
|
|
93
|
+
* is a fact about the configuration rather than about the code it traced.
|
|
94
|
+
*/
|
|
95
|
+
readonly unresolvedAddresses: readonly UnresolvedEntryPointAddress[];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* NestJS module that wires the callidescope command and its analysis services.
|
|
100
|
+
*/
|
|
101
|
+
export declare class CallidescopeModule {
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Runs one trace of a workspace, from tsconfig files to findings.
|
|
106
|
+
*/
|
|
107
|
+
export declare class CallidescopeService {
|
|
108
|
+
private readonly callablesService;
|
|
109
|
+
private readonly classHierarchyService;
|
|
110
|
+
private readonly entryPointsService;
|
|
111
|
+
private readonly externalService;
|
|
112
|
+
private readonly fileFilterService;
|
|
113
|
+
private readonly graphAssemblyService;
|
|
114
|
+
private readonly programService;
|
|
115
|
+
private readonly configurationService;
|
|
116
|
+
private readonly projectReportsService;
|
|
117
|
+
private readonly workspaceService;
|
|
118
|
+
private readonly logger;
|
|
119
|
+
constructor(callablesService: CallablesService, classHierarchyService: ClassesService, entryPointsService: EntriesService, externalService: ExternalService, fileFilterService: FileFilterService, graphAssemblyService: GraphAssemblyService, programService: ProgramService, configurationService: ConfigurationService, projectReportsService: ProjectReportsService, workspaceService: WorkspaceService, logger: LoggerService);
|
|
120
|
+
/**
|
|
121
|
+
* Walks the workspace and collects every callable, without analyzing them.
|
|
122
|
+
*
|
|
123
|
+
* Shared by `trace`, which goes on to run the full analysis, and `locate`,
|
|
124
|
+
* which only needs the collected callables and their graph to resolve one
|
|
125
|
+
* address — entry points and project reports are work `locate`'s callers
|
|
126
|
+
* never asked for. Both read the same declarations, because a
|
|
127
|
+
* project's own `exclude` decides which files exist to be collected at all,
|
|
128
|
+
* and an address resolved against a different set of files from the one a
|
|
129
|
+
* gated run measures would be an address about a different codebase.
|
|
130
|
+
*/
|
|
131
|
+
private discoverCallables;
|
|
132
|
+
/**
|
|
133
|
+
* Finds the projects a run covers and builds a program for each.
|
|
134
|
+
*
|
|
135
|
+
* Split from `discoverCallables` because the projects have to exist before
|
|
136
|
+
* anything can be read from beside them, and the run's own filter has to
|
|
137
|
+
* exist before the projects: a project the run excludes must be dropped
|
|
138
|
+
* before its `tsconfig.json` is opened, since opening it is what fails.
|
|
139
|
+
*/
|
|
140
|
+
private discoverPrograms;
|
|
141
|
+
/**
|
|
142
|
+
* Reads what each traced project declared for itself: the callables that
|
|
143
|
+
* root its stacks, and the limits those stacks are judged against.
|
|
144
|
+
*
|
|
145
|
+
* Over the whole closure rather than the projects the run was pointed at: a
|
|
146
|
+
* dependency's callables are measured by this run, and the project that owns
|
|
147
|
+
* them is the one entitled to say what roots a stack through them. A run
|
|
148
|
+
* scoped elsewhere would otherwise judge them by whoever happened to reach
|
|
149
|
+
* them.
|
|
150
|
+
*
|
|
151
|
+
* One load for both, because the two answers come out of the same files, and
|
|
152
|
+
* reading those files twice is how a run ends up gating against limits from
|
|
153
|
+
* one read and rooting stacks from another.
|
|
154
|
+
*
|
|
155
|
+
* A project declaring no entry-point rules is simply absent from that map,
|
|
156
|
+
* and `EntriesService` falls back to the run's own configuration for it —
|
|
157
|
+
* which is why this returns rules rather than a whole configuration. Limits
|
|
158
|
+
* resolve the other way round, naming every project, because a limit has to
|
|
159
|
+
* be printable per project whether or not the project chose it.
|
|
160
|
+
*
|
|
161
|
+
* Exclusions are read from the file exactly as authored rather than from the
|
|
162
|
+
* resolved configuration, which is the split `readDeclaredLimit` already makes
|
|
163
|
+
* for
|
|
164
|
+
* a limit: resolution folds the tool's own default globs into every project's
|
|
165
|
+
* `exclude`, so the resolved array can never say whether this project
|
|
166
|
+
* excluded anything. A project that excluded nothing is left out of the map
|
|
167
|
+
* entirely, so a run in which no project excludes anything is handed back the
|
|
168
|
+
* run's own filter untouched.
|
|
169
|
+
*/
|
|
170
|
+
private loadProjectDeclarations;
|
|
171
|
+
/**
|
|
172
|
+
* Reads the deepest depth any component reached, closure included.
|
|
173
|
+
*
|
|
174
|
+
* The whole trace's number, closure included, which is what belongs in a
|
|
175
|
+
* summary describing everything a run measured. It is **not** what a scoped
|
|
176
|
+
* run is judged on, which is why the log line names it `maximumDepthTraced`
|
|
177
|
+
* rather than `maximumDepth`.
|
|
178
|
+
*/
|
|
179
|
+
private readMaximumDepth;
|
|
180
|
+
/** Derives every finding from the collected callables. */
|
|
181
|
+
analyze(args: {
|
|
182
|
+
callablesById: ReadonlyMap<CallableId, DiscoveredCallable>;
|
|
183
|
+
configuration: ResolvedCallidescopeConfiguration;
|
|
184
|
+
/** Entry-point rules a project declared for itself, keyed by project name. */
|
|
185
|
+
entryPointsByProject: ReadonlyMap<string, ResolvedCallidescopeEntryPoints>;
|
|
186
|
+
fileCount: number;
|
|
187
|
+
fileCountByProject: ReadonlyMap<string, number>;
|
|
188
|
+
projectCount: number;
|
|
189
|
+
/** The depth and breadth limits each traced project is judged against. */
|
|
190
|
+
projectLimits: ProjectLimitsLookup;
|
|
191
|
+
projectNames: readonly string[];
|
|
192
|
+
workspaceRoot: string;
|
|
193
|
+
}): AnalyzeOutcome;
|
|
194
|
+
/**
|
|
195
|
+
* Collects every callable and assembles the graph over them, without
|
|
196
|
+
* running the analysis a full trace does.
|
|
197
|
+
*
|
|
198
|
+
* For the `depth` and `breadth` commands, which resolve one address against
|
|
199
|
+
* the collected callables and then walk the graph from it — neither needs
|
|
200
|
+
* entry points or project reports, both of which `analyze` builds
|
|
201
|
+
* unconditionally.
|
|
202
|
+
*
|
|
203
|
+
* Asynchronous because discovery is: every project's own configuration is
|
|
204
|
+
* read before a callable is collected, since a project's `exclude` decides
|
|
205
|
+
* which files there are to collect. The limits those same files declare are
|
|
206
|
+
* loaded and thrown away here, which is the price of one answer about what a
|
|
207
|
+
* run's callables are rather than two.
|
|
208
|
+
*/
|
|
209
|
+
locate(args: TraceArguments): Promise<LocateOutcome>;
|
|
210
|
+
/** Traces a workspace and returns everything the run found. */
|
|
211
|
+
trace(args: TraceArguments): Promise<TraceOutcome>;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* A traced workspace, before any address has been matched against it.
|
|
216
|
+
*
|
|
217
|
+
* Held apart from the match so one trace can serve both the list a prompt
|
|
218
|
+
* completes against and the lookup that follows it. Tracing twice to offer a
|
|
219
|
+
* choice and then act on it would double the slowest thing either command
|
|
220
|
+
* does.
|
|
221
|
+
*/
|
|
222
|
+
export declare interface LocatedWorkspace {
|
|
223
|
+
readonly configuration: ResolvedCallidescopeConfiguration;
|
|
224
|
+
/** What the lookup prints, resolved from the command line and refused if unknown. */
|
|
225
|
+
readonly format: CallidescopeOutputFormat;
|
|
226
|
+
readonly located: LocateOutcome;
|
|
227
|
+
readonly workspaceRoot: string;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** The collected callables and their graph, without any analysis run over them. */
|
|
231
|
+
export declare interface LocateOutcome {
|
|
232
|
+
readonly callablesById: ReadonlyMap<CallableId, DiscoveredCallable>;
|
|
233
|
+
readonly graph: CallGraph;
|
|
234
|
+
/** Workspace-relative root of each project the run was scoped to, keyed by name. */
|
|
235
|
+
readonly startingProjectRoots: ReadonlyMap<string, string>;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Structured values that belong beside a log line rather than inside it.
|
|
240
|
+
*
|
|
241
|
+
* Counts, percentages, and durations are the values that change on every
|
|
242
|
+
* occurrence, so they are carried as fields: the message stays constant and
|
|
243
|
+
* groupable in telemetry, and the numbers stay queryable instead of having to
|
|
244
|
+
* be parsed back out of prose.
|
|
245
|
+
*
|
|
246
|
+
* The named members are the recurring ones; the index signature keeps the
|
|
247
|
+
* argument open for whatever a given call site needs to attach.
|
|
248
|
+
*/
|
|
249
|
+
declare interface LogData {
|
|
250
|
+
[key: string]: unknown;
|
|
251
|
+
/** How many things the operation handled. */
|
|
252
|
+
count?: number;
|
|
253
|
+
/** Wall-clock milliseconds the operation took. */
|
|
254
|
+
durationMs?: number;
|
|
255
|
+
/** Completion between 0 and 100. */
|
|
256
|
+
percent?: number;
|
|
257
|
+
/** How many things the operation set out to handle. */
|
|
258
|
+
total?: number;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Transient-scoped logger so each injecting class gets its own instance.
|
|
263
|
+
* Each consumer calls `setContext(ClassName.name)` to tag every log line
|
|
264
|
+
* with the originating class. Backed by pino for structured JSON output in
|
|
265
|
+
* production and human-readable pretty-print in development.
|
|
266
|
+
*
|
|
267
|
+
* Messages follow one grammar: an emoji naming the subject, a verb in present
|
|
268
|
+
* progressive or past tense, then the object. Values that vary per call —
|
|
269
|
+
* counts, percentages, durations — go in the `data` argument rather than the
|
|
270
|
+
* message, so the message stays constant enough for telemetry to group on.
|
|
271
|
+
*
|
|
272
|
+
* ```ts
|
|
273
|
+
* this.logger.info("📥 Downloading CSEL sources", undefined, { total: 428 });
|
|
274
|
+
* this.logger.info("📥 Downloaded CSEL sources", undefined, { count: 412 });
|
|
275
|
+
* ```
|
|
276
|
+
*/
|
|
277
|
+
declare @Injectable({ scope: Scope.TRANSIENT })
|
|
278
|
+
class LoggerService extends ConsoleLogger {
|
|
279
|
+
// 🏗 Dependency Injection
|
|
280
|
+
|
|
281
|
+
constructor() {
|
|
282
|
+
super();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// 🔐 Private Fields
|
|
286
|
+
|
|
287
|
+
private static readonly isProduction =
|
|
288
|
+
process.env["NODE_ENV"] === "production";
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Built on first use, not when this file is evaluated.
|
|
292
|
+
*
|
|
293
|
+
* A destination fixed at import time could only ever be chosen by this
|
|
294
|
+
* package, since every consumer's own code runs after its imports.
|
|
295
|
+
*/
|
|
296
|
+
private static rootLogger: pino.Logger | undefined;
|
|
297
|
+
|
|
298
|
+
/** Whether lines go to standard error instead of standard output. */
|
|
299
|
+
private static writesToStandardError = false;
|
|
300
|
+
|
|
301
|
+
private child: pino.Logger = LoggerService.root;
|
|
302
|
+
|
|
303
|
+
// 🔑 Public Fields
|
|
304
|
+
|
|
305
|
+
// 🔏 Private Methods
|
|
306
|
+
|
|
307
|
+
/** Build the pino instance for production or local development output. */
|
|
308
|
+
private static createRootLogger(): pino.Logger {
|
|
309
|
+
const level = process.env["LOG_LEVEL"] ?? "info";
|
|
310
|
+
|
|
311
|
+
if (LoggerService.isProduction) {
|
|
312
|
+
return LoggerService.writesToStandardError
|
|
313
|
+
? pino({ level }, pino.destination(STANDARD_ERROR_DESCRIPTOR))
|
|
314
|
+
: pino({ level });
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return pino({
|
|
318
|
+
level,
|
|
319
|
+
transport: {
|
|
320
|
+
options: {
|
|
321
|
+
colorize: true,
|
|
322
|
+
destination: LoggerService.writesToStandardError
|
|
323
|
+
? STANDARD_ERROR_DESCRIPTOR
|
|
324
|
+
: STANDARD_OUTPUT_DESCRIPTOR,
|
|
325
|
+
// The emoji is a field, not part of the message, so the console can
|
|
326
|
+
// show it while telemetry stores unadorned prose. `ignore` then keeps
|
|
327
|
+
// it from being printed a second time in the trailing object.
|
|
328
|
+
ignore: "pid,hostname,emoji",
|
|
329
|
+
messageFormat: "{emoji} {msg}",
|
|
330
|
+
singleLine: true,
|
|
331
|
+
},
|
|
332
|
+
target: "pino-pretty",
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Sends every subsequent line to standard error instead of standard output.
|
|
339
|
+
*
|
|
340
|
+
* For a command-line application whose standard output *is* its result. A log
|
|
341
|
+
* line sharing that stream is not a diagnostic beside the data, it is a
|
|
342
|
+
* corruption of it. Call it before anything logs — the first statement of the
|
|
343
|
+
* application's bootstrap.
|
|
344
|
+
*
|
|
345
|
+
* A call after the first line warns and changes nothing: the destination is
|
|
346
|
+
* fixed when the pino instance is built, and tearing down a transport
|
|
347
|
+
* somebody is writing through would be worse than refusing. The warning is
|
|
348
|
+
* the point — silently leaving the lines on standard output is how a caller
|
|
349
|
+
* would ship a corrupted pipe without ever being told.
|
|
350
|
+
*/
|
|
351
|
+
static logToStandardError(): void {
|
|
352
|
+
if (LoggerService.rootLogger !== undefined) {
|
|
353
|
+
process.emitWarning(
|
|
354
|
+
"LoggerService.logToStandardError() was called after the first log line, so log lines still go to standard output and anything piping that stream will read them as data. Call it as the first statement of the application's bootstrap.",
|
|
355
|
+
);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
LoggerService.writesToStandardError = true;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Fails a malformed message in development, and never in production.
|
|
364
|
+
*
|
|
365
|
+
* A logger that throws in production turns an observability call into an
|
|
366
|
+
* outage, so the check runs only where a developer is present to fix it.
|
|
367
|
+
*/
|
|
368
|
+
private assertConventionalMessage(args: {
|
|
369
|
+
context: string | undefined;
|
|
370
|
+
parsed: ParsedLogMessage;
|
|
371
|
+
}): void {
|
|
372
|
+
if (
|
|
373
|
+
LoggerService.isProduction ||
|
|
374
|
+
this.shouldSkipConventionalMessageValidation(args.context)
|
|
375
|
+
) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const violation = this.getConventionalMessageViolation(args.parsed);
|
|
380
|
+
|
|
381
|
+
if (violation !== undefined) {
|
|
382
|
+
throw new Error(violation);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** Assembles the object pino merges into the line. */
|
|
387
|
+
private buildBindings(args: {
|
|
388
|
+
context: string | undefined;
|
|
389
|
+
data: LogData | undefined;
|
|
390
|
+
parsed: ParsedLogMessage;
|
|
391
|
+
}): Record<string, unknown> {
|
|
392
|
+
this.assertConventionalMessage({
|
|
393
|
+
context: args.context,
|
|
394
|
+
parsed: args.parsed,
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
return {
|
|
398
|
+
...args.data,
|
|
399
|
+
context: args.context,
|
|
400
|
+
// Telemetry gets prose; only the console-bound transport reads this.
|
|
401
|
+
...(LoggerService.isProduction ? {} : { emoji: args.parsed.emoji }),
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** Returns a human-readable explanation when the message format is invalid. */
|
|
406
|
+
private getConventionalMessageViolation(
|
|
407
|
+
parsed: ParsedLogMessage,
|
|
408
|
+
): string | undefined {
|
|
409
|
+
const emoji = parsed.emoji;
|
|
410
|
+
const text = parsed.text;
|
|
411
|
+
|
|
412
|
+
if (emoji === undefined) {
|
|
413
|
+
return `Log message must start with an emoji naming its subject, then a verb: "${text}"`;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const firstWord = FIRST_WORD_PATTERN.exec(text)?.[1];
|
|
417
|
+
|
|
418
|
+
if (firstWord === undefined || !this.isConventionalVerb(firstWord)) {
|
|
419
|
+
return `Log message must begin with a verb in present progressive or past tense, got "${firstWord ?? ""}": "${emoji} ${text}"`;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return undefined;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Whether a word is a verb in one of the two tenses the convention allows.
|
|
427
|
+
*
|
|
428
|
+
* Present progressive means the operation is under way; past means it
|
|
429
|
+
* finished. Regular morphology covers both, so a new verb needs no
|
|
430
|
+
* registration anywhere — only irregular pasts are enumerated.
|
|
431
|
+
*/
|
|
432
|
+
private isConventionalVerb(word: string): boolean {
|
|
433
|
+
const lowercased = word.toLowerCase();
|
|
434
|
+
|
|
435
|
+
return (
|
|
436
|
+
lowercased.endsWith("ing") ||
|
|
437
|
+
lowercased.endsWith("ed") ||
|
|
438
|
+
IRREGULAR_PAST_VERBS.has(lowercased)
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** Splits a leading emoji off a message, leaving prose behind. */
|
|
443
|
+
private parseMessage(message: unknown): ParsedLogMessage {
|
|
444
|
+
const text = String(message);
|
|
445
|
+
const match = LEADING_EMOJI_PATTERN.exec(text);
|
|
446
|
+
const emoji = match?.[1];
|
|
447
|
+
|
|
448
|
+
return emoji === undefined
|
|
449
|
+
? { emoji: undefined, text }
|
|
450
|
+
: { emoji, text: text.slice(match?.[0].length) };
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** Whether a context is intentionally exempt from the validation rule. */
|
|
454
|
+
private shouldSkipConventionalMessageValidation(
|
|
455
|
+
context: string | undefined,
|
|
456
|
+
): boolean {
|
|
457
|
+
return context !== undefined && UNVALIDATED_LOG_CONTEXTS.has(context);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// 🌎 Public Methods
|
|
461
|
+
|
|
462
|
+
/** The pino instance every logger's child is taken from. */
|
|
463
|
+
private static get root(): pino.Logger {
|
|
464
|
+
LoggerService.rootLogger ??= LoggerService.createRootLogger();
|
|
465
|
+
|
|
466
|
+
return LoggerService.rootLogger;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** Normalizes unknown errors into a stable message and timestamped log line. */
|
|
470
|
+
buildErrorLogEntry(
|
|
471
|
+
context: string,
|
|
472
|
+
error: unknown,
|
|
473
|
+
): { errorMessage: string; logLine: string } {
|
|
474
|
+
const errorMessage =
|
|
475
|
+
error instanceof Error ? error.stack || error.message : String(error);
|
|
476
|
+
|
|
477
|
+
return {
|
|
478
|
+
errorMessage,
|
|
479
|
+
logLine: `[${new Date().toISOString()}] ${context}: ${errorMessage}\n`,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/** Builds a timestamped output log file path and ensures the output directory exists. */
|
|
484
|
+
createTimestampedOutputLogFilePath(filePrefix: string): string {
|
|
485
|
+
const outputDirectory = path.join(process.cwd(), "output");
|
|
486
|
+
if (!existsSync(outputDirectory)) {
|
|
487
|
+
mkdirSync(outputDirectory, { recursive: true });
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return path.join(
|
|
491
|
+
outputDirectory,
|
|
492
|
+
`${filePrefix}-${new Date().toISOString().replaceAll(/[:.]/g, "-")}.log`,
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/** Logs a debug message at the `debug` level. */
|
|
497
|
+
override debug(message: unknown, context?: string, data?: LogData): void {
|
|
498
|
+
const parsed = this.parseMessage(message);
|
|
499
|
+
this.child.debug(
|
|
500
|
+
this.buildBindings({ context: context ?? this.context, data, parsed }),
|
|
501
|
+
parsed.text,
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Logs an error message at the `error` level, optionally including a stack trace.
|
|
507
|
+
*
|
|
508
|
+
* `ConsoleLogger.error` spends a third slot on a context string that the
|
|
509
|
+
* other levels do not have, so this one accepts either: a string keeps
|
|
510
|
+
* NestJS's meaning, an object is structured data like everywhere else.
|
|
511
|
+
*/
|
|
512
|
+
override error(
|
|
513
|
+
message: unknown,
|
|
514
|
+
stackOrContext?: string,
|
|
515
|
+
contextOrData?: LogData | string,
|
|
516
|
+
): void {
|
|
517
|
+
const parsed = this.parseMessage(message);
|
|
518
|
+
const data = typeof contextOrData === "object" ? contextOrData : undefined;
|
|
519
|
+
const context =
|
|
520
|
+
typeof contextOrData === "string" ? contextOrData : this.context;
|
|
521
|
+
|
|
522
|
+
this.child.error(
|
|
523
|
+
{
|
|
524
|
+
...this.buildBindings({ context, data, parsed }),
|
|
525
|
+
stack: stackOrContext,
|
|
526
|
+
},
|
|
527
|
+
parsed.text,
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/** Logs an informational message at the `info` level. */
|
|
532
|
+
info(message: unknown, context?: string, data?: LogData): void {
|
|
533
|
+
const parsed = this.parseMessage(message);
|
|
534
|
+
this.child.info(
|
|
535
|
+
this.buildBindings({ context: context ?? this.context, data, parsed }),
|
|
536
|
+
parsed.text,
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Logs an informational message at the `info` level.
|
|
542
|
+
*
|
|
543
|
+
* NestJS and `nest-commander` call this method directly as part of the
|
|
544
|
+
* framework's own `LoggerService` contract, so it must keep working
|
|
545
|
+
* exactly as before. Application code should call `info` instead — the
|
|
546
|
+
* same behavior under a name that says what level it logs at.
|
|
547
|
+
*/
|
|
548
|
+
override log(message: unknown, context?: string, data?: LogData): void {
|
|
549
|
+
this.info(message, context, data);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/** Sets the context label included in every subsequent log line. */
|
|
553
|
+
override setContext(context: string): void {
|
|
554
|
+
super.setContext(context);
|
|
555
|
+
this.child = LoggerService.root.child({ context });
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/** Logs a verbose message at the `trace` level. */
|
|
559
|
+
override verbose(message: unknown, context?: string, data?: LogData): void {
|
|
560
|
+
const parsed = this.parseMessage(message);
|
|
561
|
+
this.child.trace(
|
|
562
|
+
this.buildBindings({ context: context ?? this.context, data, parsed }),
|
|
563
|
+
parsed.text,
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/** Logs a warning message at the `warn` level. */
|
|
568
|
+
override warn(message: unknown, context?: string, data?: LogData): void {
|
|
569
|
+
const parsed = this.parseMessage(message);
|
|
570
|
+
this.child.warn(
|
|
571
|
+
this.buildBindings({ context: context ?? this.context, data, parsed }),
|
|
572
|
+
parsed.text,
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Root NestJS application module.
|
|
579
|
+
*/
|
|
580
|
+
export declare class MainModule {
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/** A message split into the emoji the console shows and the prose telemetry stores. */
|
|
584
|
+
declare interface ParsedLogMessage {
|
|
585
|
+
emoji: string | undefined;
|
|
586
|
+
text: string;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/** Arguments for matching an address against an already-traced workspace. */
|
|
590
|
+
declare interface ResolveAddressArguments {
|
|
591
|
+
readonly address: string;
|
|
592
|
+
readonly workspace: LocatedWorkspace;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/** Arguments for one full trace of a workspace. */
|
|
596
|
+
export declare interface TraceArguments {
|
|
597
|
+
/**
|
|
598
|
+
* The limits the workspace file itself wrote down, exactly as authored.
|
|
599
|
+
*
|
|
600
|
+
* Carried so the workspace row names the workspace file only when that file
|
|
601
|
+
* really wrote the number: resolution defaults `maximumDepth` for every run,
|
|
602
|
+
* and a path stamped from the resolved object alone would name a file for a
|
|
603
|
+
* number it never mentions.
|
|
604
|
+
*/
|
|
605
|
+
readonly authoredLimits?: CallidescopeLimits | undefined;
|
|
606
|
+
readonly configuration: ResolvedCallidescopeConfiguration;
|
|
607
|
+
/**
|
|
608
|
+
* The file `configuration` was read from, when a file was found at all.
|
|
609
|
+
*
|
|
610
|
+
* Carried so the trace can skip it while resolving a configuration beside
|
|
611
|
+
* every project it reaches: one file holds one role per run, and a package
|
|
612
|
+
* whose task names its own file would otherwise have it read a second time
|
|
613
|
+
* as that package's project configuration.
|
|
614
|
+
*/
|
|
615
|
+
readonly configurationPath?: string | undefined;
|
|
616
|
+
/** Project directories to trace. Every project in the workspace when empty. */
|
|
617
|
+
readonly directories: readonly string[];
|
|
618
|
+
/**
|
|
619
|
+
* The limits this run's command line overrode, if any.
|
|
620
|
+
*
|
|
621
|
+
* Carried into the trace because a limit is enforced per project: each
|
|
622
|
+
* project's own file declares the number its gate reads, so an override that
|
|
623
|
+
* stopped at the run's own configuration would be a flag no gate looks at.
|
|
624
|
+
*/
|
|
625
|
+
readonly limitOverrides?: CallidescopeLimitOverrides | undefined;
|
|
626
|
+
readonly workspaceRoot: string;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/** What one trace produced, alongside the projects it covered. */
|
|
630
|
+
export declare interface TraceOutcome extends AnalyzeOutcome {
|
|
631
|
+
/** Every project the run measured, its dependency closure included. */
|
|
632
|
+
readonly projectNames: readonly string[];
|
|
633
|
+
/**
|
|
634
|
+
* Workspace-relative root of each project the run was scoped to, keyed by
|
|
635
|
+
* name — the starting projects, not the closure they reached.
|
|
636
|
+
*/
|
|
637
|
+
readonly startingProjectRoots: ReadonlyMap<string, string>;
|
|
638
|
+
/**
|
|
639
|
+
* The written destinations each project declared for itself, by name.
|
|
640
|
+
*
|
|
641
|
+
* Carried out of the trace rather than re-read at write time: the files were
|
|
642
|
+
* already opened once to decide what the run measures, and reading them a
|
|
643
|
+
* second time is how a run comes to publish against one answer and measure
|
|
644
|
+
* against another.
|
|
645
|
+
*/
|
|
646
|
+
readonly writeByProject: ReadonlyMap<string, ResolvedCallidescopeWriteConfiguration>;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
export { }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
package/dist/src/main.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { o as e, t } from "../main.module-tKMvFmcY.js";
|
|
3
|
+
import { CommandFactory as n } from "nest-commander";
|
|
4
|
+
import "reflect-metadata";
|
|
5
|
+
//#region packages/ic-suite/callidescope/callidescope-cli/src/main.ts
|
|
6
|
+
async function r() {
|
|
7
|
+
let r = new e();
|
|
8
|
+
r.setContext("CommandFactory"), await n.run(t, {
|
|
9
|
+
bufferLogs: !0,
|
|
10
|
+
logger: r,
|
|
11
|
+
serviceErrorHandler: (e) => {
|
|
12
|
+
r.error("🔭 Failed a run", e.stack, { reason: e.message }), process.exitCode = 1;
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
r();
|
|
17
|
+
//#endregion
|