savemytokens 0.2.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/LICENSE +21 -0
- package/README.md +164 -0
- package/dist/adapters/claude-code/index.js +130 -0
- package/dist/adapters/claude-code/merge.js +90 -0
- package/dist/adapters/claude-code/parse.js +642 -0
- package/dist/adapters/claude-code/provider.js +105 -0
- package/dist/adapters/codex/index.js +74 -0
- package/dist/adapters/codex/parse.js +389 -0
- package/dist/adapters/codex/provider.js +171 -0
- package/dist/adapters/index.js +10 -0
- package/dist/adapters/pending.js +29 -0
- package/dist/adapters/types.js +1 -0
- package/dist/analyze/aggregate.js +180 -0
- package/dist/analyze/combine.js +11 -0
- package/dist/analyze/detectors.js +244 -0
- package/dist/analyze/index.js +29 -0
- package/dist/analyze/score.js +20 -0
- package/dist/cli-options.js +149 -0
- package/dist/cli.js +141 -0
- package/dist/collect.js +62 -0
- package/dist/commands/audit.js +74 -0
- package/dist/commands/control.js +654 -0
- package/dist/commands/hud.js +71 -0
- package/dist/commands/install.js +369 -0
- package/dist/commands/policy.js +93 -0
- package/dist/commands/privacy.js +28 -0
- package/dist/commands/set.js +83 -0
- package/dist/commands/theme.js +136 -0
- package/dist/commands/watch.js +135 -0
- package/dist/core/cost.js +24 -0
- package/dist/core/hash.js +0 -0
- package/dist/core/pricing.js +63 -0
- package/dist/core/resource.js +1 -0
- package/dist/core/tokens.js +32 -0
- package/dist/core/types.js +1 -0
- package/dist/hooks/nudge.js +111 -0
- package/dist/hooks/rules.js +14 -0
- package/dist/privacy/payload.js +22 -0
- package/dist/report/graph.js +162 -0
- package/dist/report/graphs.js +61 -0
- package/dist/report/render.js +183 -0
- package/dist/report/schedule.js +143 -0
- package/dist/report/settings.js +237 -0
- package/dist/report/views.js +418 -0
- package/dist/runtime/hook.mjs +234 -0
- package/dist/runtime/kernel.mjs +1472 -0
- package/dist/runtime/statusline.mjs +243 -0
- package/dist/scheduler/keys.js +112 -0
- package/dist/scheduler/plan.js +287 -0
- package/dist/storage/cache.js +38 -0
- package/dist/storage/paths.js +29 -0
- package/dist/storage/store.js +48 -0
- package/dist/util/ansi.js +35 -0
- package/dist/util/fmt.js +76 -0
- package/package.json +51 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const MAX_WASTE_PENALTY = 55;
|
|
2
|
+
const MAX_ERROR_PENALTY = 8;
|
|
3
|
+
const MAX_INTERRUPT_PENALTY = 5;
|
|
4
|
+
export function scoreAudit(agg, wasteRatio) {
|
|
5
|
+
const breakdown = [{ id: "base", label: "Base", points: 100 }];
|
|
6
|
+
const wastePenalty = Math.min(MAX_WASTE_PENALTY, Math.round(wasteRatio * 120));
|
|
7
|
+
if (wastePenalty > 0)
|
|
8
|
+
breakdown.push({ id: "waste", label: "Avoidable token spend", points: -wastePenalty });
|
|
9
|
+
const turns = Math.max(1, agg.totals.turns);
|
|
10
|
+
const errorRate = (agg.apiErrors + agg.toolErrors) / turns;
|
|
11
|
+
const errorPenalty = Math.min(MAX_ERROR_PENALTY, Math.round(errorRate * 40));
|
|
12
|
+
if (errorPenalty > 0)
|
|
13
|
+
breakdown.push({ id: "errors", label: "Failed calls and retries", points: -errorPenalty });
|
|
14
|
+
const tasks = Math.max(1, agg.totals.tasks);
|
|
15
|
+
const interruptPenalty = Math.min(MAX_INTERRUPT_PENALTY, Math.round((agg.outcomes.interrupted / tasks) * 20));
|
|
16
|
+
if (interruptPenalty > 0)
|
|
17
|
+
breakdown.push({ id: "interrupted", label: "Interrupted tasks", points: -interruptPenalty });
|
|
18
|
+
const total = breakdown.reduce((sum, c) => sum + c.points, 0);
|
|
19
|
+
return { score: Math.max(0, Math.min(100, Math.round(total))), breakdown };
|
|
20
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
const COMMANDS = new Set([
|
|
2
|
+
"control",
|
|
3
|
+
"status",
|
|
4
|
+
"share",
|
|
5
|
+
"priority",
|
|
6
|
+
"release",
|
|
7
|
+
"pin",
|
|
8
|
+
"park",
|
|
9
|
+
"policy",
|
|
10
|
+
"defer",
|
|
11
|
+
"hud",
|
|
12
|
+
"audit",
|
|
13
|
+
"install",
|
|
14
|
+
"uninstall",
|
|
15
|
+
"watch",
|
|
16
|
+
"history",
|
|
17
|
+
"privacy",
|
|
18
|
+
"theme",
|
|
19
|
+
"help",
|
|
20
|
+
]);
|
|
21
|
+
export function parseArgs(argv) {
|
|
22
|
+
const options = {
|
|
23
|
+
command: "control",
|
|
24
|
+
args: [],
|
|
25
|
+
days: 7,
|
|
26
|
+
project: null,
|
|
27
|
+
projectExplicit: false,
|
|
28
|
+
json: false,
|
|
29
|
+
verbose: false,
|
|
30
|
+
save: true,
|
|
31
|
+
interval: 60,
|
|
32
|
+
dryRun: false,
|
|
33
|
+
force: false,
|
|
34
|
+
window: "five_hour",
|
|
35
|
+
adapter: "claude-code",
|
|
36
|
+
rules: false,
|
|
37
|
+
purge: false,
|
|
38
|
+
help: false,
|
|
39
|
+
version: false,
|
|
40
|
+
};
|
|
41
|
+
let commandSeen = false;
|
|
42
|
+
for (let i = 0; i < argv.length; i++) {
|
|
43
|
+
const arg = argv[i];
|
|
44
|
+
if (arg === undefined)
|
|
45
|
+
continue;
|
|
46
|
+
if (!arg.startsWith("-")) {
|
|
47
|
+
if (!commandSeen && COMMANDS.has(arg)) {
|
|
48
|
+
options.command = arg;
|
|
49
|
+
commandSeen = true;
|
|
50
|
+
}
|
|
51
|
+
else if (!commandSeen && options.args.length === 0) {
|
|
52
|
+
options.command = "unknown";
|
|
53
|
+
options.args.push(arg);
|
|
54
|
+
commandSeen = true;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
options.args.push(arg);
|
|
58
|
+
}
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
switch (arg) {
|
|
62
|
+
case "--days":
|
|
63
|
+
case "-d": {
|
|
64
|
+
const value = Number(argv[++i]);
|
|
65
|
+
if (Number.isFinite(value) && value > 0)
|
|
66
|
+
options.days = Math.min(365, Math.round(value));
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
case "--here":
|
|
70
|
+
options.project = process.cwd();
|
|
71
|
+
options.projectExplicit = true;
|
|
72
|
+
break;
|
|
73
|
+
case "--project": {
|
|
74
|
+
const value = argv[++i];
|
|
75
|
+
if (value) {
|
|
76
|
+
options.project = value;
|
|
77
|
+
options.projectExplicit = true;
|
|
78
|
+
}
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
case "--all":
|
|
82
|
+
options.project = null;
|
|
83
|
+
options.projectExplicit = true;
|
|
84
|
+
break;
|
|
85
|
+
case "--json":
|
|
86
|
+
options.json = true;
|
|
87
|
+
break;
|
|
88
|
+
case "--verbose":
|
|
89
|
+
case "-v":
|
|
90
|
+
options.verbose = true;
|
|
91
|
+
break;
|
|
92
|
+
case "--no-save":
|
|
93
|
+
options.save = false;
|
|
94
|
+
break;
|
|
95
|
+
case "--dry-run":
|
|
96
|
+
options.dryRun = true;
|
|
97
|
+
break;
|
|
98
|
+
case "--force":
|
|
99
|
+
options.force = true;
|
|
100
|
+
break;
|
|
101
|
+
case "--window": {
|
|
102
|
+
const value = String(argv[++i] ?? "");
|
|
103
|
+
if (/^(7d|week|seven|seven_day)$/i.test(value))
|
|
104
|
+
options.window = "seven_day";
|
|
105
|
+
else
|
|
106
|
+
options.window = "five_hour";
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
case "--7d":
|
|
110
|
+
options.window = "seven_day";
|
|
111
|
+
break;
|
|
112
|
+
case "--adapter": {
|
|
113
|
+
const value = argv[++i];
|
|
114
|
+
if (value)
|
|
115
|
+
options.adapter = value;
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
case "--codex":
|
|
119
|
+
options.adapter = "codex";
|
|
120
|
+
break;
|
|
121
|
+
case "--claude":
|
|
122
|
+
case "--claude-code":
|
|
123
|
+
options.adapter = "claude-code";
|
|
124
|
+
break;
|
|
125
|
+
case "--rules":
|
|
126
|
+
options.rules = true;
|
|
127
|
+
break;
|
|
128
|
+
case "--purge":
|
|
129
|
+
options.purge = true;
|
|
130
|
+
break;
|
|
131
|
+
case "--interval": {
|
|
132
|
+
const value = Number(argv[++i]);
|
|
133
|
+
if (Number.isFinite(value) && value >= 5)
|
|
134
|
+
options.interval = Math.round(value);
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
case "--help":
|
|
138
|
+
case "-h":
|
|
139
|
+
options.help = true;
|
|
140
|
+
break;
|
|
141
|
+
case "--version":
|
|
142
|
+
options.version = true;
|
|
143
|
+
break;
|
|
144
|
+
default:
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return options;
|
|
149
|
+
}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { parseArgs } from "./cli-options.js";
|
|
4
|
+
import { runAudit } from "./commands/audit.js";
|
|
5
|
+
import { runControl } from "./commands/control.js";
|
|
6
|
+
import { runInstall, runUninstall } from "./commands/install.js";
|
|
7
|
+
import { runHud } from "./commands/hud.js";
|
|
8
|
+
import { runDefer, runPolicy } from "./commands/policy.js";
|
|
9
|
+
import { runPrivacy } from "./commands/privacy.js";
|
|
10
|
+
import { runSet } from "./commands/set.js";
|
|
11
|
+
import { runTheme } from "./commands/theme.js";
|
|
12
|
+
import { runWatch } from "./commands/watch.js";
|
|
13
|
+
import { renderHistory } from "./report/render.js";
|
|
14
|
+
import { loadRuns } from "./storage/store.js";
|
|
15
|
+
import { bold, dim } from "./util/ansi.js";
|
|
16
|
+
function version() {
|
|
17
|
+
try {
|
|
18
|
+
const pkg = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
19
|
+
return String(pkg.version ?? "0.0.0");
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return "0.0.0";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function help() {
|
|
26
|
+
return `
|
|
27
|
+
${bold("savemytokens")}: give every Claude Code session a target share of your Claude window.
|
|
28
|
+
|
|
29
|
+
npx savemytokens the control centre
|
|
30
|
+
npx savemytokens install hooks + status line, so it works while the TUI is closed
|
|
31
|
+
npx savemytokens uninstall remove them
|
|
32
|
+
npx savemytokens status one plain-text snapshot
|
|
33
|
+
npx savemytokens share <project> <percent|auto>
|
|
34
|
+
npx savemytokens priority <project> <high|normal|low>
|
|
35
|
+
npx savemytokens release <project> hand its unused share back
|
|
36
|
+
npx savemytokens pin|park <project> keep it visible, or drop it from the set
|
|
37
|
+
npx savemytokens policy what Claude does as the window fills
|
|
38
|
+
npx savemytokens defer work pushed to the next session
|
|
39
|
+
npx savemytokens hud status line layouts, previewed on your numbers
|
|
40
|
+
npx savemytokens theme themes for the TUI and the status line
|
|
41
|
+
npx savemytokens audit the token-waste report
|
|
42
|
+
npx savemytokens watch keep watching, and report drift as it happens
|
|
43
|
+
npx savemytokens history past audit scores
|
|
44
|
+
npx savemytokens privacy every file it reads and writes
|
|
45
|
+
|
|
46
|
+
-d, --days <n> audit window (default 7)
|
|
47
|
+
--json machine-readable
|
|
48
|
+
--force install: wrap an existing status line instead of leaving it alone
|
|
49
|
+
--rules install: also write the token-discipline block into ~/.claude/CLAUDE.md
|
|
50
|
+
--purge uninstall: also delete ~/.savemytokens
|
|
51
|
+
--7d allocate against the weekly window instead of the 5-hour one
|
|
52
|
+
--codex Codex instead of Claude Code (visibility only)
|
|
53
|
+
|
|
54
|
+
${dim("Everything runs locally. No account, no daemon, no upload. See what it stores: savemytokens privacy")}
|
|
55
|
+
${dim("Questions, bugs and ideas: hello@offbeatport.com")}
|
|
56
|
+
`;
|
|
57
|
+
}
|
|
58
|
+
const KNOWN_COMMANDS = [
|
|
59
|
+
"audit",
|
|
60
|
+
"defer",
|
|
61
|
+
"help",
|
|
62
|
+
"history",
|
|
63
|
+
"hud",
|
|
64
|
+
"install",
|
|
65
|
+
"park",
|
|
66
|
+
"pin",
|
|
67
|
+
"policy",
|
|
68
|
+
"priority",
|
|
69
|
+
"privacy",
|
|
70
|
+
"release",
|
|
71
|
+
"share",
|
|
72
|
+
"status",
|
|
73
|
+
"theme",
|
|
74
|
+
"uninstall",
|
|
75
|
+
"watch",
|
|
76
|
+
];
|
|
77
|
+
async function main() {
|
|
78
|
+
const options = parseArgs(process.argv.slice(2));
|
|
79
|
+
if (options.version) {
|
|
80
|
+
process.stdout.write(`${version()}\n`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (options.help || options.command === "help") {
|
|
84
|
+
process.stdout.write(help());
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
switch (options.command) {
|
|
88
|
+
case "install":
|
|
89
|
+
runInstall({ dryRun: options.dryRun, force: options.force, rules: options.rules });
|
|
90
|
+
return;
|
|
91
|
+
case "uninstall":
|
|
92
|
+
runUninstall(options.purge);
|
|
93
|
+
return;
|
|
94
|
+
case "theme":
|
|
95
|
+
runTheme(options.args);
|
|
96
|
+
return;
|
|
97
|
+
case "hud":
|
|
98
|
+
runHud(options);
|
|
99
|
+
return;
|
|
100
|
+
case "share":
|
|
101
|
+
case "priority":
|
|
102
|
+
case "release":
|
|
103
|
+
case "pin":
|
|
104
|
+
case "park":
|
|
105
|
+
runSet(options);
|
|
106
|
+
return;
|
|
107
|
+
case "policy":
|
|
108
|
+
runPolicy(options);
|
|
109
|
+
return;
|
|
110
|
+
case "defer":
|
|
111
|
+
runDefer(options);
|
|
112
|
+
return;
|
|
113
|
+
case "audit":
|
|
114
|
+
await runAudit(options);
|
|
115
|
+
return;
|
|
116
|
+
case "watch":
|
|
117
|
+
await runWatch(options);
|
|
118
|
+
return;
|
|
119
|
+
case "history":
|
|
120
|
+
process.stdout.write(renderHistory(loadRuns()));
|
|
121
|
+
return;
|
|
122
|
+
case "privacy":
|
|
123
|
+
runPrivacy();
|
|
124
|
+
return;
|
|
125
|
+
case "unknown": {
|
|
126
|
+
const name = options.args[0] ?? "";
|
|
127
|
+
const known = [...KNOWN_COMMANDS].filter((command) => command.startsWith(name[0] ?? "")).slice(0, 4);
|
|
128
|
+
process.stderr.write(`savemytokens: no command called ${name}.\n` +
|
|
129
|
+
(known.length > 0 ? `Did you mean: ${known.join(", ")}?\n` : "") +
|
|
130
|
+
"Run savemytokens --help for the list.\n");
|
|
131
|
+
process.exitCode = 1;
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
default:
|
|
135
|
+
await runControl(options);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
main().catch((error) => {
|
|
139
|
+
process.stderr.write(`savemytokens: ${error.message}\n`);
|
|
140
|
+
process.exitCode = 1;
|
|
141
|
+
});
|
package/dist/collect.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { activeAdapters } from "./adapters/index.js";
|
|
2
|
+
import { EvidenceCache } from "./storage/cache.js";
|
|
3
|
+
async function pool(items, limit, worker) {
|
|
4
|
+
let cursor = 0;
|
|
5
|
+
const runners = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
6
|
+
for (;;) {
|
|
7
|
+
const index = cursor++;
|
|
8
|
+
if (index >= items.length)
|
|
9
|
+
return;
|
|
10
|
+
const item = items[index];
|
|
11
|
+
if (item === undefined)
|
|
12
|
+
return;
|
|
13
|
+
await worker(item, index);
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
await Promise.all(runners);
|
|
17
|
+
}
|
|
18
|
+
export async function collect(options) {
|
|
19
|
+
const now = Date.now();
|
|
20
|
+
const since = now - options.days * 24 * 60 * 60 * 1000;
|
|
21
|
+
const sessions = [];
|
|
22
|
+
const adapters = activeAdapters();
|
|
23
|
+
const refs = [];
|
|
24
|
+
for (const adapter of adapters) {
|
|
25
|
+
const cache = new EvidenceCache(adapter.id);
|
|
26
|
+
for (const ref of adapter.discover({ since, project: options.project })) {
|
|
27
|
+
refs.push({ ref, cache });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
let done = 0;
|
|
31
|
+
await pool(refs, options.concurrency ?? 8, async ({ ref, cache }) => {
|
|
32
|
+
const cached = cache.get(ref.file, ref.size, ref.mtimeMs);
|
|
33
|
+
if (cached) {
|
|
34
|
+
sessions.push(cached);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
const adapter = adapters.find((a) => a.id === ref.adapter);
|
|
38
|
+
const evidence = adapter ? await adapter.parse(ref) : null;
|
|
39
|
+
if (evidence) {
|
|
40
|
+
cache.set(evidence);
|
|
41
|
+
sessions.push(evidence);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
done++;
|
|
45
|
+
options.onProgress?.(done, refs.length);
|
|
46
|
+
});
|
|
47
|
+
const caches = new Set(refs.map((r) => r.cache));
|
|
48
|
+
for (const cache of caches)
|
|
49
|
+
cache.flush();
|
|
50
|
+
sessions.sort((a, b) => a.startedAt - b.startedAt);
|
|
51
|
+
return {
|
|
52
|
+
scope: {
|
|
53
|
+
adapters: adapters.map((a) => a.id),
|
|
54
|
+
days: options.days,
|
|
55
|
+
project: options.project,
|
|
56
|
+
sessions: sessions.length,
|
|
57
|
+
from: since,
|
|
58
|
+
to: now,
|
|
59
|
+
},
|
|
60
|
+
sessions,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { activeAdapters, pendingDetected } from "../adapters/index.js";
|
|
4
|
+
import { encodeProject } from "../adapters/claude-code/index.js";
|
|
5
|
+
import { analyze } from "../analyze/index.js";
|
|
6
|
+
import { collect } from "../collect.js";
|
|
7
|
+
import { renderAudit } from "../report/render.js";
|
|
8
|
+
import { loadRuns, previousRun, saveRun } from "../storage/store.js";
|
|
9
|
+
import { dim } from "../util/ansi.js";
|
|
10
|
+
import { hookInstalled, nudgeStats } from "./install.js";
|
|
11
|
+
function progress(done, total) {
|
|
12
|
+
if (!process.stderr.isTTY || total < 12)
|
|
13
|
+
return;
|
|
14
|
+
const pct = Math.round((done / total) * 100);
|
|
15
|
+
process.stderr.write(`\r${dim(`reading sessions ${pct}%`)}`);
|
|
16
|
+
if (done === total)
|
|
17
|
+
process.stderr.write("\r" + " ".repeat(24) + "\r");
|
|
18
|
+
}
|
|
19
|
+
function hasLocalData(cwd) {
|
|
20
|
+
for (const adapter of activeAdapters()) {
|
|
21
|
+
if (adapter.id !== "claude-code")
|
|
22
|
+
continue;
|
|
23
|
+
try {
|
|
24
|
+
if (fs.existsSync(path.join(adapter.dataDir, encodeProject(cwd))))
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
export function defaultProject(options) {
|
|
34
|
+
if (options.projectExplicit)
|
|
35
|
+
return options.project;
|
|
36
|
+
const cwd = process.cwd();
|
|
37
|
+
return hasLocalData(cwd) ? cwd : null;
|
|
38
|
+
}
|
|
39
|
+
export async function runAudit(options) {
|
|
40
|
+
if (activeAdapters().length === 0) {
|
|
41
|
+
process.stdout.write("\nNo Claude Code or Codex data found.\nRun one of them once, then try again.\n\n");
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const project = defaultProject(options);
|
|
45
|
+
const corpus = await collect({
|
|
46
|
+
days: options.days,
|
|
47
|
+
project,
|
|
48
|
+
onProgress: options.json ? undefined : progress,
|
|
49
|
+
});
|
|
50
|
+
const audit = analyze(corpus);
|
|
51
|
+
const runs = loadRuns();
|
|
52
|
+
const previous = previousRun(audit, runs);
|
|
53
|
+
if (options.json) {
|
|
54
|
+
process.stdout.write(JSON.stringify({ audit, previous }, null, 2) + "\n");
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
process.stdout.write(renderAudit({
|
|
58
|
+
audit,
|
|
59
|
+
previous,
|
|
60
|
+
history: runs,
|
|
61
|
+
nudges: nudgeStats(),
|
|
62
|
+
installed: hookInstalled(),
|
|
63
|
+
verbose: options.verbose,
|
|
64
|
+
}));
|
|
65
|
+
if (options.verbose) {
|
|
66
|
+
for (const pending of pendingDetected()) {
|
|
67
|
+
process.stdout.write(dim(`${pending.label} found, but skipped: ${pending.reason}.\n`));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (options.save)
|
|
72
|
+
saveRun(audit);
|
|
73
|
+
return audit;
|
|
74
|
+
}
|