pqcheck 0.16.14 → 0.16.16
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/README.md +1 -1
- package/bin/pqcheck.js +129 -36
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
[](https://www.npmjs.com/package/pqcheck)
|
|
9
9
|
[](./LICENSE)
|
|
10
10
|
|
|
11
|
-
> **Latest: v0.16.
|
|
11
|
+
> **Latest: v0.16.16** — `pqcheck setup` now defaults to per-project install of the Claude Code statusLine + hooks (`./.claude/settings.json` instead of `~/.claude/settings.json`). The Cipherwake badge will only fire in projects where you ran setup, not across every Claude Code session on the machine. Pass `--scope global` to opt back into machine-wide install. [Full changelog →](./CHANGELOG.md)
|
|
12
12
|
|
|
13
13
|
## Two ways to use it
|
|
14
14
|
|
package/bin/pqcheck.js
CHANGED
|
@@ -24,7 +24,25 @@
|
|
|
24
24
|
})();
|
|
25
25
|
|
|
26
26
|
const API_BASE = process.env.PQCHECK_API_BASE || "https://cipherwake.io";
|
|
27
|
-
const VERSION = "0.16.
|
|
27
|
+
const VERSION = "0.16.15";
|
|
28
|
+
|
|
29
|
+
// v0.16.15 — attribution suffix. When the CLI runs inside GitHub Actions
|
|
30
|
+
// (GH sets GITHUB_ACTIONS=true automatically in every step) we append
|
|
31
|
+
// "(pqcheck-action)" to the User-Agent so the server-side classifier
|
|
32
|
+
// (lib/events.ts) buckets the call as `action` rather than `cli`. Lets
|
|
33
|
+
// the analytics dashboard split humans/CI invocations cleanly.
|
|
34
|
+
//
|
|
35
|
+
// No new data is collected — the User-Agent string was already being
|
|
36
|
+
// sent on every call; this is a labeling change.
|
|
37
|
+
//
|
|
38
|
+
// Opt out via PQCHECK_DISABLE_ACTION_ATTRIBUTION=1 (UA stays plain
|
|
39
|
+
// `pqcheck-cli/X.Y.Z` even under GitHub Actions). The env var is only
|
|
40
|
+
// honored when GITHUB_ACTIONS=true — it does nothing in other contexts.
|
|
41
|
+
const CI_ACTION_SUFFIX =
|
|
42
|
+
process.env.GITHUB_ACTIONS === "true" &&
|
|
43
|
+
process.env.PQCHECK_DISABLE_ACTION_ATTRIBUTION !== "1"
|
|
44
|
+
? " (pqcheck-action)"
|
|
45
|
+
: "";
|
|
28
46
|
|
|
29
47
|
// API-key support — paid tier (Founder Pro $19.99/mo launch pricing, locked while sub active) gets
|
|
30
48
|
// per-account monthly quotas instead of the per-IP rate limit. Set via:
|
|
@@ -38,7 +56,7 @@ const QP_API_KEY = (process.env.CIPHERWAKE_API_KEY || "").trim();
|
|
|
38
56
|
// Builds headers with optional Authorization. Use for every CLI → API call
|
|
39
57
|
// so a single env-var toggle authenticates every endpoint at once.
|
|
40
58
|
function apiHeaders(extra = {}) {
|
|
41
|
-
const h = { accept: "application/json", "user-agent": `pqcheck-cli/${VERSION}`, ...extra };
|
|
59
|
+
const h = { accept: "application/json", "user-agent": `pqcheck-cli/${VERSION}${CI_ACTION_SUFFIX}`, ...extra };
|
|
42
60
|
if (QP_API_KEY) h.authorization = `Bearer ${QP_API_KEY}`;
|
|
43
61
|
return h;
|
|
44
62
|
}
|
|
@@ -274,7 +292,7 @@ async function runOneScan({ domain, format, quiet, threshold, webhookUrl, multi,
|
|
|
274
292
|
const qs = fresh ? `?domain=${encodeURIComponent(domain)}&force=1` : `?domain=${encodeURIComponent(domain)}`;
|
|
275
293
|
const resp = await fetch(`${API_BASE}/api/scan${qs}`, {
|
|
276
294
|
method: "GET",
|
|
277
|
-
headers: apiHeaders({ "user-agent": `pqcheck-cli/${VERSION} (scan)` }),
|
|
295
|
+
headers: apiHeaders({ "user-agent": `pqcheck-cli/${VERSION}${CI_ACTION_SUFFIX} (scan)` }),
|
|
278
296
|
});
|
|
279
297
|
if (!quiet && format === "text") process.stderr.write("\r\x1b[K");
|
|
280
298
|
if (!resp.ok) {
|
|
@@ -321,7 +339,7 @@ async function runOneScan({ domain, format, quiet, threshold, webhookUrl, multi,
|
|
|
321
339
|
if (webhookUrl) {
|
|
322
340
|
fetch(webhookUrl, {
|
|
323
341
|
method: "POST",
|
|
324
|
-
headers: { "content-type": "application/json", "user-agent": `pqcheck-cli/${VERSION}` },
|
|
342
|
+
headers: { "content-type": "application/json", "user-agent": `pqcheck-cli/${VERSION}${CI_ACTION_SUFFIX}` },
|
|
325
343
|
body: JSON.stringify({ domain, report, source: "pqcheck-cli", at: new Date().toISOString() }),
|
|
326
344
|
}).catch(() => { /* best-effort — never fail the scan on webhook delivery */ });
|
|
327
345
|
}
|
|
@@ -543,7 +561,7 @@ async function runWatch({ domains, format, quiet, threshold, webhookUrl, interva
|
|
|
543
561
|
try {
|
|
544
562
|
const resp = await fetch(`${API_BASE}/api/scan?domain=${encodeURIComponent(domain)}`, {
|
|
545
563
|
method: "GET",
|
|
546
|
-
headers: apiHeaders({ "user-agent": `pqcheck-cli/${VERSION} (watch)` }),
|
|
564
|
+
headers: apiHeaders({ "user-agent": `pqcheck-cli/${VERSION}${CI_ACTION_SUFFIX} (watch)` }),
|
|
547
565
|
});
|
|
548
566
|
if (!resp.ok) continue;
|
|
549
567
|
const report = await resp.json();
|
|
@@ -554,7 +572,7 @@ async function runWatch({ domains, format, quiet, threshold, webhookUrl, interva
|
|
|
554
572
|
if (changed && webhookUrl) {
|
|
555
573
|
fetch(webhookUrl, {
|
|
556
574
|
method: "POST",
|
|
557
|
-
headers: { "content-type": "application/json", "user-agent": `pqcheck-cli/${VERSION}` },
|
|
575
|
+
headers: { "content-type": "application/json", "user-agent": `pqcheck-cli/${VERSION}${CI_ACTION_SUFFIX}` },
|
|
558
576
|
body: JSON.stringify({
|
|
559
577
|
type: "score_changed",
|
|
560
578
|
domain,
|
|
@@ -899,7 +917,7 @@ async function refreshVersionCacheInBackground(cachePath) {
|
|
|
899
917
|
const controller = new AbortController();
|
|
900
918
|
const timeout = setTimeout(() => controller.abort(), 2500);
|
|
901
919
|
const resp = await fetch(VERSION_REGISTRY_URL, {
|
|
902
|
-
headers: { "Accept": "application/json", "User-Agent": `pqcheck-cli/${VERSION}` },
|
|
920
|
+
headers: { "Accept": "application/json", "User-Agent": `pqcheck-cli/${VERSION}${CI_ACTION_SUFFIX}` },
|
|
903
921
|
signal: controller.signal,
|
|
904
922
|
});
|
|
905
923
|
clearTimeout(timeout);
|
|
@@ -1829,7 +1847,7 @@ async function runLockCommand(args) {
|
|
|
1829
1847
|
try {
|
|
1830
1848
|
const resp = await fetch(`${API_BASE}/api/scan?domain=${encodeURIComponent(domain)}`, {
|
|
1831
1849
|
method: "GET",
|
|
1832
|
-
headers: apiHeaders({ "user-agent": `pqcheck-cli/${VERSION} (lock)` }),
|
|
1850
|
+
headers: apiHeaders({ "user-agent": `pqcheck-cli/${VERSION}${CI_ACTION_SUFFIX} (lock)` }),
|
|
1833
1851
|
});
|
|
1834
1852
|
if (!stdout) process.stderr.write("\r\x1b[K");
|
|
1835
1853
|
if (!resp.ok) {
|
|
@@ -1909,7 +1927,7 @@ function buildQxmManifest(report, crypto) {
|
|
|
1909
1927
|
return {
|
|
1910
1928
|
schema: "https://cipherwake.io/schemas/qxm/v1",
|
|
1911
1929
|
schemaVersion: 1,
|
|
1912
|
-
generator: `pqcheck-cli/${VERSION}`,
|
|
1930
|
+
generator: `pqcheck-cli/${VERSION}${CI_ACTION_SUFFIX}`,
|
|
1913
1931
|
generatedAt: report.generatedAt || new Date().toISOString(),
|
|
1914
1932
|
domain: report.domain,
|
|
1915
1933
|
reachable: !!report.reachable,
|
|
@@ -2152,7 +2170,7 @@ async function runDepsCommand(args) {
|
|
|
2152
2170
|
batch.map(async (h) => {
|
|
2153
2171
|
try {
|
|
2154
2172
|
const r = await fetch(`${API_BASE}/api/scan?domain=${encodeURIComponent(h.host)}&source=cli-deps`, {
|
|
2155
|
-
headers: apiHeaders({ "user-agent": `pqcheck-cli/${VERSION} (deps)` }),
|
|
2173
|
+
headers: apiHeaders({ "user-agent": `pqcheck-cli/${VERSION}${CI_ACTION_SUFFIX} (deps)` }),
|
|
2156
2174
|
});
|
|
2157
2175
|
if (!r.ok) return { ...h, types: Array.from(h.types), scan: null, error: `${r.status}` };
|
|
2158
2176
|
const body = await r.json();
|
|
@@ -2391,7 +2409,7 @@ async function fetchPageHTML(domain) {
|
|
|
2391
2409
|
method: "GET",
|
|
2392
2410
|
redirect: "follow",
|
|
2393
2411
|
signal: ctrl.signal,
|
|
2394
|
-
headers: { "User-Agent": `pqcheck-cli/${VERSION} (deps; +https://cipherwake.io)` },
|
|
2412
|
+
headers: { "User-Agent": `pqcheck-cli/${VERSION}${CI_ACTION_SUFFIX} (deps; +https://cipherwake.io)` },
|
|
2395
2413
|
});
|
|
2396
2414
|
clearTimeout(t);
|
|
2397
2415
|
if (!resp.ok) return null;
|
|
@@ -2848,7 +2866,7 @@ async function runHistoryCommand(args) {
|
|
|
2848
2866
|
let h;
|
|
2849
2867
|
try {
|
|
2850
2868
|
const r = await fetch(`${API_BASE}/api/history?domain=${encodeURIComponent(domain)}&days=${days}`, {
|
|
2851
|
-
headers: apiHeaders({ "user-agent": `pqcheck-cli/${VERSION} (history)` }),
|
|
2869
|
+
headers: apiHeaders({ "user-agent": `pqcheck-cli/${VERSION}${CI_ACTION_SUFFIX} (history)` }),
|
|
2852
2870
|
});
|
|
2853
2871
|
if (!r.ok) {
|
|
2854
2872
|
console.error(color("red", `error: ${r.status} ${r.statusText}`));
|
|
@@ -2931,7 +2949,7 @@ async function runChangesCommand(args) {
|
|
|
2931
2949
|
let summary;
|
|
2932
2950
|
try {
|
|
2933
2951
|
const r = await fetch(`${API_BASE}/api/changes-summary?domain=${encodeURIComponent(domain)}`, {
|
|
2934
|
-
headers: apiHeaders({ "user-agent": `pqcheck-cli/${VERSION} (changes)` }),
|
|
2952
|
+
headers: apiHeaders({ "user-agent": `pqcheck-cli/${VERSION}${CI_ACTION_SUFFIX} (changes)` }),
|
|
2935
2953
|
});
|
|
2936
2954
|
if (!r.ok) {
|
|
2937
2955
|
console.error(color("red", `error: ${r.status} ${r.statusText}`));
|
|
@@ -3025,7 +3043,7 @@ async function runChangesCommand(args) {
|
|
|
3025
3043
|
async function runScanBasedDeployCheck(domain, args) {
|
|
3026
3044
|
const headers = {
|
|
3027
3045
|
"Content-Type": "application/json",
|
|
3028
|
-
"User-Agent": `pqcheck-cli/${VERSION}`,
|
|
3046
|
+
"User-Agent": `pqcheck-cli/${VERSION}${CI_ACTION_SUFFIX}`,
|
|
3029
3047
|
};
|
|
3030
3048
|
if (QP_API_KEY) headers["Authorization"] = `Bearer ${QP_API_KEY}`;
|
|
3031
3049
|
|
|
@@ -3198,7 +3216,7 @@ async function runTrustDiffCommand(args) {
|
|
|
3198
3216
|
// anonymous per-IP rate limit path (just like /api/scan).
|
|
3199
3217
|
const headers = {
|
|
3200
3218
|
"Content-Type": "application/json",
|
|
3201
|
-
"User-Agent": `pqcheck-cli/${VERSION}`,
|
|
3219
|
+
"User-Agent": `pqcheck-cli/${VERSION}${CI_ACTION_SUFFIX}`,
|
|
3202
3220
|
};
|
|
3203
3221
|
if (QP_API_KEY) {
|
|
3204
3222
|
headers["Authorization"] = `Bearer ${QP_API_KEY}`;
|
|
@@ -3696,7 +3714,7 @@ async function runPreviewDiffCommand(args) {
|
|
|
3696
3714
|
|
|
3697
3715
|
const headers = {
|
|
3698
3716
|
"Content-Type": "application/json",
|
|
3699
|
-
"User-Agent": `pqcheck-cli/${VERSION}`,
|
|
3717
|
+
"User-Agent": `pqcheck-cli/${VERSION}${CI_ACTION_SUFFIX}`,
|
|
3700
3718
|
};
|
|
3701
3719
|
if (QP_API_KEY) {
|
|
3702
3720
|
headers["Authorization"] = `Bearer ${QP_API_KEY}`;
|
|
@@ -4423,7 +4441,7 @@ function renderReleaseChecklist(domain, opts = {}) {
|
|
|
4423
4441
|
// `pqcheck init` — interactive workflow scaffold (habit-loop #4, locked 2026-05-16)
|
|
4424
4442
|
// =============================================================================
|
|
4425
4443
|
// Writes a ready-to-commit .github/workflows/cipherwake.yml that calls
|
|
4426
|
-
// cipherwakelabs/pqcheck
|
|
4444
|
+
// cipherwakelabs/pqcheck@v4 in trust-diff mode. Zero copy-paste docs friction.
|
|
4427
4445
|
//
|
|
4428
4446
|
// Flags:
|
|
4429
4447
|
// --domain <d> Skip the domain prompt
|
|
@@ -4600,7 +4618,7 @@ jobs:
|
|
|
4600
4618
|
runs-on: ubuntu-latest
|
|
4601
4619
|
steps:
|
|
4602
4620
|
- name: Run Cipherwake Trust Diff
|
|
4603
|
-
uses: cipherwakelabs/pqcheck
|
|
4621
|
+
uses: cipherwakelabs/pqcheck@v4
|
|
4604
4622
|
with:
|
|
4605
4623
|
mode: trust-diff
|
|
4606
4624
|
domain: ${domain}
|
|
@@ -4766,7 +4784,7 @@ async function fetchVendorOrigins(domain) {
|
|
|
4766
4784
|
try {
|
|
4767
4785
|
resp = await fetch(`${API_BASE}/api/deps?domain=${encodeURIComponent(domain)}`, {
|
|
4768
4786
|
method: "GET",
|
|
4769
|
-
headers: apiHeaders({ "user-agent": `pqcheck-cli/${VERSION} (vendors)` }),
|
|
4787
|
+
headers: apiHeaders({ "user-agent": `pqcheck-cli/${VERSION}${CI_ACTION_SUFFIX} (vendors)` }),
|
|
4770
4788
|
signal: ac.signal,
|
|
4771
4789
|
});
|
|
4772
4790
|
} catch (err) {
|
|
@@ -4814,7 +4832,7 @@ function normalizeObservedOrigin(value) {
|
|
|
4814
4832
|
function buildVendorLockfile(domain, origins) {
|
|
4815
4833
|
return {
|
|
4816
4834
|
schema_version: 1,
|
|
4817
|
-
generator: `pqcheck-cli/${VERSION}`,
|
|
4835
|
+
generator: `pqcheck-cli/${VERSION}${CI_ACTION_SUFFIX}`,
|
|
4818
4836
|
domain,
|
|
4819
4837
|
generated_at: new Date().toISOString(),
|
|
4820
4838
|
approved_script_origins: origins,
|
|
@@ -4930,7 +4948,7 @@ async function runVendorsSync(domain, outPath) {
|
|
|
4930
4948
|
resp = await fetch(`${API_BASE}/api/vendor-allowlist?domain=${encodeURIComponent(domain)}`, {
|
|
4931
4949
|
method: "GET",
|
|
4932
4950
|
headers: {
|
|
4933
|
-
"user-agent": `pqcheck-cli/${VERSION} (vendors-sync)`,
|
|
4951
|
+
"user-agent": `pqcheck-cli/${VERSION}${CI_ACTION_SUFFIX} (vendors-sync)`,
|
|
4934
4952
|
"authorization": "Bearer " + QP_API_KEY,
|
|
4935
4953
|
},
|
|
4936
4954
|
});
|
|
@@ -5084,7 +5102,7 @@ async function runOnboardCommand(args) {
|
|
|
5084
5102
|
try {
|
|
5085
5103
|
const resp = await fetch(`${API_BASE}/api/scan?domain=${encodeURIComponent(domain)}&source=onboard`, {
|
|
5086
5104
|
method: "GET",
|
|
5087
|
-
headers: apiHeaders({ "user-agent": `pqcheck-cli/${VERSION} (onboard)` }),
|
|
5105
|
+
headers: apiHeaders({ "user-agent": `pqcheck-cli/${VERSION}${CI_ACTION_SUFFIX} (onboard)` }),
|
|
5088
5106
|
});
|
|
5089
5107
|
if (resp.ok) {
|
|
5090
5108
|
const report = await resp.json();
|
|
@@ -5595,9 +5613,23 @@ async function runProtocolCommand(args) {
|
|
|
5595
5613
|
console.log("Here's what would be added:");
|
|
5596
5614
|
console.log("");
|
|
5597
5615
|
if (detected.length === 0) {
|
|
5616
|
+
// Default to creating project-local ./CLAUDE.md rather than global
|
|
5617
|
+
// ~/.claude/CLAUDE.md. Mirrors the cli/setup --scope=project default
|
|
5618
|
+
// (commit 4450e77): don't pollute global config when no signal exists
|
|
5619
|
+
// that the user wants machine-wide installation. Pass --scope=global
|
|
5620
|
+
// to override and create ~/.claude/CLAUDE.md instead.
|
|
5621
|
+
const scopeRaw = (parseFlag(args, "--scope") || "project").toLowerCase();
|
|
5622
|
+
const useGlobal = scopeRaw === "global";
|
|
5623
|
+
const fallbackPath = useGlobal
|
|
5624
|
+
? path.join(os.homedir(), ".claude", "CLAUDE.md")
|
|
5625
|
+
: path.join(process.cwd(), "CLAUDE.md");
|
|
5626
|
+
const fallbackDisplay = fallbackPath.replace(os.homedir(), "~");
|
|
5598
5627
|
console.log(color("dim", " No existing CLAUDE.md / .cursorrules / .aider.conf.yml found."));
|
|
5599
|
-
console.log(color("dim",
|
|
5600
|
-
|
|
5628
|
+
console.log(color("dim", ` Creating ${fallbackDisplay} with the protocol.`));
|
|
5629
|
+
if (!useGlobal) {
|
|
5630
|
+
console.log(color("dim", " (pass --scope global to create ~/.claude/CLAUDE.md instead)"));
|
|
5631
|
+
}
|
|
5632
|
+
detected.push({ label: useGlobal ? "Claude Code (will create global)" : "Claude Code (will create project)", path: fallbackPath });
|
|
5601
5633
|
}
|
|
5602
5634
|
for (const d of detected) {
|
|
5603
5635
|
console.log(` • Append a ~30-line "## Pre-deploy verification with Cipherwake" section to ${color("bold", d.path)}`);
|
|
@@ -5909,11 +5941,21 @@ async function runSetupCommand(args) {
|
|
|
5909
5941
|
const skipHook = args.includes("--skip-hook");
|
|
5910
5942
|
const skipStatusline = args.includes("--skip-statusline");
|
|
5911
5943
|
const skipVscode = args.includes("--skip-vscode");
|
|
5944
|
+
// Where to install Claude Code statusLine + hooks. Default 'project' keeps
|
|
5945
|
+
// Cipherwake scoped to the current repo (avoids the "Cipherwake badge
|
|
5946
|
+
// follows me into unrelated projects" UX bug). 'global' is opt-in for
|
|
5947
|
+
// power users who want one canonical domain badge across every project.
|
|
5948
|
+
const settingsScope = (parseFlag(args, "--scope") || "project").toLowerCase();
|
|
5949
|
+
if (!["project", "global"].includes(settingsScope)) {
|
|
5950
|
+
console.error(color("red", `error: --scope must be 'project' or 'global' (got '${settingsScope}')`));
|
|
5951
|
+
process.exit(3);
|
|
5952
|
+
}
|
|
5912
5953
|
|
|
5913
5954
|
if (!domain) {
|
|
5914
5955
|
console.error(color("red", "error: pqcheck setup requires --domain"));
|
|
5915
5956
|
console.error(color("dim", "Usage: npx pqcheck setup --auto --domain example.com"));
|
|
5916
|
-
console.error(color("dim", " --plan
|
|
5957
|
+
console.error(color("dim", " --plan Print the install plan without writing any files"));
|
|
5958
|
+
console.error(color("dim", " --scope project|global Where to install Claude Code hooks (default: project — this repo only)"));
|
|
5917
5959
|
console.error(color("dim", " --invoked-by=\"<name>\" --consent-phrase=\"<words>\" (audit trail)"));
|
|
5918
5960
|
console.error(color("dim", "Skip flags: --skip-workflow --skip-protocol --skip-hook --skip-statusline --skip-vscode"));
|
|
5919
5961
|
process.exit(3);
|
|
@@ -5959,11 +6001,17 @@ async function runSetupCommand(args) {
|
|
|
5959
6001
|
}
|
|
5960
6002
|
if (!skipHook) planEntries.push({ what: "git pre-push hook", to: path.join(process.cwd(), ".git", "hooks", "pre-push"), op: "create (if git repo)" });
|
|
5961
6003
|
if (!skipStatusline) {
|
|
5962
|
-
const settingsPath =
|
|
6004
|
+
const settingsPath = settingsScope === "global"
|
|
6005
|
+
? path.join(os.homedir(), ".claude", "settings.json")
|
|
6006
|
+
: path.join(process.cwd(), ".claude", "settings.json");
|
|
5963
6007
|
let exists = false;
|
|
5964
6008
|
try { await fs.access(settingsPath); exists = true; } catch { /* */ }
|
|
5965
|
-
|
|
6009
|
+
const scopeNote = settingsScope === "global"
|
|
6010
|
+
? " [GLOBAL — fires in every project on this machine]"
|
|
6011
|
+
: " [PROJECT-LOCAL — fires only when Claude Code runs in this directory]";
|
|
6012
|
+
planEntries.push({ what: `Claude Code statusLine${scopeNote}`, to: settingsPath, op: exists ? "deep-merge (backup first)" : "create" });
|
|
5966
6013
|
planEntries.push({ what: "Claude Code chat-hook (PostToolUse Bash)", to: settingsPath, op: exists ? "deep-merge into hooks.PostToolUse" : "create" });
|
|
6014
|
+
planEntries.push({ what: "Claude Code prompt-hook (UserPromptSubmit)", to: settingsPath, op: exists ? "deep-merge into hooks.UserPromptSubmit" : "create" });
|
|
5967
6015
|
}
|
|
5968
6016
|
if (!skipVscode) planEntries.push({ what: "VS Code / Cursor extension", to: "via `code --install-extension cipherwakelabs.cipherwake-statusbar`", op: "attempt (soft-fail if Marketplace listing missing)" });
|
|
5969
6017
|
for (const e of planEntries) {
|
|
@@ -5994,6 +6042,48 @@ async function runSetupCommand(args) {
|
|
|
5994
6042
|
console.log("");
|
|
5995
6043
|
}
|
|
5996
6044
|
|
|
6045
|
+
// Resolve where to write Claude Code statusLine + hook entries.
|
|
6046
|
+
// Default 'project' = ./.claude/settings.json (this repo only). 'global' = ~/.claude/settings.json
|
|
6047
|
+
// (fires in every Claude Code session on this machine). Project-local is the right default for
|
|
6048
|
+
// anyone with multiple projects; global is the legacy behavior and now an opt-in for the
|
|
6049
|
+
// "one canonical domain across everything" use case.
|
|
6050
|
+
const claudeSettingsPath = settingsScope === "global"
|
|
6051
|
+
? path.join(os.homedir(), ".claude", "settings.json")
|
|
6052
|
+
: path.join(process.cwd(), ".claude", "settings.json");
|
|
6053
|
+
|
|
6054
|
+
if (!skipStatusline) {
|
|
6055
|
+
const displayPath = claudeSettingsPath.replace(os.homedir(), "~");
|
|
6056
|
+
const scopeLabel = settingsScope === "global"
|
|
6057
|
+
? `global (${displayPath} — fires in EVERY project on this machine)`
|
|
6058
|
+
: `project (${displayPath} — fires only when Claude Code runs in this directory)`;
|
|
6059
|
+
console.log(color("dim", `Settings scope: ${scopeLabel}`));
|
|
6060
|
+
console.log(color("dim", settingsScope === "global"
|
|
6061
|
+
? ` (omit --scope or pass --scope project to install for this repo only)`
|
|
6062
|
+
: ` (pass --scope global to install for every project on this machine)`));
|
|
6063
|
+
console.log("");
|
|
6064
|
+
|
|
6065
|
+
// Detect existing GLOBAL Cipherwake install while doing a project install.
|
|
6066
|
+
// Warn so the user doesn't end up with two layers firing on top of each other.
|
|
6067
|
+
if (settingsScope === "project") {
|
|
6068
|
+
try {
|
|
6069
|
+
const globalPath = path.join(os.homedir(), ".claude", "settings.json");
|
|
6070
|
+
const raw = await fs.readFile(globalPath, "utf8");
|
|
6071
|
+
const parsed = JSON.parse(raw);
|
|
6072
|
+
const hasGlobalCipherwake =
|
|
6073
|
+
(parsed.statusLine?.command && String(parsed.statusLine.command).includes("cipherwake")) ||
|
|
6074
|
+
JSON.stringify(parsed.hooks || {}).includes("cipherwake");
|
|
6075
|
+
if (hasGlobalCipherwake) {
|
|
6076
|
+
console.log(color("yellow", ` ⚠ A GLOBAL Cipherwake install already exists at ~/.claude/settings.json`));
|
|
6077
|
+
console.log(color("dim", ` It will continue firing across every project. To remove the global install,`));
|
|
6078
|
+
console.log(color("dim", ` edit ~/.claude/settings.json and delete the statusLine entry and the`));
|
|
6079
|
+
console.log(color("dim", ` cipherwake-chat-hook / cipherwake-prompt-hook commands. Backups are taken`));
|
|
6080
|
+
console.log(color("dim", ` automatically before any write, so changes are reversible.`));
|
|
6081
|
+
console.log("");
|
|
6082
|
+
}
|
|
6083
|
+
} catch { /* no global install — fine */ }
|
|
6084
|
+
}
|
|
6085
|
+
}
|
|
6086
|
+
|
|
5997
6087
|
const installSummary = [];
|
|
5998
6088
|
|
|
5999
6089
|
// -------------------------------------------------------------------------
|
|
@@ -6158,7 +6248,8 @@ async function runSetupCommand(args) {
|
|
|
6158
6248
|
}
|
|
6159
6249
|
|
|
6160
6250
|
if (!skipStatusline) {
|
|
6161
|
-
const settingsPath =
|
|
6251
|
+
const settingsPath = claudeSettingsPath;
|
|
6252
|
+
const displayPath = settingsPath.replace(os.homedir(), "~");
|
|
6162
6253
|
try {
|
|
6163
6254
|
let settings = {};
|
|
6164
6255
|
let existed = false;
|
|
@@ -6169,7 +6260,7 @@ async function runSetupCommand(args) {
|
|
|
6169
6260
|
} catch { /* will create */ }
|
|
6170
6261
|
if (existed && settings.statusLine && typeof settings.statusLine === "object") {
|
|
6171
6262
|
// Already has a statusLine config — don't overwrite.
|
|
6172
|
-
console.log(color("dim", ` ⊝
|
|
6263
|
+
console.log(color("dim", ` ⊝ ${displayPath} already has a statusLine entry — leaving alone`));
|
|
6173
6264
|
console.log(color("dim", ` To use the Cipherwake statusline instead, set: "command": "npx --package=pqcheck@latest cipherwake-statusline"`));
|
|
6174
6265
|
installSummary.push({ component: "Claude Code statusLine", path: settingsPath, status: "skipped-existing-config" });
|
|
6175
6266
|
} else {
|
|
@@ -6178,7 +6269,7 @@ async function runSetupCommand(args) {
|
|
|
6178
6269
|
settings.statusLine = { type: "command", command: "npx --package=pqcheck@latest cipherwake-statusline" };
|
|
6179
6270
|
await fs.mkdir(path.dirname(settingsPath), { recursive: true });
|
|
6180
6271
|
await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
6181
|
-
console.log(color("green", ` ✓ added statusLine config →
|
|
6272
|
+
console.log(color("green", ` ✓ added statusLine config → ${displayPath}`));
|
|
6182
6273
|
installSummary.push({ component: "Claude Code statusLine", path: settingsPath, status: existed ? "installed-updated" : "installed-created", backup: backupPath });
|
|
6183
6274
|
}
|
|
6184
6275
|
} catch (err) {
|
|
@@ -6194,7 +6285,8 @@ async function runSetupCommand(args) {
|
|
|
6194
6285
|
// doesn't clobber other hooks per CLAUDE.md Rule 17.
|
|
6195
6286
|
// -------------------------------------------------------------------------
|
|
6196
6287
|
if (!skipStatusline) {
|
|
6197
|
-
const settingsPath =
|
|
6288
|
+
const settingsPath = claudeSettingsPath;
|
|
6289
|
+
const displayPath = settingsPath.replace(os.homedir(), "~");
|
|
6198
6290
|
try {
|
|
6199
6291
|
let settings = {};
|
|
6200
6292
|
let existed = false;
|
|
@@ -6220,7 +6312,7 @@ async function runSetupCommand(args) {
|
|
|
6220
6312
|
);
|
|
6221
6313
|
|
|
6222
6314
|
if (alreadyInstalled) {
|
|
6223
|
-
console.log(color("dim", ` ⊝ chat-hook already configured in
|
|
6315
|
+
console.log(color("dim", ` ⊝ chat-hook already configured in ${displayPath} PostToolUse — skipping`));
|
|
6224
6316
|
installSummary.push({ component: "Claude Code chat-hook", path: settingsPath, status: "skipped-already-present" });
|
|
6225
6317
|
} else {
|
|
6226
6318
|
const backupPath = existed ? await backupSettingsJson(settingsPath) : null;
|
|
@@ -6228,7 +6320,7 @@ async function runSetupCommand(args) {
|
|
|
6228
6320
|
bashEntry.hooks.push({ type: "command", command: cipherwakeHookCmd });
|
|
6229
6321
|
await fs.mkdir(path.dirname(settingsPath), { recursive: true });
|
|
6230
6322
|
await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
6231
|
-
console.log(color("green", ` ✓ added chat-hook (PostToolUse Bash) →
|
|
6323
|
+
console.log(color("green", ` ✓ added chat-hook (PostToolUse Bash) → ${displayPath}`));
|
|
6232
6324
|
console.log(color("dim", ` Every \`pqcheck\` run will now push a live status message into Claude Code chat`));
|
|
6233
6325
|
installSummary.push({ component: "Claude Code chat-hook", path: settingsPath, status: existed ? "installed-updated" : "installed-created", backup: backupPath });
|
|
6234
6326
|
}
|
|
@@ -6247,7 +6339,8 @@ async function runSetupCommand(args) {
|
|
|
6247
6339
|
// / ship_decision=pass (no spam).
|
|
6248
6340
|
// -------------------------------------------------------------------------
|
|
6249
6341
|
if (!skipStatusline) {
|
|
6250
|
-
const settingsPath =
|
|
6342
|
+
const settingsPath = claudeSettingsPath;
|
|
6343
|
+
const displayPath = settingsPath.replace(os.homedir(), "~");
|
|
6251
6344
|
try {
|
|
6252
6345
|
let settings = {};
|
|
6253
6346
|
let existed = false;
|
|
@@ -6267,7 +6360,7 @@ async function runSetupCommand(args) {
|
|
|
6267
6360
|
);
|
|
6268
6361
|
|
|
6269
6362
|
if (alreadyInstalled) {
|
|
6270
|
-
console.log(color("dim", ` ⊝ prompt-hook already configured in
|
|
6363
|
+
console.log(color("dim", ` ⊝ prompt-hook already configured in ${displayPath} UserPromptSubmit — skipping`));
|
|
6271
6364
|
installSummary.push({ component: "Claude Code prompt-hook", path: settingsPath, status: "skipped-already-present" });
|
|
6272
6365
|
} else {
|
|
6273
6366
|
const backupPath = existed ? await backupSettingsJson(settingsPath) : null;
|
|
@@ -6275,7 +6368,7 @@ async function runSetupCommand(args) {
|
|
|
6275
6368
|
settings.hooks.UserPromptSubmit.push({ hooks: [{ type: "command", command: cipherwakeHookCmd }] });
|
|
6276
6369
|
await fs.mkdir(path.dirname(settingsPath), { recursive: true });
|
|
6277
6370
|
await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
6278
|
-
console.log(color("green", ` ✓ added prompt-hook (UserPromptSubmit) →
|
|
6371
|
+
console.log(color("green", ` ✓ added prompt-hook (UserPromptSubmit) → ${displayPath}`));
|
|
6279
6372
|
console.log(color("dim", ` Claude will see latest ship_decision in context on every prompt (when REVIEW/BLOCK)`));
|
|
6280
6373
|
installSummary.push({ component: "Claude Code prompt-hook", path: settingsPath, status: existed ? "installed-updated" : "installed-created", backup: backupPath });
|
|
6281
6374
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pqcheck",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.16",
|
|
4
4
|
"description": "Deploy gate for AI-coded web apps. `pqcheck deploy-check --ai` returns ship_decision=pass|review|block for Claude Code / Cursor / Copilot / Aider to gate deploys before they ship. Anonymous, no signup, free for first use.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-coder",
|