@shrkcrft/cli 0.1.0-alpha.23 → 0.1.0-alpha.24
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/dist/commands/check.command.d.ts.map +1 -1
- package/dist/commands/check.command.js +109 -2
- package/dist/commands/command-catalog.d.ts +11 -0
- package/dist/commands/command-catalog.d.ts.map +1 -1
- package/dist/commands/command-catalog.js +91 -0
- package/dist/commands/finish.command.d.ts +3 -0
- package/dist/commands/finish.command.d.ts.map +1 -0
- package/dist/commands/finish.command.js +70 -0
- package/dist/commands/graph-code-subverbs.d.ts.map +1 -1
- package/dist/commands/graph-code-subverbs.js +42 -23
- package/dist/commands/help.command.d.ts.map +1 -1
- package/dist/commands/help.command.js +20 -2
- package/dist/commands/impact.command.d.ts.map +1 -1
- package/dist/commands/impact.command.js +17 -22
- package/dist/commands/smart-context.command.d.ts.map +1 -1
- package/dist/commands/smart-context.command.js +14 -37
- package/dist/commands/trace.command.d.ts.map +1 -1
- package/dist/commands/trace.command.js +73 -3
- package/dist/commands/wiring.command.d.ts +12 -0
- package/dist/commands/wiring.command.d.ts.map +1 -0
- package/dist/commands/wiring.command.js +384 -0
- package/dist/diff/collect-changed-paths.d.ts +5 -3
- package/dist/diff/collect-changed-paths.d.ts.map +1 -1
- package/dist/diff/collect-changed-paths.js +73 -36
- package/dist/diff/deleted-orphans.d.ts +42 -0
- package/dist/diff/deleted-orphans.d.ts.map +1 -0
- package/dist/diff/deleted-orphans.js +46 -0
- package/dist/finish/run-finish.d.ts +62 -0
- package/dist/finish/run-finish.d.ts.map +1 -0
- package/dist/finish/run-finish.js +239 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +4 -0
- package/package.json +33 -33
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CLI-side `--since <ref>` helper. Thin wrapper over `git diff
|
|
3
|
-
* --name-status <ref
|
|
4
|
-
* deleted file set + an `isAvailable` flag so callers can fall back
|
|
2
|
+
* CLI-side `--since <ref>` helper. Thin wrapper over a two-dot `git diff
|
|
3
|
+
* --name-status <ref>` (working tree vs `<ref>`) that returns the changed file
|
|
4
|
+
* set + a deleted file set + an `isAvailable` flag so callers can fall back
|
|
5
5
|
* to whole-tree mode cleanly.
|
|
6
6
|
*
|
|
7
7
|
* Default ref resolution when `--since` is passed without a value:
|
|
@@ -23,5 +23,7 @@ export declare function resolveDefaultSinceRef(cwd: string): string | undefined;
|
|
|
23
23
|
export declare function collectChangedPaths(opts: {
|
|
24
24
|
cwd: string;
|
|
25
25
|
ref?: string;
|
|
26
|
+
/** Read the staged (index) diff instead of diffing against a ref. */
|
|
27
|
+
staged?: boolean;
|
|
26
28
|
}): IChangedPaths;
|
|
27
29
|
//# sourceMappingURL=collect-changed-paths.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collect-changed-paths.d.ts","sourceRoot":"","sources":["../../src/diff/collect-changed-paths.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,WAAW,EAAE,OAAO,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAMD,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,
|
|
1
|
+
{"version":3,"file":"collect-changed-paths.d.ts","sourceRoot":"","sources":["../../src/diff/collect-changed-paths.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,WAAW,EAAE,OAAO,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAMD,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAOtE;AAoCD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE;IACxC,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,qEAAqE;IACrE,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,GAAG,aAAa,CAwDhB"}
|
|
@@ -1,22 +1,72 @@
|
|
|
1
|
-
import { execSync } from 'node:child_process';
|
|
2
1
|
import * as nodePath from 'node:path';
|
|
2
|
+
import { runGitLines } from '@shrkcrft/shared';
|
|
3
3
|
const DEFAULT_REF_CANDIDATES = ['origin/main', 'main', 'origin/master', 'master'];
|
|
4
4
|
const PER_PROCESS_CACHE = new Map();
|
|
5
5
|
export function resolveDefaultSinceRef(cwd) {
|
|
6
6
|
for (const candidate of DEFAULT_REF_CANDIDATES) {
|
|
7
|
-
|
|
8
|
-
execSync(`git -C "${cwd}" rev-parse --verify --quiet "${candidate}"`, {
|
|
9
|
-
stdio: 'pipe',
|
|
10
|
-
});
|
|
7
|
+
if (runGitLines(cwd, ['rev-parse', '--verify', '--quiet', candidate]).ok) {
|
|
11
8
|
return candidate;
|
|
12
9
|
}
|
|
13
|
-
catch {
|
|
14
|
-
// try next
|
|
15
|
-
}
|
|
16
10
|
}
|
|
17
11
|
return undefined;
|
|
18
12
|
}
|
|
13
|
+
function toPosix(path) {
|
|
14
|
+
return nodePath.normalize(path).split(nodePath.sep).join('/');
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Split `git diff --name-status` lines into changed vs deleted POSIX paths.
|
|
18
|
+
* Rename/copy detection is ON by default for `git diff`, so a rename emits a
|
|
19
|
+
* TAB-separated `R<score>\t<old>\t<new>` (copy: `C<score>\t<old>\t<new>`) — the
|
|
20
|
+
* status field is `R100`, not a bare letter. The old path of a RENAME is gone
|
|
21
|
+
* (so the orphan check must treat it as deleted); a COPY keeps its source. Every
|
|
22
|
+
* other status (A/M/T/…) carries a single path. Parsing the status by its first
|
|
23
|
+
* letter + splitting on TAB is what makes a `git mv` that orphans an importer
|
|
24
|
+
* visible to `check orphans` / `finish` instead of silently dropped.
|
|
25
|
+
*/
|
|
26
|
+
function parseNameStatus(lines) {
|
|
27
|
+
const changed = [];
|
|
28
|
+
const deleted = [];
|
|
29
|
+
for (const line of lines) {
|
|
30
|
+
const parts = line.split('\t');
|
|
31
|
+
if (parts.length < 2)
|
|
32
|
+
continue;
|
|
33
|
+
const code = parts[0][0];
|
|
34
|
+
if (code === 'D') {
|
|
35
|
+
deleted.push(toPosix(parts[1]));
|
|
36
|
+
}
|
|
37
|
+
else if ((code === 'R' || code === 'C') && parts.length >= 3) {
|
|
38
|
+
// R<score>\t<old>\t<new> (rename) / C<score>\t<old>\t<new> (copy).
|
|
39
|
+
if (code === 'R')
|
|
40
|
+
deleted.push(toPosix(parts[1])); // old path renamed away
|
|
41
|
+
changed.push(toPosix(parts[2])); // new path is added
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
changed.push(toPosix(parts[1]));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return { changed, deleted };
|
|
48
|
+
}
|
|
19
49
|
export function collectChangedPaths(opts) {
|
|
50
|
+
if (opts.staged) {
|
|
51
|
+
const cacheKey = `${opts.cwd}::staged`;
|
|
52
|
+
const cached = PER_PROCESS_CACHE.get(cacheKey);
|
|
53
|
+
if (cached)
|
|
54
|
+
return cached;
|
|
55
|
+
const out = runGitLines(opts.cwd, ['diff', '--name-status', '--cached']);
|
|
56
|
+
if (!out.ok) {
|
|
57
|
+
return {
|
|
58
|
+
ref: 'STAGED',
|
|
59
|
+
changed: [],
|
|
60
|
+
deleted: [],
|
|
61
|
+
isAvailable: false,
|
|
62
|
+
...(out.error ? { error: out.error } : {}),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const { changed, deleted } = parseNameStatus(out.lines);
|
|
66
|
+
const result = { ref: 'STAGED', changed, deleted, isAvailable: true };
|
|
67
|
+
PER_PROCESS_CACHE.set(cacheKey, result);
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
20
70
|
const ref = opts.ref ?? resolveDefaultSinceRef(opts.cwd);
|
|
21
71
|
if (!ref) {
|
|
22
72
|
return {
|
|
@@ -27,42 +77,29 @@ export function collectChangedPaths(opts) {
|
|
|
27
77
|
error: 'no resolvable default branch (tried origin/main, main, origin/master, master)',
|
|
28
78
|
};
|
|
29
79
|
}
|
|
30
|
-
|
|
80
|
+
// Namespace the ref cache key so a branch/tag literally named `staged` can't
|
|
81
|
+
// collide with the `::staged` index-diff key above.
|
|
82
|
+
const cacheKey = `${opts.cwd}::ref::${ref}`;
|
|
31
83
|
const cached = PER_PROCESS_CACHE.get(cacheKey);
|
|
32
84
|
if (cached)
|
|
33
85
|
return cached;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
for (const line of stdout.split('\n')) {
|
|
42
|
-
const trimmed = line.trim();
|
|
43
|
-
if (!trimmed)
|
|
44
|
-
continue;
|
|
45
|
-
const m = trimmed.match(/^(\w)\s+(.+)$/);
|
|
46
|
-
if (!m)
|
|
47
|
-
continue;
|
|
48
|
-
const [, status, path] = m;
|
|
49
|
-
const norm = nodePath.normalize(path).split(nodePath.sep).join('/');
|
|
50
|
-
if (status === 'D')
|
|
51
|
-
deleted.push(norm);
|
|
52
|
-
else
|
|
53
|
-
changed.push(norm);
|
|
54
|
-
}
|
|
55
|
-
const result = { ref, changed, deleted, isAvailable: true };
|
|
56
|
-
PER_PROCESS_CACHE.set(cacheKey, result);
|
|
57
|
-
return result;
|
|
58
|
-
}
|
|
59
|
-
catch (e) {
|
|
86
|
+
// Two-dot diff against the ref to match the inspector's
|
|
87
|
+
// `getChangedFiles({ since })` behavior — includes both committed
|
|
88
|
+
// changes and the working tree relative to <ref>. `runGitLines` is
|
|
89
|
+
// shell-free + high-maxBuffer, so a large changeset no longer ENOBUFS-crashes
|
|
90
|
+
// the `impact --deleted` orphan check (this is its only write-safety guard).
|
|
91
|
+
const out = runGitLines(opts.cwd, ['diff', '--name-status', ref]);
|
|
92
|
+
if (!out.ok) {
|
|
60
93
|
return {
|
|
61
94
|
ref,
|
|
62
95
|
changed: [],
|
|
63
96
|
deleted: [],
|
|
64
97
|
isAvailable: false,
|
|
65
|
-
error
|
|
98
|
+
...(out.error ? { error: out.error } : {}),
|
|
66
99
|
};
|
|
67
100
|
}
|
|
101
|
+
const { changed, deleted } = parseNameStatus(out.lines);
|
|
102
|
+
const result = { ref, changed, deleted, isAvailable: true };
|
|
103
|
+
PER_PROCESS_CACHE.set(cacheKey, result);
|
|
104
|
+
return result;
|
|
68
105
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { IDeletedOrphanReport } from '@shrkcrft/impact-engine';
|
|
2
|
+
/**
|
|
3
|
+
* Why the scan could not produce an orphan report (so callers pick an exit
|
|
4
|
+
* code / message): the diff was unavailable (no resolvable ref, git error) or
|
|
5
|
+
* the code-graph store is missing.
|
|
6
|
+
*/
|
|
7
|
+
export type DeletedOrphanScanFailure = 'diff-unavailable' | 'graph-missing';
|
|
8
|
+
/** Result of {@link computeDeletedOrphans}. */
|
|
9
|
+
export interface IDeletedOrphanScan {
|
|
10
|
+
/** True when the diff resolved (and, when anything was deleted, the graph too). */
|
|
11
|
+
readonly ok: boolean;
|
|
12
|
+
/** What was diffed: `'STAGED'` for `--staged`, otherwise the resolved ref. */
|
|
13
|
+
readonly ref: string;
|
|
14
|
+
/** Deleted files read from the diff (empty list is a valid, clean result). */
|
|
15
|
+
readonly deleted: readonly string[];
|
|
16
|
+
/**
|
|
17
|
+
* The alias-resolved orphan report. Present iff `ok` AND something was
|
|
18
|
+
* deleted — when `deleted` is empty the scan short-circuits clean without
|
|
19
|
+
* loading the graph, so there is no report (and none is needed).
|
|
20
|
+
*/
|
|
21
|
+
readonly report?: IDeletedOrphanReport;
|
|
22
|
+
/** Why the scan failed. Present iff not `ok`. */
|
|
23
|
+
readonly reason?: DeletedOrphanScanFailure;
|
|
24
|
+
/** Human-readable failure detail. */
|
|
25
|
+
readonly error?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Diff-driven reverse-closure: read the files DELETED in the changeset (vs
|
|
29
|
+
* `since`, or the staged index when `staged`), then query the code-graph
|
|
30
|
+
* snapshot for surviving files that still import them or reference a symbol
|
|
31
|
+
* they declared (alias-resolved, incl. barrel re-exports). The shared core
|
|
32
|
+
* behind `impact --deleted`, `check orphans`, and the composite `finish` gate.
|
|
33
|
+
*
|
|
34
|
+
* Never throws: a bad diff → `{ reason: 'diff-unavailable' }`, a missing index
|
|
35
|
+
* → `{ reason: 'graph-missing' }`. Graph + engine are imported lazily so a
|
|
36
|
+
* command that never hits the orphan path doesn't pay to load them.
|
|
37
|
+
*/
|
|
38
|
+
export declare function computeDeletedOrphans(cwd: string, opts: {
|
|
39
|
+
since?: string;
|
|
40
|
+
staged?: boolean;
|
|
41
|
+
}): Promise<IDeletedOrphanScan>;
|
|
42
|
+
//# sourceMappingURL=deleted-orphans.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deleted-orphans.d.ts","sourceRoot":"","sources":["../../src/diff/deleted-orphans.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAGpE;;;;GAIG;AACH,MAAM,MAAM,wBAAwB,GAAG,kBAAkB,GAAG,eAAe,CAAC;AAE5E,+CAA+C;AAC/C,MAAM,WAAW,kBAAkB;IACjC,mFAAmF;IACnF,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,8EAA8E;IAC9E,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,8EAA8E;IAC9E,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,oBAAoB,CAAC;IACvC,iDAAiD;IACjD,QAAQ,CAAC,MAAM,CAAC,EAAE,wBAAwB,CAAC;IAC3C,qCAAqC;IACrC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,qBAAqB,CACzC,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,GACzC,OAAO,CAAC,kBAAkB,CAAC,CAmC7B"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { collectChangedPaths } from "./collect-changed-paths.js";
|
|
2
|
+
/**
|
|
3
|
+
* Diff-driven reverse-closure: read the files DELETED in the changeset (vs
|
|
4
|
+
* `since`, or the staged index when `staged`), then query the code-graph
|
|
5
|
+
* snapshot for surviving files that still import them or reference a symbol
|
|
6
|
+
* they declared (alias-resolved, incl. barrel re-exports). The shared core
|
|
7
|
+
* behind `impact --deleted`, `check orphans`, and the composite `finish` gate.
|
|
8
|
+
*
|
|
9
|
+
* Never throws: a bad diff → `{ reason: 'diff-unavailable' }`, a missing index
|
|
10
|
+
* → `{ reason: 'graph-missing' }`. Graph + engine are imported lazily so a
|
|
11
|
+
* command that never hits the orphan path doesn't pay to load them.
|
|
12
|
+
*/
|
|
13
|
+
export async function computeDeletedOrphans(cwd, opts) {
|
|
14
|
+
const changed = collectChangedPaths({
|
|
15
|
+
cwd,
|
|
16
|
+
...(opts.since ? { ref: opts.since } : {}),
|
|
17
|
+
...(opts.staged ? { staged: true } : {}),
|
|
18
|
+
});
|
|
19
|
+
if (!changed.isAvailable) {
|
|
20
|
+
return {
|
|
21
|
+
ok: false,
|
|
22
|
+
ref: changed.ref,
|
|
23
|
+
deleted: [],
|
|
24
|
+
reason: 'diff-unavailable',
|
|
25
|
+
...(changed.error ? { error: changed.error } : {}),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
// Nothing deleted → clean by definition; don't pay to load the graph (and
|
|
29
|
+
// don't fail when no index exists — there is nothing to check).
|
|
30
|
+
if (changed.deleted.length === 0) {
|
|
31
|
+
return { ok: true, ref: changed.ref, deleted: [] };
|
|
32
|
+
}
|
|
33
|
+
const { GraphStore, GraphQueryApi } = await import('@shrkcrft/graph');
|
|
34
|
+
if (!new GraphStore(cwd).exists()) {
|
|
35
|
+
return {
|
|
36
|
+
ok: false,
|
|
37
|
+
ref: changed.ref,
|
|
38
|
+
deleted: changed.deleted,
|
|
39
|
+
reason: 'graph-missing',
|
|
40
|
+
error: 'code-graph store missing — run `shrk graph index` first.',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const { findDeletedOrphans } = await import('@shrkcrft/impact-engine');
|
|
44
|
+
const report = findDeletedOrphans(GraphQueryApi.fromStore(cwd), changed.deleted);
|
|
45
|
+
return { ok: true, ref: changed.ref, deleted: changed.deleted, report };
|
|
46
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type IChangedScopeOptions } from '@shrkcrft/inspector';
|
|
2
|
+
export declare const FINISH_SCHEMA: "sharkcraft.finish/v1";
|
|
3
|
+
/** Outcome of one sub-gate. `skipped` = nothing to evaluate (loud, never silent green). */
|
|
4
|
+
export type FinishGateStatus = 'pass' | 'fail' | 'skipped';
|
|
5
|
+
/** One failing/relevant item, with file:line where the engine provides it. */
|
|
6
|
+
export interface IFinishItem {
|
|
7
|
+
readonly file?: string;
|
|
8
|
+
readonly line?: number;
|
|
9
|
+
readonly message: string;
|
|
10
|
+
}
|
|
11
|
+
export interface IFinishGate {
|
|
12
|
+
readonly name: 'boundaries' | 'imports' | 'wiring' | 'policy' | 'orphans';
|
|
13
|
+
readonly status: FinishGateStatus;
|
|
14
|
+
/** One-line reason (e.g. why skipped, or the error/warning counts). */
|
|
15
|
+
readonly detail: string;
|
|
16
|
+
readonly errors: number;
|
|
17
|
+
readonly warnings: number;
|
|
18
|
+
/** Failing/notable items (capped by the renderer, full in JSON). */
|
|
19
|
+
readonly items: readonly IFinishItem[];
|
|
20
|
+
}
|
|
21
|
+
export interface IFinishImpact {
|
|
22
|
+
readonly ran: boolean;
|
|
23
|
+
readonly risk?: string;
|
|
24
|
+
readonly directDependents?: number;
|
|
25
|
+
readonly transitiveDependents?: number;
|
|
26
|
+
/** Why the summary could not run (no graph index / no changed files). */
|
|
27
|
+
readonly note?: string;
|
|
28
|
+
}
|
|
29
|
+
export interface IFinishReport {
|
|
30
|
+
readonly schema: typeof FINISH_SCHEMA;
|
|
31
|
+
readonly scope: {
|
|
32
|
+
readonly mode: 'worktree' | 'staged' | 'since' | 'files';
|
|
33
|
+
readonly files: readonly string[];
|
|
34
|
+
readonly fileCount: number;
|
|
35
|
+
};
|
|
36
|
+
readonly gates: readonly IFinishGate[];
|
|
37
|
+
readonly impact: IFinishImpact;
|
|
38
|
+
/** `fail` iff any gate failed OR the config could not load; else `pass`. */
|
|
39
|
+
readonly verdict: 'pass' | 'fail';
|
|
40
|
+
/** Total warning-severity findings across gates (non-blocking). */
|
|
41
|
+
readonly warnings: number;
|
|
42
|
+
/** Set when sharkcraft.config.ts could not be loaded — forces a `fail`. */
|
|
43
|
+
readonly configError?: string;
|
|
44
|
+
readonly summary: string;
|
|
45
|
+
readonly nextAction: string;
|
|
46
|
+
}
|
|
47
|
+
export interface IRunFinishInput {
|
|
48
|
+
readonly cwd: string;
|
|
49
|
+
readonly mode: 'worktree' | 'staged' | 'since' | 'files';
|
|
50
|
+
readonly scope: IChangedScopeOptions;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The composite "is this changeset safe to finish?" orchestrator. Runs every
|
|
54
|
+
* deterministic CHANGED-ONLY gate inline — boundaries + import-hygiene + wiring
|
|
55
|
+
* + policy + deleted-orphans — plus a best-effort impact summary, and folds
|
|
56
|
+
* them into ONE pass/fail. This is the single trustworthy "done?" call an
|
|
57
|
+
* autonomous agent needs and can't reliably assemble by hand (only shrk can run
|
|
58
|
+
* the alias-resolved layer/wiring gates). Honors the `0-rules → skipped`
|
|
59
|
+
* semantics so a no-op sub-check is reported, never silently passed. Read-only.
|
|
60
|
+
*/
|
|
61
|
+
export declare function runFinishGates(input: IRunFinishInput): Promise<IFinishReport>;
|
|
62
|
+
//# sourceMappingURL=run-finish.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-finish.d.ts","sourceRoot":"","sources":["../../src/finish/run-finish.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,oBAAoB,EAC1B,MAAM,qBAAqB,CAAC;AAa7B,eAAO,MAAM,aAAa,EAAG,sBAA+B,CAAC;AAE7D,2FAA2F;AAC3F,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;AAE3D,8EAA8E;AAC9E,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,YAAY,GAAG,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAC;IAC1E,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAClC,uEAAuE;IACvE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,oEAAoE;IACpE,QAAQ,CAAC,KAAK,EAAE,SAAS,WAAW,EAAE,CAAC;CACxC;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,CAAC;IACvC,yEAAyE;IACzE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,OAAO,aAAa,CAAC;IACtC,QAAQ,CAAC,KAAK,EAAE;QACd,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG,QAAQ,GAAG,OAAO,GAAG,OAAO,CAAC;QACzD,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;QAClC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;KAC5B,CAAC;IACF,QAAQ,CAAC,KAAK,EAAE,SAAS,WAAW,EAAE,CAAC;IACvC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,4EAA4E;IAC5E,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;IAClC,mEAAmE;IACnE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,2EAA2E;IAC3E,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG,QAAQ,GAAG,OAAO,GAAG,OAAO,CAAC;IACzD,QAAQ,CAAC,KAAK,EAAE,oBAAoB,CAAC;CACtC;AAYD;;;;;;;;GAQG;AACH,wBAAsB,cAAc,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,aAAa,CAAC,CAkHnF"}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { buildImportHygieneReport, filterViolationsToChangedScope, inspectSharkcraft, resolveChangedFiles, resolveProjectConfig, } from '@shrkcrft/inspector';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import * as nodePath from 'node:path';
|
|
4
|
+
import { evaluateBoundaries, loadTsconfigPaths, runPolicyLint, runWiring, scanImports, } from '@shrkcrft/boundaries';
|
|
5
|
+
import { computeDeletedOrphans } from "../diff/deleted-orphans.js";
|
|
6
|
+
export const FINISH_SCHEMA = 'sharkcraft.finish/v1';
|
|
7
|
+
/** Map the changed-scope onto the orphan check's diff inputs. */
|
|
8
|
+
function orphanOptsFor(input) {
|
|
9
|
+
if (input.mode === 'files')
|
|
10
|
+
return undefined; // a file list has no diff to read deletions from
|
|
11
|
+
if (input.mode === 'staged')
|
|
12
|
+
return { staged: true };
|
|
13
|
+
if (input.mode === 'since' && input.scope.since)
|
|
14
|
+
return { since: input.scope.since };
|
|
15
|
+
// Worktree mode: diff deletions vs HEAD so the orphan gate's "deleted" set
|
|
16
|
+
// matches the working-tree scope the other gates use (not the whole branch).
|
|
17
|
+
return { since: 'HEAD' };
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The composite "is this changeset safe to finish?" orchestrator. Runs every
|
|
21
|
+
* deterministic CHANGED-ONLY gate inline — boundaries + import-hygiene + wiring
|
|
22
|
+
* + policy + deleted-orphans — plus a best-effort impact summary, and folds
|
|
23
|
+
* them into ONE pass/fail. This is the single trustworthy "done?" call an
|
|
24
|
+
* autonomous agent needs and can't reliably assemble by hand (only shrk can run
|
|
25
|
+
* the alias-resolved layer/wiring gates). Honors the `0-rules → skipped`
|
|
26
|
+
* semantics so a no-op sub-check is reported, never silently passed. Read-only.
|
|
27
|
+
*/
|
|
28
|
+
export async function runFinishGates(input) {
|
|
29
|
+
const { cwd } = input;
|
|
30
|
+
const changed = resolveChangedFiles(input.scope);
|
|
31
|
+
const changedFiles = changed.files;
|
|
32
|
+
const gates = [];
|
|
33
|
+
// ── boundaries (changed-only) ────────────────────────────────────────
|
|
34
|
+
const inspection = await inspectSharkcraft({ cwd });
|
|
35
|
+
const boundaryRules = inspection.boundaryRegistry.list();
|
|
36
|
+
if (boundaryRules.length === 0) {
|
|
37
|
+
gates.push(skip('boundaries', 'no boundary rules configured'));
|
|
38
|
+
}
|
|
39
|
+
else if (changedFiles.length === 0) {
|
|
40
|
+
gates.push(skip('boundaries', 'no files in changed scope'));
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
const scan = scanImports({ projectRoot: cwd });
|
|
44
|
+
const tsconfigPaths = loadTsconfigPaths(cwd);
|
|
45
|
+
const evalResult = evaluateBoundaries(scan, boundaryRules, {
|
|
46
|
+
...(tsconfigPaths.aliases.size > 0 ? { tsconfigPaths } : {}),
|
|
47
|
+
});
|
|
48
|
+
const filtered = filterViolationsToChangedScope(evalResult.violations, input.scope);
|
|
49
|
+
const errors = filtered.includedViolations.filter((v) => v.severity === 'error');
|
|
50
|
+
const warnings = filtered.includedViolations.filter((v) => v.severity === 'warning');
|
|
51
|
+
gates.push({
|
|
52
|
+
name: 'boundaries',
|
|
53
|
+
status: errors.length > 0 ? 'fail' : 'pass',
|
|
54
|
+
detail: `${errors.length} error(s), ${warnings.length} warning(s) across ${changedFiles.length} changed file(s)`,
|
|
55
|
+
errors: errors.length,
|
|
56
|
+
warnings: warnings.length,
|
|
57
|
+
items: [...errors, ...warnings].map((v) => ({
|
|
58
|
+
file: v.file,
|
|
59
|
+
line: v.line,
|
|
60
|
+
message: `[${v.severity}] ${v.ruleId}: ${v.message}`,
|
|
61
|
+
})),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
// ── import hygiene (changed-only) ────────────────────────────────────
|
|
65
|
+
if (changedFiles.length === 0) {
|
|
66
|
+
gates.push(skip('imports', 'no files in changed scope'));
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
const report = buildImportHygieneReport(cwd, { files: changedFiles });
|
|
70
|
+
const errors = report.counts?.['error'] ?? (report.verdict === 'errors' ? report.findings.length : 0);
|
|
71
|
+
const warnings = report.counts?.['warning'] ?? (report.verdict === 'warnings' ? report.findings.length : 0);
|
|
72
|
+
gates.push({
|
|
73
|
+
name: 'imports',
|
|
74
|
+
status: report.verdict === 'errors' ? 'fail' : 'pass',
|
|
75
|
+
detail: `verdict=${report.verdict} (${report.findings.length} finding(s))`,
|
|
76
|
+
errors,
|
|
77
|
+
warnings,
|
|
78
|
+
items: report.findings.map((f) => ({
|
|
79
|
+
file: f.file,
|
|
80
|
+
line: f.line,
|
|
81
|
+
message: `${f.kind}: ${f.suggestedFix || f.reason || f.snippet}`.trim(),
|
|
82
|
+
})),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
// ── wiring + policy (changed-only, from resolved config) ─────────────
|
|
86
|
+
const loaded = await resolveProjectConfig(cwd);
|
|
87
|
+
let configError;
|
|
88
|
+
if (!loaded.ok) {
|
|
89
|
+
// A MALFORMED config in a real sharkcraft project is a fail (the wiring/
|
|
90
|
+
// policy gates can't be trusted). The mere ABSENCE of a sharkcraft/ folder
|
|
91
|
+
// is not — those gates simply don't apply, so skip them without failing.
|
|
92
|
+
const isSharkcraftProject = existsSync(nodePath.join(cwd, 'sharkcraft'));
|
|
93
|
+
if (isSharkcraftProject)
|
|
94
|
+
configError = loaded.error.message;
|
|
95
|
+
const detail = isSharkcraftProject
|
|
96
|
+
? `config did not load: ${loaded.error.message}`
|
|
97
|
+
: 'no sharkcraft config (gate not applicable)';
|
|
98
|
+
gates.push(skip('wiring', detail));
|
|
99
|
+
gates.push(skip('policy', detail));
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
gates.push(wiringGate(cwd, loaded.value.config.wiringRules ?? [], changedFiles), policyGate(cwd, loaded.value.config.policyRules ?? [], changedFiles));
|
|
103
|
+
}
|
|
104
|
+
// ── deleted-orphans (write-safety) ───────────────────────────────────
|
|
105
|
+
gates.push(await orphansGate(input));
|
|
106
|
+
// ── impact summary (informational, best-effort) ──────────────────────
|
|
107
|
+
const impact = await impactSummary(cwd, changedFiles);
|
|
108
|
+
// ── fold into one verdict ────────────────────────────────────────────
|
|
109
|
+
const failed = gates.filter((g) => g.status === 'fail');
|
|
110
|
+
const warnings = gates.reduce((n, g) => n + g.warnings, 0);
|
|
111
|
+
const verdict = failed.length > 0 || configError ? 'fail' : 'pass';
|
|
112
|
+
const skipped = gates.filter((g) => g.status === 'skipped').map((g) => g.name);
|
|
113
|
+
const summary = verdict === 'fail'
|
|
114
|
+
? `Not safe to finish: ${configError ? 'config failed to load; ' : ''}${failed.map((g) => `${g.name} (${g.errors} error(s))`).join(', ') || 'see gates'}.`
|
|
115
|
+
: changedFiles.length === 0 && input.mode !== 'files'
|
|
116
|
+
? 'No files in the changed scope — nothing to gate.'
|
|
117
|
+
: `Safe to finish: every applicable gate passed${warnings > 0 ? ` (${warnings} non-blocking warning(s))` : ''}${skipped.length > 0 ? `; skipped: ${skipped.join(', ')}` : ''}.`;
|
|
118
|
+
const nextAction = verdict === 'fail'
|
|
119
|
+
? 'Fix every failing gate item (each carries file:line), then re-run `shrk finish`.'
|
|
120
|
+
: 'Safe to declare done.';
|
|
121
|
+
return {
|
|
122
|
+
schema: FINISH_SCHEMA,
|
|
123
|
+
scope: { mode: input.mode, files: changedFiles, fileCount: changedFiles.length },
|
|
124
|
+
gates,
|
|
125
|
+
impact,
|
|
126
|
+
verdict,
|
|
127
|
+
warnings,
|
|
128
|
+
...(configError ? { configError } : {}),
|
|
129
|
+
summary,
|
|
130
|
+
nextAction,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function skip(name, detail) {
|
|
134
|
+
return { name, status: 'skipped', detail, errors: 0, warnings: 0, items: [] };
|
|
135
|
+
}
|
|
136
|
+
function wiringGate(cwd, rules, changedFiles) {
|
|
137
|
+
if (rules.length === 0)
|
|
138
|
+
return skip('wiring', 'no wiring rules configured');
|
|
139
|
+
const report = runWiring(cwd, rules, { changedOnly: true, changedFiles });
|
|
140
|
+
if (report.evaluated === 0) {
|
|
141
|
+
return skip('wiring', `${report.rules.length} rule(s) configured but none matched the changed scope`);
|
|
142
|
+
}
|
|
143
|
+
const errors = report.violations.filter((v) => v.severity === 'error');
|
|
144
|
+
const warnings = report.violations.filter((v) => v.severity === 'warning');
|
|
145
|
+
// A misconfigured rule (bad regex / no capture group) yields a diagnostic +
|
|
146
|
+
// rule-level error but NO violation — it must FAIL the gate, never read as a
|
|
147
|
+
// silent green ("loud, never silent green").
|
|
148
|
+
const diag = report.diagnostics;
|
|
149
|
+
return {
|
|
150
|
+
name: 'wiring',
|
|
151
|
+
status: report.verdict === 'errors' || diag.length > 0 ? 'fail' : 'pass',
|
|
152
|
+
detail: `${report.evaluated}/${report.rules.length} rule(s) evaluated — ${errors.length} error(s), ${warnings.length} warning(s)${diag.length > 0 ? `, ${diag.length} misconfigured` : ''}`,
|
|
153
|
+
errors: errors.length + diag.length,
|
|
154
|
+
warnings: warnings.length,
|
|
155
|
+
items: [
|
|
156
|
+
...report.violations.map((v) => ({
|
|
157
|
+
file: v.file,
|
|
158
|
+
line: v.line,
|
|
159
|
+
message: `[${v.severity}] ${v.ruleId}: "${v.token}" ${v.direction === 'registered-missing' ? 'registered but not declared' : 'declared but not registered'}`,
|
|
160
|
+
})),
|
|
161
|
+
...diag.map((d) => ({ message: `misconfigured rule: ${d}` })),
|
|
162
|
+
],
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function policyGate(cwd, rules, changedFiles) {
|
|
166
|
+
if (rules.length === 0)
|
|
167
|
+
return skip('policy', 'no policy rules configured');
|
|
168
|
+
const report = runPolicyLint(cwd, rules, { changedOnly: true, changedFiles });
|
|
169
|
+
if (report.evaluated === 0) {
|
|
170
|
+
return skip('policy', `${report.rules.length} rule(s) configured but none matched the changed scope`);
|
|
171
|
+
}
|
|
172
|
+
const errors = report.findings.filter((f) => f.severity === 'error');
|
|
173
|
+
const warnings = report.findings.filter((f) => f.severity === 'warning');
|
|
174
|
+
const diag = report.diagnostics;
|
|
175
|
+
return {
|
|
176
|
+
name: 'policy',
|
|
177
|
+
status: report.verdict === 'errors' || diag.length > 0 ? 'fail' : 'pass',
|
|
178
|
+
detail: `${report.evaluated}/${report.rules.length} rule(s) evaluated — ${errors.length} error(s), ${warnings.length} warning(s)${diag.length > 0 ? `, ${diag.length} misconfigured` : ''}`,
|
|
179
|
+
errors: errors.length + diag.length,
|
|
180
|
+
warnings: warnings.length,
|
|
181
|
+
items: [
|
|
182
|
+
...report.findings.map((f) => ({
|
|
183
|
+
file: f.file,
|
|
184
|
+
line: f.line,
|
|
185
|
+
message: `[${f.severity}] ${f.ruleId}: ${f.message ?? ''}`.trim(),
|
|
186
|
+
})),
|
|
187
|
+
...diag.map((d) => ({ message: `misconfigured rule: ${d}` })),
|
|
188
|
+
],
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
async function orphansGate(input) {
|
|
192
|
+
const opts = orphanOptsFor(input);
|
|
193
|
+
if (!opts)
|
|
194
|
+
return skip('orphans', 'explicit --files scope has no diff to read deletions from');
|
|
195
|
+
const scan = await computeDeletedOrphans(input.cwd, opts);
|
|
196
|
+
if (!scan.ok) {
|
|
197
|
+
return skip('orphans', scan.reason === 'graph-missing'
|
|
198
|
+
? 'code-graph index missing — run `shrk graph index` to enable the orphan check'
|
|
199
|
+
: `diff unavailable: ${scan.error ?? 'unknown'}`);
|
|
200
|
+
}
|
|
201
|
+
if (scan.deleted.length === 0)
|
|
202
|
+
return skip('orphans', `nothing deleted (vs ${scan.ref})`);
|
|
203
|
+
const orphans = scan.report?.orphans ?? [];
|
|
204
|
+
return {
|
|
205
|
+
name: 'orphans',
|
|
206
|
+
status: orphans.length > 0 ? 'fail' : 'pass',
|
|
207
|
+
detail: `${scan.deleted.length} deleted file(s) (vs ${scan.ref}) — ${orphans.length} surviving importer(s)`,
|
|
208
|
+
errors: orphans.length,
|
|
209
|
+
warnings: 0,
|
|
210
|
+
items: orphans.map((o) => ({
|
|
211
|
+
file: o.path ?? o.id,
|
|
212
|
+
...(typeof o.line === 'number' ? { line: o.line } : {}),
|
|
213
|
+
message: o.via === 'reference' && o.symbol
|
|
214
|
+
? `references \`${o.symbol}\` from deleted ${o.deletedFile}`
|
|
215
|
+
: `imports deleted ${o.deletedFile}`,
|
|
216
|
+
})),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
async function impactSummary(cwd, changedFiles) {
|
|
220
|
+
if (changedFiles.length === 0)
|
|
221
|
+
return { ran: false, note: 'no changed files' };
|
|
222
|
+
try {
|
|
223
|
+
const { GraphStore } = await import('@shrkcrft/graph');
|
|
224
|
+
if (!new GraphStore(cwd).exists()) {
|
|
225
|
+
return { ran: false, note: 'code-graph index missing — run `shrk graph index`' };
|
|
226
|
+
}
|
|
227
|
+
const { analyzeGraphImpact } = await import('@shrkcrft/impact-engine');
|
|
228
|
+
const analysis = analyzeGraphImpact({ kind: 'files', files: [...changedFiles] }, { projectRoot: cwd, maxDepth: 5, limit: 200 });
|
|
229
|
+
return {
|
|
230
|
+
ran: true,
|
|
231
|
+
risk: analysis.risk,
|
|
232
|
+
directDependents: analysis.directDependents.length,
|
|
233
|
+
transitiveDependents: analysis.transitiveDependents.length,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
catch (e) {
|
|
237
|
+
return { ran: false, note: `impact summary unavailable: ${e instanceof Error ? e.message : String(e)}` };
|
|
238
|
+
}
|
|
239
|
+
}
|
package/dist/main.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";AAEA,OAAO,EACL,eAAe,EAKhB,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";AAEA,OAAO,EACL,eAAe,EAKhB,MAAM,uBAAuB,CAAC;AA4X/B,wBAAgB,aAAa,IAAI,eAAe,CAgY/C;AAED,wBAAsB,MAAM,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA2BrE;AAqID;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAoDxE"}
|
package/dist/main.js
CHANGED
|
@@ -22,6 +22,7 @@ import { presetsListCommand, presetsGetCommand, presetsExplainCommand, presetsRe
|
|
|
22
22
|
import { taskCommand } from "./commands/task.command.js";
|
|
23
23
|
import { preflightCommand } from "./commands/preflight.command.js";
|
|
24
24
|
import { checkCommand } from "./commands/check.command.js";
|
|
25
|
+
import { finishCommand } from "./commands/finish.command.js";
|
|
25
26
|
import { diffCheckCommand } from "./commands/diff-check.command.js";
|
|
26
27
|
import { driftCommand } from "./commands/drift.command.js";
|
|
27
28
|
import { graphCommand } from "./commands/graph.command.js";
|
|
@@ -33,6 +34,7 @@ import { frameworkCommand } from "./commands/framework.command.js";
|
|
|
33
34
|
import { apiDiffCommand } from "./commands/api-diff.command.js";
|
|
34
35
|
import { gateCommand } from "./commands/gate.command.js";
|
|
35
36
|
import { policyLintCommand } from "./commands/policy-lint.command.js";
|
|
37
|
+
import { wiringCommand } from "./commands/wiring.command.js";
|
|
36
38
|
import { reuseCommand } from "./commands/reuse.command.js";
|
|
37
39
|
import { migrateCommand } from "./commands/migrate.command.js";
|
|
38
40
|
import { coverageCommand } from "./commands/coverage.command.js";
|
|
@@ -189,6 +191,7 @@ export function buildRegistry() {
|
|
|
189
191
|
registry.register(explainCommand);
|
|
190
192
|
registry.register(checkCommand);
|
|
191
193
|
registry.register(diffCheckCommand);
|
|
194
|
+
registry.register(finishCommand);
|
|
192
195
|
// changed-only preflight orchestrator.
|
|
193
196
|
registry.register(preflightCommand);
|
|
194
197
|
registry.register(driftCommand);
|
|
@@ -201,6 +204,7 @@ export function buildRegistry() {
|
|
|
201
204
|
registry.register(apiDiffCommand);
|
|
202
205
|
registry.register(gateCommand);
|
|
203
206
|
registry.register(policyLintCommand);
|
|
207
|
+
registry.register(wiringCommand);
|
|
204
208
|
registry.register(reuseCommand);
|
|
205
209
|
registry.register(migrateCommand);
|
|
206
210
|
registry.register(coverageCommand);
|