@checkbox-site/cli 0.2.0 → 0.3.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/dist/checkbox.js +362 -222
- package/package.json +1 -1
package/dist/checkbox.js
CHANGED
|
@@ -3673,9 +3673,9 @@ var init_client = __esm({
|
|
|
3673
3673
|
const p = globalThis.process;
|
|
3674
3674
|
return p?.env?.[name] || void 0;
|
|
3675
3675
|
};
|
|
3676
|
-
sleep = (ms, signal) => new Promise((
|
|
3676
|
+
sleep = (ms, signal) => new Promise((resolve9, reject) => {
|
|
3677
3677
|
if (signal?.aborted) return reject(signal.reason);
|
|
3678
|
-
const t = setTimeout(
|
|
3678
|
+
const t = setTimeout(resolve9, ms);
|
|
3679
3679
|
signal?.addEventListener("abort", () => (clearTimeout(t), reject(signal.reason)), { once: true });
|
|
3680
3680
|
});
|
|
3681
3681
|
Checkbox = class {
|
|
@@ -4113,11 +4113,11 @@ async function listCommands(dir = COMMANDS_DIR) {
|
|
|
4113
4113
|
}
|
|
4114
4114
|
function readAll(stream) {
|
|
4115
4115
|
if (!stream || stream.isTTY) return Promise.resolve("");
|
|
4116
|
-
return new Promise((
|
|
4116
|
+
return new Promise((resolve9, reject) => {
|
|
4117
4117
|
let data = "";
|
|
4118
4118
|
stream.setEncoding?.("utf8");
|
|
4119
4119
|
stream.on("data", (c) => data += c.toString());
|
|
4120
|
-
stream.on("end", () =>
|
|
4120
|
+
stream.on("end", () => resolve9(data));
|
|
4121
4121
|
stream.on("error", reject);
|
|
4122
4122
|
});
|
|
4123
4123
|
}
|
|
@@ -10465,8 +10465,8 @@ function isNear(el, cta, viewport, device) {
|
|
|
10465
10465
|
}
|
|
10466
10466
|
return verticalGap(el, cta) <= maxVerticalGapPx(device, viewport) && horizontalGap(el, cta) <= maxHorizontalGapPx(device);
|
|
10467
10467
|
}
|
|
10468
|
-
function tastefulSmall(
|
|
10469
|
-
if (
|
|
10468
|
+
function tastefulSmall(relative7, style2, opacity) {
|
|
10469
|
+
if (relative7 < CONSPICUOUS_TASTEFUL_RELATIVE_FONT) return false;
|
|
10470
10470
|
const contrast = textContrast({ color: style2.color, backgroundColor: style2.backgroundColor, opacity });
|
|
10471
10471
|
if (contrast === null || contrast < CONSPICUOUS_MIN_CONTRAST) return false;
|
|
10472
10472
|
return (style2.fontWeight ?? 400) >= 600 || contrast >= CONSPICUOUS_STRONG_CONTRAST;
|
|
@@ -10486,18 +10486,18 @@ function assessConspicuousness(el, opts = {}) {
|
|
|
10486
10486
|
warnings.push("font size could not be measured");
|
|
10487
10487
|
} else {
|
|
10488
10488
|
const ref = opts.referenceFontPx && opts.referenceFontPx > 0 ? opts.referenceFontPx : void 0;
|
|
10489
|
-
const
|
|
10489
|
+
const relative7 = ref ? fontSizePx / ref : void 0;
|
|
10490
10490
|
if (fontSizePx < CONSPICUOUS_MIN_FONT_PX) {
|
|
10491
|
-
if (
|
|
10491
|
+
if (relative7 !== void 0 && relative7 >= CONSPICUOUS_MIN_RELATIVE_FONT) {
|
|
10492
10492
|
warnings.push(
|
|
10493
|
-
`font is ${round2(fontSizePx)}px (below ${CONSPICUOUS_MIN_FONT_PX}px, but ${Math.round(
|
|
10493
|
+
`font is ${round2(fontSizePx)}px (below ${CONSPICUOUS_MIN_FONT_PX}px, but ${Math.round(relative7 * 100)}% of the surrounding ${round2(ref)}px text)`
|
|
10494
10494
|
);
|
|
10495
10495
|
} else {
|
|
10496
10496
|
failures.push(`font is ${round2(fontSizePx)}px (min ${CONSPICUOUS_MIN_FONT_PX}px)`);
|
|
10497
10497
|
}
|
|
10498
|
-
} else if (
|
|
10498
|
+
} else if (relative7 !== void 0 && relative7 < CONSPICUOUS_MIN_RELATIVE_FONT && !tastefulSmall(relative7, style2, opacity)) {
|
|
10499
10499
|
warnings.push(
|
|
10500
|
-
`font is ${round2(fontSizePx)}px, ${Math.round(
|
|
10500
|
+
`font is ${round2(fontSizePx)}px, ${Math.round(relative7 * 100)}% of the surrounding ${round2(ref)}px text \u2014 smaller than surrounding text must contrast or be set off to be conspicuous`
|
|
10501
10501
|
);
|
|
10502
10502
|
}
|
|
10503
10503
|
}
|
|
@@ -17328,9 +17328,9 @@ function limiter(max) {
|
|
|
17328
17328
|
run2();
|
|
17329
17329
|
}
|
|
17330
17330
|
};
|
|
17331
|
-
return async (fn) => new Promise((
|
|
17331
|
+
return async (fn) => new Promise((resolve9, reject) => {
|
|
17332
17332
|
queue.push(() => {
|
|
17333
|
-
fn().then(
|
|
17333
|
+
fn().then(resolve9, reject).finally(() => {
|
|
17334
17334
|
active--;
|
|
17335
17335
|
next();
|
|
17336
17336
|
});
|
|
@@ -22747,7 +22747,15 @@ function loadConfig(path) {
|
|
|
22747
22747
|
function validateConfig(value) {
|
|
22748
22748
|
const is = new Issues();
|
|
22749
22749
|
if (!isObj2(value)) return ["must be a JSON object (see https://checkbox.site/docs/guides/ci#config)"];
|
|
22750
|
-
is.keys("", value, ["$schema", "merchant", "jurisdictions", "plans", "checks"]);
|
|
22750
|
+
is.keys("", value, ["$schema", "merchant", "jurisdictions", "plans", "checks", "coverage"]);
|
|
22751
|
+
const cov = value.coverage;
|
|
22752
|
+
if (cov !== void 0 && cov !== false) {
|
|
22753
|
+
if (!isObj2(cov)) is.add("coverage", `must be false or an object like { "ignore": ["scripts/**"] } (got ${show(cov)})`);
|
|
22754
|
+
else {
|
|
22755
|
+
is.keys("coverage", cov, ["ignore"]);
|
|
22756
|
+
if (cov.ignore !== void 0 && (!Array.isArray(cov.ignore) || !cov.ignore.every((g) => typeof g === "string" && g.trim()))) is.add("coverage.ignore", `must be a list of file globs like ["scripts/**", "admin/*.ts"] (got ${show(cov.ignore)})`);
|
|
22757
|
+
}
|
|
22758
|
+
}
|
|
22751
22759
|
if (value.$schema !== void 0 && typeof value.$schema !== "string") is.add("$schema", "must be a string");
|
|
22752
22760
|
const m = value.merchant;
|
|
22753
22761
|
if (m !== void 0) {
|
|
@@ -22820,13 +22828,16 @@ function validateConfig(value) {
|
|
|
22820
22828
|
} else if (needsPlan === "always" && !planCount) {
|
|
22821
22829
|
is.add(at, 'needs the plan it describes: add it to "plans" (name, price, interval, trial_days)');
|
|
22822
22830
|
}
|
|
22831
|
+
if (e.covers !== void 0 && (!Array.isArray(e.covers) || !e.covers.every((c) => typeof c === "string" && /^[^#]+(?:#[1-9]\d*)?$/.test(c)))) {
|
|
22832
|
+
is.add(`${at}.covers`, `must be a list of files, or "file#n" for the n-th call (got ${show(e.covers)})`);
|
|
22833
|
+
}
|
|
22823
22834
|
each(at, e);
|
|
22824
22835
|
});
|
|
22825
22836
|
};
|
|
22826
22837
|
entries(
|
|
22827
22838
|
"stripe_checkout",
|
|
22828
22839
|
(at, e) => {
|
|
22829
|
-
is.keys(at, e, ["name", "plan", "params", "file", "module", "args", "source", "call"]);
|
|
22840
|
+
is.keys(at, e, ["name", "plan", "params", "file", "module", "args", "source", "call", "covers"]);
|
|
22830
22841
|
is.exactlyOne(at, e, ["params", "file", "module", "source"]);
|
|
22831
22842
|
if (e.params !== void 0 && !isObj2(e.params)) is.add(`${at}.params`, `must be the params object (got ${show(e.params)})`);
|
|
22832
22843
|
if (e.file !== void 0) is.path(`${at}.file`, e.file);
|
|
@@ -22842,7 +22853,7 @@ function validateConfig(value) {
|
|
|
22842
22853
|
entries(
|
|
22843
22854
|
"disclosures",
|
|
22844
22855
|
(at, e) => {
|
|
22845
|
-
is.keys(at, e, ["name", "plan", "text", "file", "button_text", "placement", "consent"]);
|
|
22856
|
+
is.keys(at, e, ["name", "plan", "text", "file", "button_text", "placement", "consent", "covers"]);
|
|
22846
22857
|
is.exactlyOne(at, e, ["text", "file"]);
|
|
22847
22858
|
if (e.text !== void 0) is.str(`${at}.text`, e.text, { max: 5e3 });
|
|
22848
22859
|
if (e.file !== void 0) is.path(`${at}.file`, e.file);
|
|
@@ -22854,7 +22865,7 @@ function validateConfig(value) {
|
|
|
22854
22865
|
entries(
|
|
22855
22866
|
"pages",
|
|
22856
22867
|
(at, e) => {
|
|
22857
|
-
is.keys(at, e, ["name", "plan", "file", "url", "kind"]);
|
|
22868
|
+
is.keys(at, e, ["name", "plan", "file", "url", "kind", "covers"]);
|
|
22858
22869
|
is.exactlyOne(at, e, ["file", "url"]);
|
|
22859
22870
|
if (e.file !== void 0) is.path(`${at}.file`, e.file);
|
|
22860
22871
|
if (e.url !== void 0) is.str(`${at}.url`, e.url, { pattern: /^https?:\/\/\S+$/, patternHint: "an http(s) URL" });
|
|
@@ -23524,15 +23535,299 @@ var init_detect = __esm({
|
|
|
23524
23535
|
}
|
|
23525
23536
|
});
|
|
23526
23537
|
|
|
23538
|
+
// src/ci/scaffold.ts
|
|
23539
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync6, statSync as statSync2 } from "node:fs";
|
|
23540
|
+
import { basename, join as join5, relative as relative2 } from "node:path";
|
|
23541
|
+
function walk3(root, visit) {
|
|
23542
|
+
let n = 0;
|
|
23543
|
+
const go = (dir, depth) => {
|
|
23544
|
+
if (depth > 8 || n > MAX_FILES2) return;
|
|
23545
|
+
let names = [];
|
|
23546
|
+
try {
|
|
23547
|
+
names = readdirSync3(dir).sort();
|
|
23548
|
+
} catch {
|
|
23549
|
+
return;
|
|
23550
|
+
}
|
|
23551
|
+
for (const name of names) {
|
|
23552
|
+
if (IGNORE.has(name) || name.startsWith(".") && name !== ".well-known") continue;
|
|
23553
|
+
const abs = join5(dir, name);
|
|
23554
|
+
let st;
|
|
23555
|
+
try {
|
|
23556
|
+
st = statSync2(abs);
|
|
23557
|
+
} catch {
|
|
23558
|
+
continue;
|
|
23559
|
+
}
|
|
23560
|
+
if (st.isDirectory()) go(abs, depth + 1);
|
|
23561
|
+
else if (st.isFile() && st.size < 4e5) {
|
|
23562
|
+
n++;
|
|
23563
|
+
visit(relative2(root, abs).split("\\").join("/"));
|
|
23564
|
+
}
|
|
23565
|
+
}
|
|
23566
|
+
};
|
|
23567
|
+
go(root, 0);
|
|
23568
|
+
}
|
|
23569
|
+
function emailKindOf(file) {
|
|
23570
|
+
const name = basename(file).replace(/\.[^.]+$/, "");
|
|
23571
|
+
return EMAIL_NAMES.find(([re]) => re.test(name))?.[1];
|
|
23572
|
+
}
|
|
23573
|
+
function scaffoldConfig(root, env2 = {}) {
|
|
23574
|
+
const det = detectProject(root, env2);
|
|
23575
|
+
const todos = [];
|
|
23576
|
+
const found = [];
|
|
23577
|
+
const names = /* @__PURE__ */ new Set();
|
|
23578
|
+
const merchant = {};
|
|
23579
|
+
merchant.name = det.brand?.name ?? "TODO: the brand subscribers see";
|
|
23580
|
+
if (!det.brand) todos.push("merchant.name: the brand subscribers see");
|
|
23581
|
+
const cancelPath = det.policyPaths.cancellation;
|
|
23582
|
+
if (det.siteUrl && cancelPath) merchant.cancel_url = `${originOf(det.siteUrl)}${cancelPath}`;
|
|
23583
|
+
else {
|
|
23584
|
+
merchant.cancel_url = "TODO: https://\u2026 page where subscribers cancel online";
|
|
23585
|
+
todos.push("merchant.cancel_url: the page where subscribers cancel online");
|
|
23586
|
+
}
|
|
23587
|
+
const plans = det.plans.map((p, i) => ({
|
|
23588
|
+
name: p.name ?? `Plan ${i + 1}`,
|
|
23589
|
+
amount: p.amount,
|
|
23590
|
+
currency: p.currency,
|
|
23591
|
+
interval: p.interval,
|
|
23592
|
+
...p.interval_count ? { interval_count: p.interval_count } : {},
|
|
23593
|
+
...p.trial_days ? { trial_days: p.trial_days } : {}
|
|
23594
|
+
}));
|
|
23595
|
+
for (const p of det.plans) found.push(`plan ${p.name ?? ""} ${p.amount / 100} ${p.currency}/${p.interval} (${p.source})`.replace(/\s+/g, " "));
|
|
23596
|
+
if (!plans.length) {
|
|
23597
|
+
plans.push({ name: "TODO: plan name", price: "TODO: e.g. 19.99", currency: "usd", interval: "month" });
|
|
23598
|
+
todos.push("plans: the price, interval and trial of each plan (the checkout uses Price ids, which cannot be read from code)");
|
|
23599
|
+
}
|
|
23600
|
+
const planNames = plans.map((p) => String(p.name));
|
|
23601
|
+
const planFor = () => plans.length > 1 ? { plan: `TODO: one of ${planNames.map((n) => JSON.stringify(n)).join(", ")}` } : {};
|
|
23602
|
+
if (plans.length > 1) todos.push("plan: pick the plan of each disclosure, page and email entry");
|
|
23603
|
+
const stripe = [];
|
|
23604
|
+
const perFile = /* @__PURE__ */ new Map();
|
|
23605
|
+
for (const s2 of det.checkoutSessions) {
|
|
23606
|
+
const call = (perFile.get(s2.file) ?? 0) + 1;
|
|
23607
|
+
perFile.set(s2.file, call);
|
|
23608
|
+
const entry = { name: unique(names, `Stripe Checkout (${basename(s2.file)}${call > 1 ? ` #${call}` : ""})`), source: s2.file, ...call > 1 ? { call } : {} };
|
|
23609
|
+
if (!s2.params) {
|
|
23610
|
+
todos.push(`${s2.file}:${s2.line}: the params are not an object literal; use "module" (an exported function returning the params) instead of "source"`);
|
|
23611
|
+
delete entry.source;
|
|
23612
|
+
delete entry.call;
|
|
23613
|
+
entry.module = `TODO: ${s2.file.replace(/\.[^.]+$/, "")}.ts#buildCheckoutParams`;
|
|
23614
|
+
} else if (s2.unresolved.length) found.push(`${s2.file}:${s2.line}: Checkout Session (${s2.unresolved.length} value(s) are expressions, read as-is)`);
|
|
23615
|
+
else found.push(`${s2.file}:${s2.line}: Checkout Session`);
|
|
23616
|
+
stripe.push(entry);
|
|
23617
|
+
}
|
|
23618
|
+
const emails = [];
|
|
23619
|
+
const pages = [];
|
|
23620
|
+
const disclosures = [];
|
|
23621
|
+
walk3(root, (rel2) => {
|
|
23622
|
+
if (IN_EMAIL_DIR.test(rel2)) {
|
|
23623
|
+
const kind = emailKindOf(rel2);
|
|
23624
|
+
if (/\.(?:html?|txt|md)$/i.test(rel2) && kind) {
|
|
23625
|
+
emails.push({ name: unique(names, `Email: ${basename(rel2)}`), kind, file: rel2, ...planFor() });
|
|
23626
|
+
found.push(`${rel2}: ${kind} email`);
|
|
23627
|
+
return;
|
|
23628
|
+
}
|
|
23629
|
+
if (/\.[jt]sx$|\.mjml$/i.test(rel2) && kind) {
|
|
23630
|
+
todos.push(`${rel2}: a ${kind} email component; add it with "module": "<file>#<export>" returning the rendered { subject, html } (React Email: render() with sample props)`);
|
|
23631
|
+
return;
|
|
23632
|
+
}
|
|
23633
|
+
}
|
|
23634
|
+
if (PAGE_FILE.test(rel2)) {
|
|
23635
|
+
const kind = /checkout|subscribe/i.test(rel2) ? "checkout" : "pricing";
|
|
23636
|
+
pages.push({ name: unique(names, `Page: ${rel2}`), file: rel2, kind, ...planFor() });
|
|
23637
|
+
found.push(`${rel2}: ${kind} page`);
|
|
23638
|
+
return;
|
|
23639
|
+
}
|
|
23640
|
+
if (/\.(?:txt|md)$/i.test(rel2) && !/(?:^|\/)(?:README|CHANGELOG|LICENSE|CONTRIBUTING|AGENTS|CLAUDE)[^/]*$/i.test(rel2) && !rel2.startsWith("docs/")) {
|
|
23641
|
+
let text = "";
|
|
23642
|
+
try {
|
|
23643
|
+
text = readFileSync6(join5(root, rel2), "utf8");
|
|
23644
|
+
} catch {
|
|
23645
|
+
return;
|
|
23646
|
+
}
|
|
23647
|
+
if (text.length < 1500 && DISCLOSURE_TEXT.test(text)) {
|
|
23648
|
+
disclosures.push({ name: unique(names, `Disclosure: ${basename(rel2)}`), file: rel2, ...planFor() });
|
|
23649
|
+
found.push(`${rel2}: disclosure text`);
|
|
23650
|
+
}
|
|
23651
|
+
}
|
|
23652
|
+
});
|
|
23653
|
+
if (!disclosures.length && det.payButtons.length) {
|
|
23654
|
+
todos.push(`The renewal disclosure lives in code (${det.payButtons[0].file}); keep its text in a .txt file the component imports and add it under checks.disclosures, or check the rendered page under checks.pages`);
|
|
23655
|
+
}
|
|
23656
|
+
const checks = {};
|
|
23657
|
+
if (stripe.length) checks.stripe_checkout = stripe;
|
|
23658
|
+
if (disclosures.length) checks.disclosures = disclosures;
|
|
23659
|
+
if (pages.length) checks.pages = pages;
|
|
23660
|
+
if (emails.length) checks.emails = emails;
|
|
23661
|
+
checks.notice_schedule = true;
|
|
23662
|
+
if (!stripe.length && !disclosures.length && !pages.length && !emails.length) {
|
|
23663
|
+
todos.push('No checkout code, email templates or pages were found: add entries under "checks" (https://checkbox.site/docs/guides/ci#config)');
|
|
23664
|
+
}
|
|
23665
|
+
return {
|
|
23666
|
+
config: { $schema: SCHEMA_URL, merchant, jurisdictions: "all", plans, checks },
|
|
23667
|
+
todos,
|
|
23668
|
+
found
|
|
23669
|
+
};
|
|
23670
|
+
}
|
|
23671
|
+
var IGNORE, MAX_FILES2, EMAIL_NAMES, IN_EMAIL_DIR, PAGE_FILE, DISCLOSURE_TEXT, unique;
|
|
23672
|
+
var init_scaffold = __esm({
|
|
23673
|
+
"src/ci/scaffold.ts"() {
|
|
23674
|
+
"use strict";
|
|
23675
|
+
init_detect();
|
|
23676
|
+
init_config3();
|
|
23677
|
+
IGNORE = /* @__PURE__ */ new Set(["node_modules", ".git", ".next", ".nuxt", ".svelte-kit", ".output", ".vercel", ".netlify", ".turbo", ".cache", "coverage", ".astro", "vendor", ".venv", "__pycache__", ".github"]);
|
|
23678
|
+
MAX_FILES2 = 5e3;
|
|
23679
|
+
EMAIL_NAMES = [
|
|
23680
|
+
[/trial/i, "trial_reminder"],
|
|
23681
|
+
[/price[-_ ]?(?:change|increase|update)/i, "price_change"],
|
|
23682
|
+
[/annual|yearly[-_ ]?terms/i, "annual_terms"],
|
|
23683
|
+
[/renew/i, "renewal_reminder"],
|
|
23684
|
+
[/cancel/i, "cancellation_confirmation"],
|
|
23685
|
+
[/receipt|charge|invoice|payment[-_ ]?(?:succeeded|receipt)/i, "charge_notice"],
|
|
23686
|
+
[/welcome|confirm|acknowledg|subscribed|signup|sign[-_]up|order/i, "acknowledgment"]
|
|
23687
|
+
];
|
|
23688
|
+
IN_EMAIL_DIR = /(?:^|\/)(?:e-?mails?|mail(?:ers?)?|notifications?)(?:\/|$)|(?:^|\/)templates\/(?:e-?mails?|mail)\//i;
|
|
23689
|
+
PAGE_FILE = /(?:^|\/)(?:public|dist|out|build|static|_site|www)\/(?:.*\/)?(pricing|checkout|subscribe|plans)(?:\/index)?\.html?$/i;
|
|
23690
|
+
DISCLOSURE_TEXT = /renews? automatically|automatically renews?|auto-?renew|until (?:you )?cancel/i;
|
|
23691
|
+
unique = (names, base) => {
|
|
23692
|
+
let n = base;
|
|
23693
|
+
for (let i = 2; names.has(n); i++) n = `${base} ${i}`;
|
|
23694
|
+
names.add(n);
|
|
23695
|
+
return n;
|
|
23696
|
+
};
|
|
23697
|
+
}
|
|
23698
|
+
});
|
|
23699
|
+
|
|
23700
|
+
// src/ci/coverage.ts
|
|
23701
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
23702
|
+
import { join as join6, relative as relative3, resolve as resolve3 } from "node:path";
|
|
23703
|
+
function callsIn(text, file) {
|
|
23704
|
+
const hits = [];
|
|
23705
|
+
for (const [re, kind] of PATTERNS) {
|
|
23706
|
+
for (const m of text.matchAll(re)) hits.push({ at: m.index, end: m.index + m[0].length, kind });
|
|
23707
|
+
}
|
|
23708
|
+
hits.sort((a, b) => a.at - b.at);
|
|
23709
|
+
const out = [];
|
|
23710
|
+
const count = { checkout_session: 0, payment_link: 0 };
|
|
23711
|
+
let last = -1;
|
|
23712
|
+
for (const h of hits) {
|
|
23713
|
+
if (h.at < last) continue;
|
|
23714
|
+
last = h.end;
|
|
23715
|
+
const lineStart2 = text.lastIndexOf("\n", h.at) + 1;
|
|
23716
|
+
const before = text.slice(lineStart2, h.at).trim();
|
|
23717
|
+
if (/^(?:\/\/|#|\*|\/\*)/.test(before)) continue;
|
|
23718
|
+
out.push({ file, line: text.slice(0, h.at).split("\n").length, kind: h.kind, index: ++count[h.kind], arg: text.slice(h.end, h.end + 200) });
|
|
23719
|
+
}
|
|
23720
|
+
return out;
|
|
23721
|
+
}
|
|
23722
|
+
function coverageOf(loaded, root, files) {
|
|
23723
|
+
const cfg = loaded.config;
|
|
23724
|
+
const toRoot = (p) => relative3(root, resolve3(loaded.dir, p)).split("\\").join("/");
|
|
23725
|
+
const calls = [];
|
|
23726
|
+
const visit = (rel2) => {
|
|
23727
|
+
if (!CODE.test(rel2) || TESTISH.test(rel2)) return;
|
|
23728
|
+
let text = "";
|
|
23729
|
+
try {
|
|
23730
|
+
text = readFileSync7(join6(root, rel2), "utf8");
|
|
23731
|
+
} catch {
|
|
23732
|
+
return;
|
|
23733
|
+
}
|
|
23734
|
+
if (!/create\s*\(/.test(text)) return;
|
|
23735
|
+
calls.push(...callsIn(text, rel2));
|
|
23736
|
+
};
|
|
23737
|
+
if (files) files.forEach(visit);
|
|
23738
|
+
else walk3(root, visit);
|
|
23739
|
+
const ignore = (cfg.coverage && typeof cfg.coverage === "object" ? cfg.coverage.ignore ?? [] : []).map((g) => glob(toRoot(g)));
|
|
23740
|
+
const wholeFiles = /* @__PURE__ */ new Set();
|
|
23741
|
+
const exactCalls = /* @__PURE__ */ new Set();
|
|
23742
|
+
const exportNames = [];
|
|
23743
|
+
const entries = [...cfg.checks.stripe_checkout ?? [], ...cfg.checks.disclosures ?? [], ...cfg.checks.pages ?? []];
|
|
23744
|
+
for (const e of cfg.checks.stripe_checkout ?? []) {
|
|
23745
|
+
if (e.source) exactCalls.add(`${toRoot(e.source)}#checkout_session#${e.call ?? 1}`);
|
|
23746
|
+
if (e.file) wholeFiles.add(toRoot(e.file));
|
|
23747
|
+
if (e.module) {
|
|
23748
|
+
const [path, name] = e.module.split("#");
|
|
23749
|
+
wholeFiles.add(toRoot(path));
|
|
23750
|
+
if (name && name !== "default") exportNames.push(name);
|
|
23751
|
+
}
|
|
23752
|
+
}
|
|
23753
|
+
for (const e of entries) {
|
|
23754
|
+
for (const c of e.covers ?? []) {
|
|
23755
|
+
const [path, n] = c.split("#");
|
|
23756
|
+
if (n) {
|
|
23757
|
+
exactCalls.add(`${toRoot(path)}#checkout_session#${n}`);
|
|
23758
|
+
exactCalls.add(`${toRoot(path)}#payment_link#${n}`);
|
|
23759
|
+
} else wholeFiles.add(toRoot(path));
|
|
23760
|
+
}
|
|
23761
|
+
}
|
|
23762
|
+
const uncovered = [];
|
|
23763
|
+
const ignored = [];
|
|
23764
|
+
for (const c of calls) {
|
|
23765
|
+
if (ignore.some((re) => re.test(c.file))) ignored.push(c);
|
|
23766
|
+
else if (wholeFiles.has(c.file) || exactCalls.has(`${c.file}#${c.kind}#${c.index}`)) continue;
|
|
23767
|
+
else if (exportNames.some((n) => new RegExp(`\\b${n.replace(/[$]/g, "\\$")}\\b`).test(c.arg))) continue;
|
|
23768
|
+
else uncovered.push(c);
|
|
23769
|
+
}
|
|
23770
|
+
return { calls, uncovered, ignored };
|
|
23771
|
+
}
|
|
23772
|
+
function coverageResults(cov) {
|
|
23773
|
+
return cov.uncovered.map((c) => {
|
|
23774
|
+
const fix = c.kind === "checkout_session" ? `Add it to checks.stripe_checkout: { "name": "\u2026", "source": "${c.file}"${c.index > 1 ? `, "call": ${c.index}` : ""} } (or "module" if the params come from a function). If it never reaches a subscriber (an admin tool, a script), add the file to "coverage": { "ignore": [...] }.` : `Payment Links are checked live from Stripe (connect Stripe in Checkbox). Say which check covers it with "covers": ["${c.file}${c.index > 1 ? `#${c.index}` : ""}"] on that entry, or add the file to "coverage": { "ignore": [...] } if it never reaches a subscriber.`;
|
|
23775
|
+
return {
|
|
23776
|
+
name: `${KIND_LABEL2[c.kind]} at ${c.file}:${c.line}`,
|
|
23777
|
+
type: "stripe_checkout",
|
|
23778
|
+
target: { file: c.file, line: c.line },
|
|
23779
|
+
status: "error",
|
|
23780
|
+
label: "Not in checkbox.json",
|
|
23781
|
+
message: `This ${KIND_LABEL2[c.kind]} call is not checked by any entry in checkbox.json, so a change to it would pass unnoticed.`,
|
|
23782
|
+
fix,
|
|
23783
|
+
fixes: [fix],
|
|
23784
|
+
warnings: []
|
|
23785
|
+
};
|
|
23786
|
+
});
|
|
23787
|
+
}
|
|
23788
|
+
var CODE, TESTISH, PATTERNS, glob, KIND_LABEL2;
|
|
23789
|
+
var init_coverage = __esm({
|
|
23790
|
+
"src/ci/coverage.ts"() {
|
|
23791
|
+
"use strict";
|
|
23792
|
+
init_scaffold();
|
|
23793
|
+
CODE = /\.(?:[cm]?[jt]sx?|py|rb|php)$/i;
|
|
23794
|
+
TESTISH = /(?:^|\/)(?:__tests__|__mocks__|tests?|spec|specs|fixtures?|mocks?|e2e|cypress|playwright|stories|storybook)(?:\/|$)|\.(?:test|spec|stories|e2e)\.[^/]+$|(?:^|\/)test_[^/]+\.py$|_test\.(?:py|rb)$|_spec\.rb$/i;
|
|
23795
|
+
PATTERNS = [
|
|
23796
|
+
[/\bcheckout\s*\.\s*sessions\s*\.\s*create\s*\(/g, "checkout_session"],
|
|
23797
|
+
// Node, Python StripeClient
|
|
23798
|
+
[/\bcheckout\s*\.\s*Session\s*\.\s*create\s*\(/g, "checkout_session"],
|
|
23799
|
+
// Python stripe.checkout.Session
|
|
23800
|
+
[/Checkout::Session\s*\.\s*create\s*\(/g, "checkout_session"],
|
|
23801
|
+
// Ruby
|
|
23802
|
+
[/checkout\s*->\s*sessions\s*->\s*create\s*\(/g, "checkout_session"],
|
|
23803
|
+
// PHP
|
|
23804
|
+
[/\bpaymentLinks\s*\.\s*create\s*\(/g, "payment_link"],
|
|
23805
|
+
// Node
|
|
23806
|
+
[/\bpayment_links\s*\.\s*create\s*\(/g, "payment_link"],
|
|
23807
|
+
// Python StripeClient
|
|
23808
|
+
[/\bPaymentLink\s*\.\s*create\s*\(/g, "payment_link"],
|
|
23809
|
+
// Python, Ruby (Stripe::PaymentLink.create)
|
|
23810
|
+
[/paymentLinks\s*->\s*create\s*\(/g, "payment_link"]
|
|
23811
|
+
// PHP
|
|
23812
|
+
];
|
|
23813
|
+
glob = (pattern) => {
|
|
23814
|
+
const p = pattern.replace(/^\.\//, "").replace(/\/$/, "/**");
|
|
23815
|
+
const re = p.split("**").map((part) => part.split("*").map((s2) => s2.replace(/[.+^${}()|[\]\\?]/g, "\\$&")).join("[^/]*")).join(".*");
|
|
23816
|
+
return new RegExp(`^${re}$`);
|
|
23817
|
+
};
|
|
23818
|
+
KIND_LABEL2 = { checkout_session: "Stripe Checkout Session", payment_link: "Stripe Payment Link" };
|
|
23819
|
+
}
|
|
23820
|
+
});
|
|
23821
|
+
|
|
23527
23822
|
// src/ci/inputs.ts
|
|
23528
23823
|
import { execFile } from "node:child_process";
|
|
23529
|
-
import { existsSync as existsSync5, readFileSync as
|
|
23530
|
-
import { dirname as dirname3, isAbsolute as isAbsolute2, join as
|
|
23824
|
+
import { existsSync as existsSync5, readFileSync as readFileSync8 } from "node:fs";
|
|
23825
|
+
import { dirname as dirname3, isAbsolute as isAbsolute2, join as join7, relative as relative4, resolve as resolve4 } from "node:path";
|
|
23531
23826
|
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
23532
23827
|
function readRel(dir, file) {
|
|
23533
|
-
const abs = isAbsolute2(file) ? file :
|
|
23828
|
+
const abs = isAbsolute2(file) ? file : resolve4(dir, file);
|
|
23534
23829
|
try {
|
|
23535
|
-
return
|
|
23830
|
+
return readFileSync8(abs, "utf8");
|
|
23536
23831
|
} catch (err) {
|
|
23537
23832
|
const code2 = err.code;
|
|
23538
23833
|
throw new InputError(code2 === "ENOENT" ? `${file} does not exist (paths are relative to checkbox.json)` : `cannot read ${file} (${code2 ?? err.message})`);
|
|
@@ -23540,7 +23835,7 @@ function readRel(dir, file) {
|
|
|
23540
23835
|
}
|
|
23541
23836
|
function hasTsx(dir) {
|
|
23542
23837
|
for (let d = dir, i = 0; i < 8; i++) {
|
|
23543
|
-
if (existsSync5(
|
|
23838
|
+
if (existsSync5(join7(d, "node_modules", "tsx", "package.json"))) return true;
|
|
23544
23839
|
const up = dirname3(d);
|
|
23545
23840
|
if (up === d) break;
|
|
23546
23841
|
d = up;
|
|
@@ -23560,7 +23855,7 @@ function runModule(dir, ref, args = [], env2 = process.env, timeoutMs = 3e4) {
|
|
|
23560
23855
|
const hash = ref.lastIndexOf("#");
|
|
23561
23856
|
const file = hash > 0 ? ref.slice(0, hash) : ref;
|
|
23562
23857
|
const name = hash > 0 ? ref.slice(hash + 1) : "default";
|
|
23563
|
-
const abs =
|
|
23858
|
+
const abs = resolve4(dir, file);
|
|
23564
23859
|
if (!existsSync5(abs)) return Promise.reject(new InputError(`${file} does not exist (paths are relative to checkbox.json)`));
|
|
23565
23860
|
const flags = loaderFlags(file, dir);
|
|
23566
23861
|
if (typeof flags === "string") return Promise.reject(new InputError(flags));
|
|
@@ -23602,7 +23897,7 @@ function projectEnv(dir) {
|
|
|
23602
23897
|
const out = {};
|
|
23603
23898
|
for (const f of [".env.example", ".env", ".env.local", ".env.test"]) {
|
|
23604
23899
|
try {
|
|
23605
|
-
Object.assign(out, parseDotenv(
|
|
23900
|
+
Object.assign(out, parseDotenv(readFileSync8(join7(dir, f), "utf8")));
|
|
23606
23901
|
} catch {
|
|
23607
23902
|
}
|
|
23608
23903
|
}
|
|
@@ -23628,7 +23923,7 @@ function planTerms(config, plan2) {
|
|
|
23628
23923
|
return t;
|
|
23629
23924
|
}
|
|
23630
23925
|
function rel(loaded, root, file) {
|
|
23631
|
-
return
|
|
23926
|
+
return relative4(root, resolve4(loaded.dir, file)).split("\\").join("/") || file;
|
|
23632
23927
|
}
|
|
23633
23928
|
function emailFromFile(content, file, subject) {
|
|
23634
23929
|
let body = content;
|
|
@@ -23857,7 +24152,8 @@ function annotations(run2) {
|
|
|
23857
24152
|
const level = x.status === "fail" || x.status === "error" ? "error" : x.status === "needs_review" ? run2.strict ? "error" : "warning" : void 0;
|
|
23858
24153
|
if (!level) continue;
|
|
23859
24154
|
const props = [`file=${escProp(x.target.file)}`, ...x.target.line ? [`line=${x.target.line}`] : [], `title=${escProp(`Checkbox: ${x.name} \u2014 ${x.label}`)}`];
|
|
23860
|
-
const body = x.status === "error" ?
|
|
24155
|
+
const body = x.status === "error" ? x.fix ? `${x.message ?? ""}
|
|
24156
|
+
${x.fix}` : `Could not run: ${x.message ?? ""}` : [x.fix ?? x.label, ...x.fixes.slice(1, 4).map((f) => `\u2022 ${f}`), x.corrected ? `${x.corrected.title}: see the job summary.` : ""].filter(Boolean).join("\n");
|
|
23861
24157
|
out.push(`::${level} ${props.join(",")}::${escData(body)}`);
|
|
23862
24158
|
}
|
|
23863
24159
|
return out;
|
|
@@ -23869,7 +24165,7 @@ function stepSummary(run2) {
|
|
|
23869
24165
|
lines.push(`**${overallLine(run2)}** \u2014 \`${run2.config}\` \xB7 ${run2.mode}${versions}`, "");
|
|
23870
24166
|
lines.push("| Check | Status | Fix | File |", "|---|---|---|---|");
|
|
23871
24167
|
for (const x of run2.checks) {
|
|
23872
|
-
const fix = x.status === "error" || x.status === "skipped" ? x.message
|
|
24168
|
+
const fix = x.status === "error" || x.status === "skipped" ? [x.message, x.fix].filter(Boolean).join(" ") : x.fix ? `${x.fix}${x.fixes.length > 1 ? ` (+${x.fixes.length - 1} more)` : ""}` : "";
|
|
23873
24169
|
lines.push(`| ${STATUS_MD[x.status]} ${cell(x.name)}<br><sub>${TYPE_LABEL[x.type]}</sub> | ${cell(x.label)} | ${cell(fix)} | \`${cell(where(x))}\` |`);
|
|
23874
24170
|
}
|
|
23875
24171
|
const detailed = run2.checks.filter((x) => x.corrected || x.fixes.length > 1);
|
|
@@ -24441,7 +24737,7 @@ function exitCodeOf(checks, strict) {
|
|
|
24441
24737
|
return { status: "pass", exit: EXIT.ok };
|
|
24442
24738
|
}
|
|
24443
24739
|
async function runCi(jobs, engine, opts) {
|
|
24444
|
-
const checks = [];
|
|
24740
|
+
const checks = [...opts.extra ?? []];
|
|
24445
24741
|
for (const j of jobs) checks.push(await runJob(j, engine, opts.config));
|
|
24446
24742
|
const summary = { pass: 0, fail: 0, needs_review: 0, not_applicable: 0, skipped: 0, error: 0 };
|
|
24447
24743
|
for (const c of checks) summary[c.status]++;
|
|
@@ -24498,168 +24794,6 @@ var init_runner = __esm({
|
|
|
24498
24794
|
}
|
|
24499
24795
|
});
|
|
24500
24796
|
|
|
24501
|
-
// src/ci/scaffold.ts
|
|
24502
|
-
import { readdirSync as readdirSync3, readFileSync as readFileSync7, statSync as statSync2 } from "node:fs";
|
|
24503
|
-
import { basename, join as join6, relative as relative3 } from "node:path";
|
|
24504
|
-
function walk3(root, visit) {
|
|
24505
|
-
let n = 0;
|
|
24506
|
-
const go = (dir, depth) => {
|
|
24507
|
-
if (depth > 8 || n > MAX_FILES2) return;
|
|
24508
|
-
let names = [];
|
|
24509
|
-
try {
|
|
24510
|
-
names = readdirSync3(dir).sort();
|
|
24511
|
-
} catch {
|
|
24512
|
-
return;
|
|
24513
|
-
}
|
|
24514
|
-
for (const name of names) {
|
|
24515
|
-
if (IGNORE.has(name) || name.startsWith(".") && name !== ".well-known") continue;
|
|
24516
|
-
const abs = join6(dir, name);
|
|
24517
|
-
let st;
|
|
24518
|
-
try {
|
|
24519
|
-
st = statSync2(abs);
|
|
24520
|
-
} catch {
|
|
24521
|
-
continue;
|
|
24522
|
-
}
|
|
24523
|
-
if (st.isDirectory()) go(abs, depth + 1);
|
|
24524
|
-
else if (st.isFile() && st.size < 4e5) {
|
|
24525
|
-
n++;
|
|
24526
|
-
visit(relative3(root, abs).split("\\").join("/"));
|
|
24527
|
-
}
|
|
24528
|
-
}
|
|
24529
|
-
};
|
|
24530
|
-
go(root, 0);
|
|
24531
|
-
}
|
|
24532
|
-
function emailKindOf(file) {
|
|
24533
|
-
const name = basename(file).replace(/\.[^.]+$/, "");
|
|
24534
|
-
return EMAIL_NAMES.find(([re]) => re.test(name))?.[1];
|
|
24535
|
-
}
|
|
24536
|
-
function scaffoldConfig(root, env2 = {}) {
|
|
24537
|
-
const det = detectProject(root, env2);
|
|
24538
|
-
const todos = [];
|
|
24539
|
-
const found = [];
|
|
24540
|
-
const names = /* @__PURE__ */ new Set();
|
|
24541
|
-
const merchant = {};
|
|
24542
|
-
merchant.name = det.brand?.name ?? "TODO: the brand subscribers see";
|
|
24543
|
-
if (!det.brand) todos.push("merchant.name: the brand subscribers see");
|
|
24544
|
-
const cancelPath = det.policyPaths.cancellation;
|
|
24545
|
-
if (det.siteUrl && cancelPath) merchant.cancel_url = `${originOf(det.siteUrl)}${cancelPath}`;
|
|
24546
|
-
else {
|
|
24547
|
-
merchant.cancel_url = "TODO: https://\u2026 page where subscribers cancel online";
|
|
24548
|
-
todos.push("merchant.cancel_url: the page where subscribers cancel online");
|
|
24549
|
-
}
|
|
24550
|
-
const plans = det.plans.map((p, i) => ({
|
|
24551
|
-
name: p.name ?? `Plan ${i + 1}`,
|
|
24552
|
-
amount: p.amount,
|
|
24553
|
-
currency: p.currency,
|
|
24554
|
-
interval: p.interval,
|
|
24555
|
-
...p.interval_count ? { interval_count: p.interval_count } : {},
|
|
24556
|
-
...p.trial_days ? { trial_days: p.trial_days } : {}
|
|
24557
|
-
}));
|
|
24558
|
-
for (const p of det.plans) found.push(`plan ${p.name ?? ""} ${p.amount / 100} ${p.currency}/${p.interval} (${p.source})`.replace(/\s+/g, " "));
|
|
24559
|
-
if (!plans.length) {
|
|
24560
|
-
plans.push({ name: "TODO: plan name", price: "TODO: e.g. 19.99", currency: "usd", interval: "month" });
|
|
24561
|
-
todos.push("plans: the price, interval and trial of each plan (the checkout uses Price ids, which cannot be read from code)");
|
|
24562
|
-
}
|
|
24563
|
-
const planNames = plans.map((p) => String(p.name));
|
|
24564
|
-
const planFor = () => plans.length > 1 ? { plan: `TODO: one of ${planNames.map((n) => JSON.stringify(n)).join(", ")}` } : {};
|
|
24565
|
-
if (plans.length > 1) todos.push("plan: pick the plan of each disclosure, page and email entry");
|
|
24566
|
-
const stripe = [];
|
|
24567
|
-
const perFile = /* @__PURE__ */ new Map();
|
|
24568
|
-
for (const s2 of det.checkoutSessions) {
|
|
24569
|
-
const call = (perFile.get(s2.file) ?? 0) + 1;
|
|
24570
|
-
perFile.set(s2.file, call);
|
|
24571
|
-
const entry = { name: unique(names, `Stripe Checkout (${basename(s2.file)}${call > 1 ? ` #${call}` : ""})`), source: s2.file, ...call > 1 ? { call } : {} };
|
|
24572
|
-
if (!s2.params) {
|
|
24573
|
-
todos.push(`${s2.file}:${s2.line}: the params are not an object literal; use "module" (an exported function returning the params) instead of "source"`);
|
|
24574
|
-
delete entry.source;
|
|
24575
|
-
delete entry.call;
|
|
24576
|
-
entry.module = `TODO: ${s2.file.replace(/\.[^.]+$/, "")}.ts#buildCheckoutParams`;
|
|
24577
|
-
} else if (s2.unresolved.length) found.push(`${s2.file}:${s2.line}: Checkout Session (${s2.unresolved.length} value(s) are expressions, read as-is)`);
|
|
24578
|
-
else found.push(`${s2.file}:${s2.line}: Checkout Session`);
|
|
24579
|
-
stripe.push(entry);
|
|
24580
|
-
}
|
|
24581
|
-
const emails = [];
|
|
24582
|
-
const pages = [];
|
|
24583
|
-
const disclosures = [];
|
|
24584
|
-
walk3(root, (rel2) => {
|
|
24585
|
-
if (IN_EMAIL_DIR.test(rel2)) {
|
|
24586
|
-
const kind = emailKindOf(rel2);
|
|
24587
|
-
if (/\.(?:html?|txt|md)$/i.test(rel2) && kind) {
|
|
24588
|
-
emails.push({ name: unique(names, `Email: ${basename(rel2)}`), kind, file: rel2, ...planFor() });
|
|
24589
|
-
found.push(`${rel2}: ${kind} email`);
|
|
24590
|
-
return;
|
|
24591
|
-
}
|
|
24592
|
-
if (/\.[jt]sx$|\.mjml$/i.test(rel2) && kind) {
|
|
24593
|
-
todos.push(`${rel2}: a ${kind} email component; add it with "module": "<file>#<export>" returning the rendered { subject, html } (React Email: render() with sample props)`);
|
|
24594
|
-
return;
|
|
24595
|
-
}
|
|
24596
|
-
}
|
|
24597
|
-
if (PAGE_FILE.test(rel2)) {
|
|
24598
|
-
const kind = /checkout|subscribe/i.test(rel2) ? "checkout" : "pricing";
|
|
24599
|
-
pages.push({ name: unique(names, `Page: ${rel2}`), file: rel2, kind, ...planFor() });
|
|
24600
|
-
found.push(`${rel2}: ${kind} page`);
|
|
24601
|
-
return;
|
|
24602
|
-
}
|
|
24603
|
-
if (/\.(?:txt|md)$/i.test(rel2) && !/(?:^|\/)(?:README|CHANGELOG|LICENSE|CONTRIBUTING|AGENTS|CLAUDE)[^/]*$/i.test(rel2) && !rel2.startsWith("docs/")) {
|
|
24604
|
-
let text = "";
|
|
24605
|
-
try {
|
|
24606
|
-
text = readFileSync7(join6(root, rel2), "utf8");
|
|
24607
|
-
} catch {
|
|
24608
|
-
return;
|
|
24609
|
-
}
|
|
24610
|
-
if (text.length < 1500 && DISCLOSURE_TEXT.test(text)) {
|
|
24611
|
-
disclosures.push({ name: unique(names, `Disclosure: ${basename(rel2)}`), file: rel2, ...planFor() });
|
|
24612
|
-
found.push(`${rel2}: disclosure text`);
|
|
24613
|
-
}
|
|
24614
|
-
}
|
|
24615
|
-
});
|
|
24616
|
-
if (!disclosures.length && det.payButtons.length) {
|
|
24617
|
-
todos.push(`The renewal disclosure lives in code (${det.payButtons[0].file}); keep its text in a .txt file the component imports and add it under checks.disclosures, or check the rendered page under checks.pages`);
|
|
24618
|
-
}
|
|
24619
|
-
const checks = {};
|
|
24620
|
-
if (stripe.length) checks.stripe_checkout = stripe;
|
|
24621
|
-
if (disclosures.length) checks.disclosures = disclosures;
|
|
24622
|
-
if (pages.length) checks.pages = pages;
|
|
24623
|
-
if (emails.length) checks.emails = emails;
|
|
24624
|
-
checks.notice_schedule = true;
|
|
24625
|
-
if (!stripe.length && !disclosures.length && !pages.length && !emails.length) {
|
|
24626
|
-
todos.push('No checkout code, email templates or pages were found: add entries under "checks" (https://checkbox.site/docs/guides/ci#config)');
|
|
24627
|
-
}
|
|
24628
|
-
return {
|
|
24629
|
-
config: { $schema: SCHEMA_URL, merchant, jurisdictions: "all", plans, checks },
|
|
24630
|
-
todos,
|
|
24631
|
-
found
|
|
24632
|
-
};
|
|
24633
|
-
}
|
|
24634
|
-
var IGNORE, MAX_FILES2, EMAIL_NAMES, IN_EMAIL_DIR, PAGE_FILE, DISCLOSURE_TEXT, unique;
|
|
24635
|
-
var init_scaffold = __esm({
|
|
24636
|
-
"src/ci/scaffold.ts"() {
|
|
24637
|
-
"use strict";
|
|
24638
|
-
init_detect();
|
|
24639
|
-
init_config3();
|
|
24640
|
-
IGNORE = /* @__PURE__ */ new Set(["node_modules", ".git", ".next", ".nuxt", ".svelte-kit", ".output", ".vercel", ".netlify", ".turbo", ".cache", "coverage", ".astro", "vendor", ".venv", "__pycache__", ".github"]);
|
|
24641
|
-
MAX_FILES2 = 5e3;
|
|
24642
|
-
EMAIL_NAMES = [
|
|
24643
|
-
[/trial/i, "trial_reminder"],
|
|
24644
|
-
[/price[-_ ]?(?:change|increase|update)/i, "price_change"],
|
|
24645
|
-
[/annual|yearly[-_ ]?terms/i, "annual_terms"],
|
|
24646
|
-
[/renew/i, "renewal_reminder"],
|
|
24647
|
-
[/cancel/i, "cancellation_confirmation"],
|
|
24648
|
-
[/receipt|charge|invoice|payment[-_ ]?(?:succeeded|receipt)/i, "charge_notice"],
|
|
24649
|
-
[/welcome|confirm|acknowledg|subscribed|signup|sign[-_]up|order/i, "acknowledgment"]
|
|
24650
|
-
];
|
|
24651
|
-
IN_EMAIL_DIR = /(?:^|\/)(?:e-?mails?|mail(?:ers?)?|notifications?)(?:\/|$)|(?:^|\/)templates\/(?:e-?mails?|mail)\//i;
|
|
24652
|
-
PAGE_FILE = /(?:^|\/)(?:public|dist|out|build|static|_site|www)\/(?:.*\/)?(pricing|checkout|subscribe|plans)(?:\/index)?\.html?$/i;
|
|
24653
|
-
DISCLOSURE_TEXT = /renews? automatically|automatically renews?|auto-?renew|until (?:you )?cancel/i;
|
|
24654
|
-
unique = (names, base) => {
|
|
24655
|
-
let n = base;
|
|
24656
|
-
for (let i = 2; names.has(n); i++) n = `${base} ${i}`;
|
|
24657
|
-
names.add(n);
|
|
24658
|
-
return n;
|
|
24659
|
-
};
|
|
24660
|
-
}
|
|
24661
|
-
});
|
|
24662
|
-
|
|
24663
24797
|
// src/ci/workflow.ts
|
|
24664
24798
|
var WORKFLOW_PATH, WORKFLOW_YAML;
|
|
24665
24799
|
var init_workflow = __esm({
|
|
@@ -24706,12 +24840,12 @@ __export(ci_exports, {
|
|
|
24706
24840
|
import { execFileSync } from "node:child_process";
|
|
24707
24841
|
import { appendFileSync, existsSync as existsSync6, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
24708
24842
|
import { createInterface } from "node:readline";
|
|
24709
|
-
import { dirname as dirname4, join as
|
|
24843
|
+
import { dirname as dirname4, join as join8, relative as relative5, resolve as resolve5 } from "node:path";
|
|
24710
24844
|
function gitChanged(cwd, base) {
|
|
24711
24845
|
try {
|
|
24712
24846
|
const root = execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
24713
24847
|
const out = execFileSync("git", ["diff", "--name-only", `${base}...HEAD`], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
24714
|
-
return { root, files: out.split("\n").map((l) => l.trim()).filter(Boolean).map((f) =>
|
|
24848
|
+
return { root, files: out.split("\n").map((l) => l.trim()).filter(Boolean).map((f) => resolve5(root, f)) };
|
|
24715
24849
|
} catch (err) {
|
|
24716
24850
|
const msg2 = String(err.stderr || err.message).trim().split("\n")[0];
|
|
24717
24851
|
return `git diff ${base}...HEAD failed (${msg2}); fetch the base branch (actions/checkout with fetch-depth: 0)`;
|
|
@@ -24722,7 +24856,7 @@ function touchesConfig(loaded, jobs, changed) {
|
|
|
24722
24856
|
if (set.has(loaded.path)) return true;
|
|
24723
24857
|
const codeChanged = changed.some((f) => /\.(?:[cm]?[jt]sx?)$/.test(f));
|
|
24724
24858
|
for (const j of jobs) {
|
|
24725
|
-
for (const f of j.inputs) if (set.has(
|
|
24859
|
+
for (const f of j.inputs) if (set.has(resolve5(loaded.dir, f))) return true;
|
|
24726
24860
|
}
|
|
24727
24861
|
const hasModules = [...loaded.config.checks.stripe_checkout ?? [], ...loaded.config.checks.emails ?? []].some((e) => "module" in e && e.module);
|
|
24728
24862
|
return hasModules && codeChanged;
|
|
@@ -24747,8 +24881,8 @@ async function runInit(ctx, argv) {
|
|
|
24747
24881
|
const p = parseArgs(argv, { boolean: ["yes", "force", "dry-run", "workflow"], string: ["config"], alias: { y: "yes" } });
|
|
24748
24882
|
if (p.positionals.length) throw new UsageError(`Unexpected argument "${p.positionals[0]}"`);
|
|
24749
24883
|
const explicit = str(p.flags, "config");
|
|
24750
|
-
const configPath = explicit ?
|
|
24751
|
-
const workflowPath =
|
|
24884
|
+
const configPath = explicit ? resolve5(ctx.cwd, explicit) : CONFIG_FILES.map((f) => join8(ctx.cwd, f)).find((f) => existsSync6(f)) ?? join8(ctx.cwd, CONFIG_FILES[0]);
|
|
24885
|
+
const workflowPath = join8(ctx.cwd, WORKFLOW_PATH);
|
|
24752
24886
|
const force = bool(p.flags, "force") === true;
|
|
24753
24887
|
const withWorkflow = bool(p.flags, "workflow") !== false;
|
|
24754
24888
|
const s2 = scaffoldConfig(ctx.cwd, ctx.env);
|
|
@@ -24761,7 +24895,7 @@ async function runInit(ctx, argv) {
|
|
|
24761
24895
|
let write = !dry && toWrite.length > 0;
|
|
24762
24896
|
if (write && bool(p.flags, "yes") !== true) {
|
|
24763
24897
|
if (process.stdin.isTTY && !ctx.flags.json) {
|
|
24764
|
-
for (const f of toWrite) ctx.out.info(`${f.action === "create" ? "create" : "overwrite"} ${
|
|
24898
|
+
for (const f of toWrite) ctx.out.info(`${f.action === "create" ? "create" : "overwrite"} ${relative5(ctx.cwd, f.path)}`);
|
|
24765
24899
|
write = await confirm("Write these files? [Y/n] ");
|
|
24766
24900
|
} else write = false;
|
|
24767
24901
|
}
|
|
@@ -24771,7 +24905,7 @@ async function runInit(ctx, argv) {
|
|
|
24771
24905
|
writeFileSync2(f.path, f.content);
|
|
24772
24906
|
}
|
|
24773
24907
|
}
|
|
24774
|
-
const relName = (f) =>
|
|
24908
|
+
const relName = (f) => relative5(ctx.cwd, f) || f;
|
|
24775
24909
|
const result = {
|
|
24776
24910
|
object: "ci_init",
|
|
24777
24911
|
written: write ? toWrite.map((f) => relName(f.path)) : [],
|
|
@@ -24800,6 +24934,7 @@ var init_ci = __esm({
|
|
|
24800
24934
|
"use strict";
|
|
24801
24935
|
init_args();
|
|
24802
24936
|
init_config3();
|
|
24937
|
+
init_coverage();
|
|
24803
24938
|
init_inputs();
|
|
24804
24939
|
init_report();
|
|
24805
24940
|
init_runner();
|
|
@@ -24858,21 +24993,26 @@ var init_ci = __esm({
|
|
|
24858
24993
|
loaded = loadConfig(path);
|
|
24859
24994
|
} catch (err) {
|
|
24860
24995
|
if (!(err instanceof ConfigError)) throw err;
|
|
24861
|
-
const file =
|
|
24996
|
+
const file = relative5(ctx.env.GITHUB_WORKSPACE ?? ctx.cwd, path) || path;
|
|
24862
24997
|
if (gha) for (const i of err.issues) emit(`::error file=${file},title=Checkbox config::${i.replace(/%/g, "%25").replace(/\n/g, "%0A")}`);
|
|
24863
|
-
ctx.out.error({ message: `${
|
|
24998
|
+
ctx.out.error({ message: `${relative5(ctx.cwd, path) || path} is not valid:
|
|
24864
24999
|
- ${err.issues.join("\n - ")}`, code: "config_invalid", type: "usage_error", issues: err.issues, next_action: { type: "edit_config", message: "Fix the keys listed above (reference: https://checkbox.site/docs/guides/ci#config)." } });
|
|
24865
25000
|
return EXIT.usage;
|
|
24866
25001
|
}
|
|
24867
|
-
const configName =
|
|
25002
|
+
const configName = relative5(ctx.cwd, path) || path;
|
|
24868
25003
|
const jobs = await resolveJobs(loaded, { online, env: ctx.env, root: ctx.env.GITHUB_WORKSPACE ?? ctx.cwd });
|
|
25004
|
+
const workspace = ctx.env.GITHUB_WORKSPACE ?? ctx.cwd;
|
|
25005
|
+
const extra = (loaded.config.coverage === false ? [] : coverageResults(coverageOf(loaded, loaded.dir))).map((r) => ({
|
|
25006
|
+
...r,
|
|
25007
|
+
target: { ...r.target, file: relative5(workspace, join8(loaded.dir, r.target.file)).split("\\").join("/") || r.target.file }
|
|
25008
|
+
}));
|
|
24869
25009
|
if (bool(p.flags, "changed-only")) {
|
|
24870
25010
|
const base = str(p.flags, "base") ?? (ctx.env.GITHUB_BASE_REF ? `origin/${ctx.env.GITHUB_BASE_REF}` : void 0);
|
|
24871
25011
|
if (!base) ctx.out.info("--changed-only: no --base and not a pull request (GITHUB_BASE_REF); running every check.");
|
|
24872
25012
|
else {
|
|
24873
25013
|
const changed = gitChanged(ctx.cwd, base);
|
|
24874
25014
|
if (typeof changed === "string") ctx.out.warn(`${changed}; running every check.`);
|
|
24875
|
-
else if (!touchesConfig(loaded, jobs, changed.files)) {
|
|
25015
|
+
else if (!extra.length && !touchesConfig(loaded, jobs, changed.files)) {
|
|
24876
25016
|
const message = `No file checkbox.json reads changed since ${base}: ${jobs.length} check(s) skipped.`;
|
|
24877
25017
|
if (gha) {
|
|
24878
25018
|
writeGithubFile(ctx.env.GITHUB_STEP_SUMMARY, `## Checkbox compliance checks
|
|
@@ -24887,7 +25027,7 @@ ${message}
|
|
|
24887
25027
|
}
|
|
24888
25028
|
}
|
|
24889
25029
|
const engine = online ? onlineEngine(ctx.client()) : offlineEngine();
|
|
24890
|
-
const run2 = await runCi(jobs, engine, { config: loaded.config, configPath: configName, strict });
|
|
25030
|
+
const run2 = await runCi(jobs, engine, { config: loaded.config, configPath: configName, strict, extra });
|
|
24891
25031
|
ctx.out.result(run2, () => renderHuman(run2, ctx.out.c, { verbose: bool(p.flags, "verbose") === true }));
|
|
24892
25032
|
if (gha) {
|
|
24893
25033
|
for (const a of annotations(run2)) emit(a);
|
|
@@ -25119,8 +25259,8 @@ var init_patch = __esm({
|
|
|
25119
25259
|
});
|
|
25120
25260
|
|
|
25121
25261
|
// src/commands/plan/fix.ts
|
|
25122
|
-
import { existsSync as existsSync7, readFileSync as
|
|
25123
|
-
import { relative as
|
|
25262
|
+
import { existsSync as existsSync7, readFileSync as readFileSync9, writeFileSync as writeFileSync3 } from "node:fs";
|
|
25263
|
+
import { relative as relative6, resolve as resolve6 } from "node:path";
|
|
25124
25264
|
function fixForTask(task, cwd, file) {
|
|
25125
25265
|
const base = {
|
|
25126
25266
|
object: "task_fix",
|
|
@@ -25132,17 +25272,17 @@ function fixForTask(task, cwd, file) {
|
|
|
25132
25272
|
verify: task.verify
|
|
25133
25273
|
};
|
|
25134
25274
|
const verifyCmd = `checkbox verify ${task.id} --wait`;
|
|
25135
|
-
const candidate = file ?? (task.where.file_hint && existsSync7(
|
|
25275
|
+
const candidate = file ?? (task.where.file_hint && existsSync7(resolve6(cwd, task.where.file_hint)) ? task.where.file_hint : void 0);
|
|
25136
25276
|
if (task.change.type === "patch" && task.change.edits.length && candidate) {
|
|
25137
|
-
const path =
|
|
25277
|
+
const path = resolve6(cwd, candidate);
|
|
25138
25278
|
let src;
|
|
25139
25279
|
try {
|
|
25140
|
-
src =
|
|
25280
|
+
src = readFileSync9(path, "utf8");
|
|
25141
25281
|
} catch (err) {
|
|
25142
25282
|
throw new UsageError(`Cannot read ${candidate}: ${err.code ?? err.message}`);
|
|
25143
25283
|
}
|
|
25144
25284
|
const r = applyEdits(src, task.change.edits, { jsx: isJsxFile(candidate) });
|
|
25145
|
-
const rel2 =
|
|
25285
|
+
const rel2 = relative6(cwd, path).split("\\").join("/");
|
|
25146
25286
|
const diff2 = unifiedDiff(src, r.patched, rel2);
|
|
25147
25287
|
const complete = r.unmatched.length === 0;
|
|
25148
25288
|
return {
|
|
@@ -25197,7 +25337,7 @@ var init_fix = __esm({
|
|
|
25197
25337
|
const task = await ctx.client().sdk.tasks.retrieve(id);
|
|
25198
25338
|
const result = fixForTask(task, ctx.cwd, str(flags, "file"));
|
|
25199
25339
|
const out = str(flags, "out");
|
|
25200
|
-
if (out && result.diff) writeFileSync3(
|
|
25340
|
+
if (out && result.diff) writeFileSync3(resolve6(ctx.cwd, out), result.diff);
|
|
25201
25341
|
ctx.out.result({ ...result, task_object: task }, () => {
|
|
25202
25342
|
const c = ctx.out.c;
|
|
25203
25343
|
const lines = [`${c.bold(task.title)} ${c.dim(`(${task.priority}, ${task.id})`)}`, c.dim(`${task.requirement.citation}`), task.why, ""];
|
|
@@ -25299,11 +25439,11 @@ var init_device = __esm({
|
|
|
25299
25439
|
});
|
|
25300
25440
|
|
|
25301
25441
|
// src/commands/init/patch.ts
|
|
25302
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as
|
|
25303
|
-
import { dirname as dirname5, join as
|
|
25442
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync10, writeFileSync as writeFileSync4 } from "node:fs";
|
|
25443
|
+
import { dirname as dirname5, join as join9 } from "node:path";
|
|
25304
25444
|
function read(root, rel2) {
|
|
25305
25445
|
try {
|
|
25306
|
-
return
|
|
25446
|
+
return readFileSync10(join9(root, rel2), "utf8");
|
|
25307
25447
|
} catch {
|
|
25308
25448
|
return null;
|
|
25309
25449
|
}
|
|
@@ -25466,7 +25606,7 @@ function planGitignore(d) {
|
|
|
25466
25606
|
const covered = lines.some((l) => l === env2 || l === `/${env2}` || l === ".env*" || l === "*.env" || env2 === ".env.local" && (l === ".env*.local" || l === "*.local" || l === ".env.local*") || env2 === ".env" && l === ".env*");
|
|
25467
25607
|
if (covered) return null;
|
|
25468
25608
|
}
|
|
25469
|
-
if (existing === null && !existsSync8(
|
|
25609
|
+
if (existing === null && !existsSync8(join9(d.root, ".git"))) return null;
|
|
25470
25610
|
const text = `${existing ?? ""}${existing && !existing.endsWith("\n") ? "\n" : ""}# Checkbox keys (checkbox init)
|
|
25471
25611
|
${env2}
|
|
25472
25612
|
`;
|
|
@@ -25482,7 +25622,7 @@ function planWellKnown(d, token) {
|
|
|
25482
25622
|
}
|
|
25483
25623
|
function applyChange(root, c) {
|
|
25484
25624
|
if (c.action === "unchanged" || c.contents === void 0) return;
|
|
25485
|
-
const abs =
|
|
25625
|
+
const abs = join9(root, c.file);
|
|
25486
25626
|
mkdirSync3(dirname5(abs), { recursive: true });
|
|
25487
25627
|
writeFileSync4(abs, c.contents, c.file.startsWith(".env") ? { mode: 384 } : void 0);
|
|
25488
25628
|
}
|
|
@@ -25545,7 +25685,7 @@ function saveState(ctx, root, patch) {
|
|
|
25545
25685
|
async function ask2(question) {
|
|
25546
25686
|
const rl = createInterface2({ input: process.stdin, output: process.stderr, terminal: true });
|
|
25547
25687
|
try {
|
|
25548
|
-
return await new Promise((
|
|
25688
|
+
return await new Promise((resolve9) => rl.question(question, resolve9));
|
|
25549
25689
|
} finally {
|
|
25550
25690
|
rl.close();
|
|
25551
25691
|
}
|
|
@@ -26092,7 +26232,7 @@ import { createInterface as createInterface3 } from "node:readline";
|
|
|
26092
26232
|
async function prompt(question) {
|
|
26093
26233
|
const rl = createInterface3({ input: process.stdin, output: process.stderr, terminal: true });
|
|
26094
26234
|
try {
|
|
26095
|
-
return await new Promise((
|
|
26235
|
+
return await new Promise((resolve9) => rl.question(question, resolve9));
|
|
26096
26236
|
} finally {
|
|
26097
26237
|
rl.close();
|
|
26098
26238
|
}
|
|
@@ -26187,7 +26327,7 @@ var init_logout = __esm({
|
|
|
26187
26327
|
});
|
|
26188
26328
|
|
|
26189
26329
|
// src/commands/plan/lib.ts
|
|
26190
|
-
import { dirname as dirname6, resolve as
|
|
26330
|
+
import { dirname as dirname6, resolve as resolve7 } from "node:path";
|
|
26191
26331
|
async function resolveSite(ctx, flag, client) {
|
|
26192
26332
|
const explicit = flag ?? ctx.env.CHECKBOX_SITE;
|
|
26193
26333
|
if (explicit) {
|
|
@@ -26195,7 +26335,7 @@ async function resolveSite(ctx, flag, client) {
|
|
|
26195
26335
|
return explicit;
|
|
26196
26336
|
}
|
|
26197
26337
|
const projects = ctx.config.load().init?.projects ?? {};
|
|
26198
|
-
for (let dir =
|
|
26338
|
+
for (let dir = resolve7(ctx.cwd); ; dir = dirname6(dir)) {
|
|
26199
26339
|
const site = projects[dir]?.site;
|
|
26200
26340
|
if (site) return site;
|
|
26201
26341
|
if (dirname6(dir) === dir) break;
|
|
@@ -26294,11 +26434,11 @@ var init_plan = __esm({
|
|
|
26294
26434
|
});
|
|
26295
26435
|
|
|
26296
26436
|
// src/commands/plan/verify.ts
|
|
26297
|
-
import { readFileSync as
|
|
26298
|
-
import { resolve as
|
|
26437
|
+
import { readFileSync as readFileSync11 } from "node:fs";
|
|
26438
|
+
import { resolve as resolve8 } from "node:path";
|
|
26299
26439
|
function readJson2(ctx, file, what) {
|
|
26300
26440
|
try {
|
|
26301
|
-
const v = JSON.parse(
|
|
26441
|
+
const v = JSON.parse(readFileSync11(resolve8(ctx.cwd, file), "utf8"));
|
|
26302
26442
|
if (!v || typeof v !== "object" || Array.isArray(v)) throw new Error("not a JSON object");
|
|
26303
26443
|
return v;
|
|
26304
26444
|
} catch (err) {
|
|
@@ -26344,7 +26484,7 @@ var init_verify2 = __esm({
|
|
|
26344
26484
|
const htmlFile = str(flags, "html-file");
|
|
26345
26485
|
if (htmlFile) {
|
|
26346
26486
|
try {
|
|
26347
|
-
body.html =
|
|
26487
|
+
body.html = readFileSync11(resolve8(ctx.cwd, htmlFile), "utf8");
|
|
26348
26488
|
} catch (err) {
|
|
26349
26489
|
throw new UsageError(`Cannot read ${htmlFile}: ${err.code ?? err.message}`);
|
|
26350
26490
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkbox-site/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "The checkbox CLI: automatic-renewal law checks (ROSCA, California ARL, New York and other states) for subscription checkouts, disclosures and emails, from a terminal, CI or an AI coding agent. Runs offline.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"subscription",
|