pi-jscpd 0.1.0
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/CHANGELOG.md +99 -0
- package/CONTRIBUTING.md +144 -0
- package/LICENSE +21 -0
- package/README.md +231 -0
- package/SECURITY.md +93 -0
- package/docs/automatic-checkpoint.md +235 -0
- package/docs/compatibility.md +119 -0
- package/docs/effect-architecture.md +128 -0
- package/docs/fallow-coexistence.md +120 -0
- package/docs/overlay-interaction.md +347 -0
- package/docs/release.md +115 -0
- package/package.json +86 -0
- package/scripts/check-compatibility.mjs +103 -0
- package/skills/jscpd/SKILL.md +90 -0
- package/src/acknowledgements.ts +268 -0
- package/src/automatic.ts +396 -0
- package/src/baseline.ts +400 -0
- package/src/capability.ts +569 -0
- package/src/changed-files.ts +372 -0
- package/src/changed.ts +548 -0
- package/src/clone-identity.ts +373 -0
- package/src/config.ts +414 -0
- package/src/contract.ts +39 -0
- package/src/dispatch.ts +90 -0
- package/src/effect/clock.ts +10 -0
- package/src/effect/errors.ts +311 -0
- package/src/effect/filesystem.ts +240 -0
- package/src/effect/runtime-boundary.ts +25 -0
- package/src/effect/runtime-contract.ts +18 -0
- package/src/effect/services.ts +131 -0
- package/src/extension.ts +708 -0
- package/src/fallow.ts +479 -0
- package/src/finding-presentation.ts +73 -0
- package/src/index.ts +8 -0
- package/src/jscpd-report.ts +819 -0
- package/src/jscpd.ts +748 -0
- package/src/overlay.ts +1166 -0
- package/src/parser.ts +189 -0
- package/src/path-utils.ts +44 -0
- package/src/presentation.ts +232 -0
- package/src/process.ts +425 -0
- package/src/registry.ts +102 -0
- package/src/scan.ts +441 -0
- package/src/scheduler.ts +434 -0
- package/src/session-state.ts +229 -0
- package/src/status.ts +534 -0
- package/src/types.ts +334 -0
- package/src/value-utils.ts +14 -0
- package/src/verification.ts +220 -0
package/src/status.ts
ADDED
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
import { CONFIG_DIR_NAME } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Context, Effect, Layer, MutableRef } from "effect";
|
|
3
|
+
import type { JscpdCapabilityResult, JscpdCapabilityService } from "./capability.js";
|
|
4
|
+
import type { JscpdConfigLoadResult, JscpdConfigService, JscpdConfigSource } from "./config.js";
|
|
5
|
+
import type {
|
|
6
|
+
JscpdProcess,
|
|
7
|
+
JscpdWorkflowRequirements as JscpdRuntimeRequirements,
|
|
8
|
+
} from "./effect/services.js";
|
|
9
|
+
import type { JscpdFallowCoexistenceService } from "./fallow.js";
|
|
10
|
+
import { renderJscpdCommandHelp } from "./registry.js";
|
|
11
|
+
import { capabilityProbeEffect } from "./scan.js";
|
|
12
|
+
import type {
|
|
13
|
+
JscpdCommandExecutor,
|
|
14
|
+
JscpdExecutionContext,
|
|
15
|
+
JscpdExecutionResult,
|
|
16
|
+
JscpdLastCheck,
|
|
17
|
+
JscpdStatusResult,
|
|
18
|
+
} from "./types.js";
|
|
19
|
+
|
|
20
|
+
export interface JscpdStatusService {
|
|
21
|
+
inspectEffect: (
|
|
22
|
+
context: JscpdExecutionContext,
|
|
23
|
+
) => Effect.Effect<JscpdStatusResult, never, JscpdProcess>;
|
|
24
|
+
record(result: JscpdExecutionResult, expectedScope?: number): void;
|
|
25
|
+
recordEffect: (result: JscpdExecutionResult, expectedScope?: number) => Effect.Effect<void>;
|
|
26
|
+
restore(lastCheck?: JscpdLastCheck): void;
|
|
27
|
+
lastCheck(): JscpdLastCheck;
|
|
28
|
+
/** Lifecycle generation used to reject results from a superseded branch. */
|
|
29
|
+
scope(): number;
|
|
30
|
+
readonly scopeEffect: Effect.Effect<number>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface JscpdSessionModeService {
|
|
34
|
+
isEnabled(): boolean;
|
|
35
|
+
source(): "configuration" | "session";
|
|
36
|
+
override(): "enabled" | "disabled" | null;
|
|
37
|
+
restore(configuredEnabled: boolean, override?: "enabled" | "disabled" | null): void;
|
|
38
|
+
enable(): void;
|
|
39
|
+
disable(): void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface SessionModeState {
|
|
43
|
+
readonly enabled: boolean;
|
|
44
|
+
readonly source: "configuration" | "session";
|
|
45
|
+
readonly modeOverride: "enabled" | "disabled" | null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface JscpdSessionModeEffectService {
|
|
49
|
+
readonly isEnabled: Effect.Effect<boolean>;
|
|
50
|
+
readonly source: Effect.Effect<"configuration" | "session">;
|
|
51
|
+
readonly override: Effect.Effect<"enabled" | "disabled" | null>;
|
|
52
|
+
readonly restore: (
|
|
53
|
+
configuredEnabled: boolean,
|
|
54
|
+
override?: "enabled" | "disabled" | null,
|
|
55
|
+
) => Effect.Effect<void>;
|
|
56
|
+
readonly enable: Effect.Effect<void>;
|
|
57
|
+
readonly disable: Effect.Effect<void>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const JscpdSessionMode = Context.GenericTag<JscpdSessionModeEffectService>(
|
|
61
|
+
"pi-jscpd/effect/SessionMode",
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
interface JscpdStatusWorkflowService {
|
|
65
|
+
readonly inspect: (
|
|
66
|
+
context: JscpdExecutionContext,
|
|
67
|
+
) => Effect.Effect<JscpdStatusResult, never, JscpdProcess>;
|
|
68
|
+
readonly record: (result: JscpdExecutionResult, expectedScope?: number) => Effect.Effect<void>;
|
|
69
|
+
readonly restore: (lastCheck?: JscpdLastCheck) => Effect.Effect<void>;
|
|
70
|
+
readonly lastCheck: Effect.Effect<JscpdLastCheck>;
|
|
71
|
+
readonly scope: Effect.Effect<number>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const JscpdStatusWorkflow = Context.GenericTag<JscpdStatusWorkflowService>(
|
|
75
|
+
"pi-jscpd/effect/StatusWorkflow",
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
export function createJscpdSessionModeService(): JscpdSessionModeService {
|
|
79
|
+
return sessionModeServiceFor(new SessionModeOwner());
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function createJscpdSessionModeLayer(
|
|
83
|
+
service: JscpdSessionModeService = createJscpdSessionModeService(),
|
|
84
|
+
) {
|
|
85
|
+
return Layer.succeed(JscpdSessionMode, {
|
|
86
|
+
isEnabled: Effect.sync(() => service.isEnabled()),
|
|
87
|
+
source: Effect.sync(() => service.source()),
|
|
88
|
+
override: Effect.sync(() => service.override()),
|
|
89
|
+
restore: (configuredEnabled, override) =>
|
|
90
|
+
Effect.sync(() => service.restore(configuredEnabled, override)),
|
|
91
|
+
enable: Effect.sync(() => service.enable()),
|
|
92
|
+
disable: Effect.sync(() => service.disable()),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
class SessionModeOwner {
|
|
97
|
+
readonly #state = MutableRef.make<SessionModeState>({
|
|
98
|
+
enabled: true,
|
|
99
|
+
source: "configuration",
|
|
100
|
+
modeOverride: null,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
isEnabled(): boolean {
|
|
104
|
+
return MutableRef.get(this.#state).enabled;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
source(): "configuration" | "session" {
|
|
108
|
+
return MutableRef.get(this.#state).source;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
override(): "enabled" | "disabled" | null {
|
|
112
|
+
return MutableRef.get(this.#state).modeOverride;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
restore(
|
|
116
|
+
configuredEnabled: boolean,
|
|
117
|
+
restoredOverride: "enabled" | "disabled" | null = null,
|
|
118
|
+
): void {
|
|
119
|
+
MutableRef.set(this.#state, {
|
|
120
|
+
modeOverride: restoredOverride,
|
|
121
|
+
enabled: restoredOverride === null ? configuredEnabled : restoredOverride === "enabled",
|
|
122
|
+
source: restoredOverride === null ? "configuration" : "session",
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
enable(): void {
|
|
127
|
+
MutableRef.set(this.#state, { enabled: true, source: "session", modeOverride: "enabled" });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
disable(): void {
|
|
131
|
+
MutableRef.set(this.#state, { enabled: false, source: "session", modeOverride: "disabled" });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function sessionModeServiceFor(owner: SessionModeOwner): JscpdSessionModeService {
|
|
136
|
+
return {
|
|
137
|
+
isEnabled: () => owner.isEnabled(),
|
|
138
|
+
source: () => owner.source(),
|
|
139
|
+
override: () => owner.override(),
|
|
140
|
+
restore: (configuredEnabled, override) => owner.restore(configuredEnabled, override),
|
|
141
|
+
enable: () => owner.enable(),
|
|
142
|
+
disable: () => owner.disable(),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Route all registered commands while retaining bounded session status. */
|
|
147
|
+
export function createJscpdStatusAwareExecutor(
|
|
148
|
+
scanExecutor: JscpdCommandExecutor,
|
|
149
|
+
statusService: JscpdStatusService,
|
|
150
|
+
sessionMode: JscpdSessionModeService,
|
|
151
|
+
stateChanged: () => void = () => {},
|
|
152
|
+
changedExecutor: JscpdCommandExecutor = scanExecutor,
|
|
153
|
+
): JscpdCommandExecutor {
|
|
154
|
+
const executeEffect = (
|
|
155
|
+
invocation: Parameters<JscpdCommandExecutor["executeEffect"]>[0],
|
|
156
|
+
context: Parameters<JscpdCommandExecutor["executeEffect"]>[1],
|
|
157
|
+
) =>
|
|
158
|
+
statusAwareExecutionEffect(
|
|
159
|
+
invocation,
|
|
160
|
+
context,
|
|
161
|
+
scanExecutor,
|
|
162
|
+
changedExecutor,
|
|
163
|
+
statusService,
|
|
164
|
+
sessionMode,
|
|
165
|
+
stateChanged,
|
|
166
|
+
);
|
|
167
|
+
return {
|
|
168
|
+
executeEffect,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function createJscpdStatusService(
|
|
173
|
+
capabilityService: JscpdCapabilityService,
|
|
174
|
+
configService: JscpdConfigService,
|
|
175
|
+
sessionMode: JscpdSessionModeService,
|
|
176
|
+
fallowCoexistence?: JscpdFallowCoexistenceService,
|
|
177
|
+
): JscpdStatusService {
|
|
178
|
+
const owner = new StatusOwner(capabilityService, configService, sessionMode, fallowCoexistence);
|
|
179
|
+
return statusServiceFor(owner);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function createJscpdStatusWorkflowLayer(
|
|
183
|
+
capabilityService: JscpdCapabilityService,
|
|
184
|
+
configService: JscpdConfigService,
|
|
185
|
+
sessionMode: JscpdSessionModeService,
|
|
186
|
+
fallowCoexistence?: JscpdFallowCoexistenceService,
|
|
187
|
+
) {
|
|
188
|
+
const owner = new StatusOwner(capabilityService, configService, sessionMode, fallowCoexistence);
|
|
189
|
+
return Layer.succeed(JscpdStatusWorkflow, statusWorkflowFor(owner));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
interface StatusState {
|
|
193
|
+
readonly scope: number;
|
|
194
|
+
readonly lastCheck: JscpdLastCheck;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
class StatusOwner {
|
|
198
|
+
readonly #capability: JscpdCapabilityService;
|
|
199
|
+
readonly #config: JscpdConfigService;
|
|
200
|
+
readonly #mode: JscpdSessionModeService;
|
|
201
|
+
readonly #fallow: JscpdFallowCoexistenceService | undefined;
|
|
202
|
+
readonly #state = MutableRef.make<StatusState>({
|
|
203
|
+
scope: 0,
|
|
204
|
+
lastCheck: Object.freeze({ state: "never" }),
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
constructor(
|
|
208
|
+
capability: JscpdCapabilityService,
|
|
209
|
+
config: JscpdConfigService,
|
|
210
|
+
mode: JscpdSessionModeService,
|
|
211
|
+
fallow: JscpdFallowCoexistenceService | undefined,
|
|
212
|
+
) {
|
|
213
|
+
this.#capability = capability;
|
|
214
|
+
this.#config = config;
|
|
215
|
+
this.#mode = mode;
|
|
216
|
+
this.#fallow = fallow;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
inspectEffect(
|
|
220
|
+
context: JscpdExecutionContext,
|
|
221
|
+
): Effect.Effect<JscpdStatusResult, never, JscpdProcess> {
|
|
222
|
+
return capabilityProbeEffect(this.#capability, {
|
|
223
|
+
cwd: context.cwd,
|
|
224
|
+
signal: context.signal,
|
|
225
|
+
}).pipe(
|
|
226
|
+
Effect.map((capability) =>
|
|
227
|
+
presentStatus(
|
|
228
|
+
capability,
|
|
229
|
+
this.#config.current(),
|
|
230
|
+
this.#mode,
|
|
231
|
+
MutableRef.get(this.#state).lastCheck,
|
|
232
|
+
this.#fallow,
|
|
233
|
+
),
|
|
234
|
+
),
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
record(result: JscpdExecutionResult, expectedScope = this.scope()): void {
|
|
239
|
+
const current = MutableRef.get(this.#state);
|
|
240
|
+
if (current.scope !== expectedScope) return;
|
|
241
|
+
const recorded = lastCheckFromResult(result);
|
|
242
|
+
if (recorded) MutableRef.set(this.#state, { ...current, lastCheck: recorded });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
restore(restored?: JscpdLastCheck): void {
|
|
246
|
+
const current = MutableRef.get(this.#state);
|
|
247
|
+
MutableRef.set(this.#state, {
|
|
248
|
+
scope: current.scope + 1,
|
|
249
|
+
lastCheck: restored ?? Object.freeze({ state: "never" }),
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
lastCheck(): JscpdLastCheck {
|
|
254
|
+
return MutableRef.get(this.#state).lastCheck;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
scope(): number {
|
|
258
|
+
return MutableRef.get(this.#state).scope;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function statusServiceFor(owner: StatusOwner): JscpdStatusService {
|
|
263
|
+
return {
|
|
264
|
+
inspectEffect: (context) => owner.inspectEffect(context),
|
|
265
|
+
record: (result, expectedScope) => owner.record(result, expectedScope),
|
|
266
|
+
recordEffect: (result, expectedScope) => Effect.sync(() => owner.record(result, expectedScope)),
|
|
267
|
+
restore: (lastCheck) => owner.restore(lastCheck),
|
|
268
|
+
lastCheck: () => owner.lastCheck(),
|
|
269
|
+
scope: () => owner.scope(),
|
|
270
|
+
scopeEffect: Effect.sync(() => owner.scope()),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function statusWorkflowFor(owner: StatusOwner): JscpdStatusWorkflowService {
|
|
275
|
+
return {
|
|
276
|
+
inspect: (context) => owner.inspectEffect(context),
|
|
277
|
+
record: (result, expectedScope) => Effect.sync(() => owner.record(result, expectedScope)),
|
|
278
|
+
restore: (lastCheck) => Effect.sync(() => owner.restore(lastCheck)),
|
|
279
|
+
lastCheck: Effect.sync(() => owner.lastCheck()),
|
|
280
|
+
scope: Effect.sync(() => owner.scope()),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function statusAwareExecutionEffect(
|
|
285
|
+
invocation: Parameters<JscpdCommandExecutor["executeEffect"]>[0],
|
|
286
|
+
context: Parameters<JscpdCommandExecutor["executeEffect"]>[1],
|
|
287
|
+
scanExecutor: JscpdCommandExecutor,
|
|
288
|
+
changedExecutor: JscpdCommandExecutor,
|
|
289
|
+
statusService: JscpdStatusService,
|
|
290
|
+
sessionMode: JscpdSessionModeService,
|
|
291
|
+
stateChanged: () => void,
|
|
292
|
+
): Effect.Effect<JscpdExecutionResult, never, JscpdRuntimeRequirements> {
|
|
293
|
+
switch (invocation.command) {
|
|
294
|
+
case "status":
|
|
295
|
+
return statusInspectEffect(statusService, context);
|
|
296
|
+
case "off":
|
|
297
|
+
return Effect.sync(() => {
|
|
298
|
+
sessionMode.disable();
|
|
299
|
+
stateChanged();
|
|
300
|
+
return controlResult(
|
|
301
|
+
"disabled",
|
|
302
|
+
"jscpd behavior is disabled for this session. Run /jscpd on to re-enable it.",
|
|
303
|
+
);
|
|
304
|
+
});
|
|
305
|
+
case "on":
|
|
306
|
+
return Effect.sync(() => {
|
|
307
|
+
sessionMode.enable();
|
|
308
|
+
stateChanged();
|
|
309
|
+
return controlResult(
|
|
310
|
+
"enabled",
|
|
311
|
+
"jscpd behavior is enabled for this session. Project configuration was not changed.",
|
|
312
|
+
);
|
|
313
|
+
});
|
|
314
|
+
case "help":
|
|
315
|
+
return Effect.sync(helpResult);
|
|
316
|
+
case "scan":
|
|
317
|
+
return recordedExecutionEffect(
|
|
318
|
+
scanExecutor,
|
|
319
|
+
invocation,
|
|
320
|
+
context,
|
|
321
|
+
statusService,
|
|
322
|
+
stateChanged,
|
|
323
|
+
);
|
|
324
|
+
case "changed":
|
|
325
|
+
return recordedExecutionEffect(
|
|
326
|
+
changedExecutor,
|
|
327
|
+
invocation,
|
|
328
|
+
context,
|
|
329
|
+
statusService,
|
|
330
|
+
stateChanged,
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function statusInspectEffect(
|
|
336
|
+
status: JscpdStatusService,
|
|
337
|
+
context: JscpdExecutionContext,
|
|
338
|
+
): Effect.Effect<JscpdStatusResult, never, JscpdProcess> {
|
|
339
|
+
return status.inspectEffect(context);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function recordedExecutionEffect(
|
|
343
|
+
executor: JscpdCommandExecutor,
|
|
344
|
+
invocation: Parameters<JscpdCommandExecutor["executeEffect"]>[0],
|
|
345
|
+
context: Parameters<JscpdCommandExecutor["executeEffect"]>[1],
|
|
346
|
+
status: JscpdStatusService,
|
|
347
|
+
stateChanged: () => void,
|
|
348
|
+
): Effect.Effect<JscpdExecutionResult, never, JscpdRuntimeRequirements> {
|
|
349
|
+
return Effect.gen(function* () {
|
|
350
|
+
const expectedScope = yield* statusScopeEffect(status);
|
|
351
|
+
const result = yield* commandExecutionEffect(executor, invocation, context);
|
|
352
|
+
yield* status.recordEffect(result, expectedScope);
|
|
353
|
+
yield* Effect.sync(stateChanged);
|
|
354
|
+
return result;
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function commandExecutionEffect(
|
|
359
|
+
executor: JscpdCommandExecutor,
|
|
360
|
+
invocation: Parameters<JscpdCommandExecutor["executeEffect"]>[0],
|
|
361
|
+
context: Parameters<JscpdCommandExecutor["executeEffect"]>[1],
|
|
362
|
+
): Effect.Effect<JscpdExecutionResult, never, JscpdRuntimeRequirements> {
|
|
363
|
+
return executor.executeEffect(invocation, context);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function statusScopeEffect(service: JscpdStatusService): Effect.Effect<number | undefined> {
|
|
367
|
+
return service.scopeEffect;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function presentStatus(
|
|
371
|
+
capability: JscpdCapabilityResult,
|
|
372
|
+
loadedConfig: JscpdConfigLoadResult,
|
|
373
|
+
sessionMode: JscpdSessionModeService,
|
|
374
|
+
lastCheck: JscpdLastCheck,
|
|
375
|
+
fallowCoexistence?: JscpdFallowCoexistenceService,
|
|
376
|
+
): JscpdStatusResult {
|
|
377
|
+
const mode = sessionMode.isEnabled() ? "enabled" : "disabled";
|
|
378
|
+
const modeSource = sessionMode.source();
|
|
379
|
+
const configSource = effectiveConfigSource(loadedConfig.sources);
|
|
380
|
+
const lines = [
|
|
381
|
+
"jscpd status",
|
|
382
|
+
`Mode: ${mode}${modeSource === "session" ? " (session override)" : ""}`,
|
|
383
|
+
`Configuration: ${configSourceLabel(configSource)}`,
|
|
384
|
+
capabilityLine(capability),
|
|
385
|
+
stateLine(capability, mode, modeSource),
|
|
386
|
+
`Last check: ${lastCheckLabel(lastCheck)}`,
|
|
387
|
+
];
|
|
388
|
+
const overlap = fallowCoexistence?.current();
|
|
389
|
+
if (overlap) lines.push(overlap.statusText);
|
|
390
|
+
if (loadedConfig.diagnostics.length > 0) {
|
|
391
|
+
lines.push(
|
|
392
|
+
`Configuration diagnostics: ${loadedConfig.diagnostics.length} invalid source${loadedConfig.diagnostics.length === 1 ? "" : "s"} ignored.`,
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
const message = lines.join("\n");
|
|
396
|
+
return Object.freeze({
|
|
397
|
+
status: "status",
|
|
398
|
+
message,
|
|
399
|
+
terminalMessage: message,
|
|
400
|
+
mode,
|
|
401
|
+
modeSource,
|
|
402
|
+
configSource,
|
|
403
|
+
configSources: Object.freeze([...loadedConfig.sources]),
|
|
404
|
+
configDiagnostics: loadedConfig.diagnostics.length,
|
|
405
|
+
capability,
|
|
406
|
+
lastCheck,
|
|
407
|
+
...(overlap
|
|
408
|
+
? {
|
|
409
|
+
fallowOverlap: overlap.status,
|
|
410
|
+
fallowAutomatic: overlap.automaticAllowed ? ("allowed" as const) : ("on-demand" as const),
|
|
411
|
+
}
|
|
412
|
+
: {}),
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function effectiveConfigSource(sources: readonly JscpdConfigSource[]): JscpdConfigSource {
|
|
417
|
+
return sources.at(-1) ?? "defaults";
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function configSourceLabel(source: JscpdConfigSource): string {
|
|
421
|
+
switch (source) {
|
|
422
|
+
case "defaults":
|
|
423
|
+
return "built-in defaults";
|
|
424
|
+
case "project":
|
|
425
|
+
return `${CONFIG_DIR_NAME}/jscpd-guardrail.json`;
|
|
426
|
+
case "local":
|
|
427
|
+
return `${CONFIG_DIR_NAME}/jscpd-guardrail.local.json (local override)`;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function capabilityLine(capability: JscpdCapabilityResult): string {
|
|
432
|
+
switch (capability.status) {
|
|
433
|
+
case "available":
|
|
434
|
+
return `Binary: ${capability.executable} v${capability.version}${capability.source === "bundled" ? " (bundled)" : ""}`;
|
|
435
|
+
case "missing":
|
|
436
|
+
return "Binary: unavailable (checked project, PATH, and bundled jscpd)";
|
|
437
|
+
case "incompatible":
|
|
438
|
+
return `Binary: ${capability.executable} v${capability.version} (requires v5)`;
|
|
439
|
+
case "cancelled":
|
|
440
|
+
return `Binary: ${capability.executable} check cancelled`;
|
|
441
|
+
case "timed-out":
|
|
442
|
+
return `Binary: ${capability.executable} check timed out`;
|
|
443
|
+
case "failed":
|
|
444
|
+
return `Binary: ${capability.executable} check failed`;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function stateLine(
|
|
449
|
+
capability: JscpdCapabilityResult,
|
|
450
|
+
mode: JscpdStatusResult["mode"],
|
|
451
|
+
modeSource: JscpdStatusResult["modeSource"],
|
|
452
|
+
): string {
|
|
453
|
+
if (mode === "disabled") {
|
|
454
|
+
return modeSource === "session"
|
|
455
|
+
? "State: disabled for this session; run /jscpd on to re-enable it."
|
|
456
|
+
: "State: disabled by trusted extension configuration.";
|
|
457
|
+
}
|
|
458
|
+
if (capability.status === "missing") {
|
|
459
|
+
return "State: dormant — the bundled jscpd dependency is unavailable; reinstall pi-jscpd.";
|
|
460
|
+
}
|
|
461
|
+
if (capability.status === "available") return "State: ready for explicit scans.";
|
|
462
|
+
return "State: dormant until the binary check succeeds with jscpd v5.";
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function lastCheckFromResult(result: JscpdExecutionResult): JscpdLastCheck | undefined {
|
|
466
|
+
if (result.status === "completed") return completedLastCheck(result);
|
|
467
|
+
if (result.status === "failed") return failedLastCheck(result);
|
|
468
|
+
if (result.status === "unavailable") return unavailableLastCheck(result);
|
|
469
|
+
return result.status === "changed" ? changedLastCheck(result) : undefined;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function changedLastCheck(
|
|
473
|
+
result: Extract<JscpdExecutionResult, { status: "changed" }>,
|
|
474
|
+
): JscpdLastCheck | undefined {
|
|
475
|
+
if (!result.scanPerformed) return undefined;
|
|
476
|
+
return result.outcome === "clean"
|
|
477
|
+
? Object.freeze({ state: "clean" })
|
|
478
|
+
: Object.freeze({
|
|
479
|
+
state: "findings",
|
|
480
|
+
clones: result.findings.length + result.omittedFindings,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function completedLastCheck(
|
|
485
|
+
result: Extract<JscpdExecutionResult, { status: "completed" }>,
|
|
486
|
+
): JscpdLastCheck {
|
|
487
|
+
return result.outcome === "clean"
|
|
488
|
+
? Object.freeze({ state: "clean" })
|
|
489
|
+
: Object.freeze({ state: "findings", clones: result.summary.clones });
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function failedLastCheck(
|
|
493
|
+
result: Extract<JscpdExecutionResult, { status: "failed" }>,
|
|
494
|
+
): JscpdLastCheck {
|
|
495
|
+
return result.reason === "scan-cancelled"
|
|
496
|
+
? Object.freeze({ state: "cancelled" })
|
|
497
|
+
: Object.freeze({ state: "failed", reason: result.reason });
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function unavailableLastCheck(
|
|
501
|
+
result: Extract<JscpdExecutionResult, { status: "unavailable" }>,
|
|
502
|
+
): JscpdLastCheck {
|
|
503
|
+
return result.reason === "probe-cancelled"
|
|
504
|
+
? Object.freeze({ state: "cancelled" })
|
|
505
|
+
: Object.freeze({ state: "failed", reason: result.reason });
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function lastCheckLabel(lastCheck: JscpdLastCheck): string {
|
|
509
|
+
switch (lastCheck.state) {
|
|
510
|
+
case "never":
|
|
511
|
+
return "never run";
|
|
512
|
+
case "clean":
|
|
513
|
+
return "clean";
|
|
514
|
+
case "findings":
|
|
515
|
+
return `${lastCheck.clones} duplicate block${lastCheck.clones === 1 ? "" : "s"} found`;
|
|
516
|
+
case "cancelled":
|
|
517
|
+
return "cancelled";
|
|
518
|
+
case "failed":
|
|
519
|
+
return `failed (${failureLabel(lastCheck.reason)})`;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function controlResult(action: "enabled" | "disabled", message: string): JscpdExecutionResult {
|
|
524
|
+
return Object.freeze({ status: "control", action, message, terminalMessage: message });
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function helpResult(): JscpdExecutionResult {
|
|
528
|
+
const message = renderJscpdCommandHelp();
|
|
529
|
+
return Object.freeze({ status: "help", message, terminalMessage: message });
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function failureLabel(reason: Extract<JscpdLastCheck, { state: "failed" }>["reason"]): string {
|
|
533
|
+
return reason.replaceAll("-", " ");
|
|
534
|
+
}
|