@kinosec/cli 0.10.3 → 0.11.1
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/cli.js +278 -7
- package/dist/cli.js.map +1 -1
- package/dist/vendor/README.md +134 -0
- package/dist/vendor/ctxpack.py +701 -0
- package/package.json +2 -1
- package/vendor/README.md +134 -0
- package/vendor/ctxpack.py +701 -0
package/dist/cli.js
CHANGED
|
@@ -6732,10 +6732,11 @@ function parseApiError(body, status) {
|
|
|
6732
6732
|
});
|
|
6733
6733
|
}
|
|
6734
6734
|
const b = body;
|
|
6735
|
+
const detail = typeof b.detail === "string" ? b.detail : typeof b.error === "string" ? b.error : "An unexpected error occurred";
|
|
6735
6736
|
return new ApiError({
|
|
6736
6737
|
status: typeof b.status === "number" ? b.status : status,
|
|
6737
6738
|
code: typeof b.code === "string" ? b.code : "unknown_error",
|
|
6738
|
-
detail
|
|
6739
|
+
detail,
|
|
6739
6740
|
requestId: typeof b.request_id === "string" ? b.request_id : "unknown"
|
|
6740
6741
|
});
|
|
6741
6742
|
}
|
|
@@ -8435,7 +8436,7 @@ ${import_picocolors3.default.dim("use of its services.")}
|
|
|
8435
8436
|
var import_picocolors4 = __toESM(require_picocolors(), 1);
|
|
8436
8437
|
|
|
8437
8438
|
// src/lib/validate-mode.ts
|
|
8438
|
-
var VALID_SCAN_MODES = ["full", "quick", "htb", "defcon"];
|
|
8439
|
+
var VALID_SCAN_MODES = ["full", "quick", "htb", "defcon", "exploit-only"];
|
|
8439
8440
|
function validateScanMode(mode) {
|
|
8440
8441
|
if (!VALID_SCAN_MODES.includes(mode)) {
|
|
8441
8442
|
console.error(`Invalid mode "${mode}". Must be one of: ${VALID_SCAN_MODES.join(", ")}.`);
|
|
@@ -12561,16 +12562,228 @@ function parseTarget(input) {
|
|
|
12561
12562
|
};
|
|
12562
12563
|
}
|
|
12563
12564
|
|
|
12565
|
+
// src/lib/graybox.ts
|
|
12566
|
+
import { spawnSync } from "child_process";
|
|
12567
|
+
import { existsSync as existsSync3, mkdtempSync, readFileSync as readFileSync3, rmSync, statSync } from "fs";
|
|
12568
|
+
import { tmpdir } from "os";
|
|
12569
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
12570
|
+
import { fileURLToPath } from "url";
|
|
12571
|
+
var SOURCE_BYTES_WARN = 2e5;
|
|
12572
|
+
function findPython() {
|
|
12573
|
+
const candidates = [
|
|
12574
|
+
{ cmd: "python3", args: [] },
|
|
12575
|
+
{ cmd: "py", args: ["-3"] },
|
|
12576
|
+
{ cmd: "python", args: [] }
|
|
12577
|
+
];
|
|
12578
|
+
for (const interp of candidates) {
|
|
12579
|
+
try {
|
|
12580
|
+
const result = spawnSync(interp.cmd, [...interp.args, "--version"], { stdio: "ignore" });
|
|
12581
|
+
if (!result.error && result.status === 0) return interp;
|
|
12582
|
+
} catch {
|
|
12583
|
+
}
|
|
12584
|
+
}
|
|
12585
|
+
return null;
|
|
12586
|
+
}
|
|
12587
|
+
function resolveCtxpackScript() {
|
|
12588
|
+
const here = dirname3(fileURLToPath(import.meta.url));
|
|
12589
|
+
const candidates = [
|
|
12590
|
+
join3(here, "vendor", "ctxpack.py"),
|
|
12591
|
+
// bundled: dist/cli.js -> dist/vendor/
|
|
12592
|
+
join3(here, "..", "vendor", "ctxpack.py"),
|
|
12593
|
+
// published package root -> vendor/
|
|
12594
|
+
join3(here, "..", "..", "vendor", "ctxpack.py")
|
|
12595
|
+
// dev: src/lib -> packages/cli/vendor/
|
|
12596
|
+
];
|
|
12597
|
+
for (const candidate of candidates) {
|
|
12598
|
+
if (existsSync3(candidate)) return candidate;
|
|
12599
|
+
}
|
|
12600
|
+
throw new Error(
|
|
12601
|
+
`Could not locate the vendored ctxpack packer (looked in: ${candidates.join(", ")}). This is a packaging bug \u2014 reinstall @kinosec/cli.`
|
|
12602
|
+
);
|
|
12603
|
+
}
|
|
12604
|
+
var PYTHON_MISSING_MESSAGE = 'Packing --source requires Python 3, but none of "python3", "py -3", or "python" was found on PATH.\nInstall Python 3 (https://www.python.org/downloads/) and re-run, or pass an already-packed bundle: --source <bundle.txt>.';
|
|
12605
|
+
async function packSource(source, options) {
|
|
12606
|
+
if (!existsSync3(source)) {
|
|
12607
|
+
throw new Error(`--source path not found: ${source}`);
|
|
12608
|
+
}
|
|
12609
|
+
const stats = statSync(source);
|
|
12610
|
+
if (stats.isFile() && source.toLowerCase().endsWith(".txt")) {
|
|
12611
|
+
const bundleText = readFileSync3(source, "utf8");
|
|
12612
|
+
return {
|
|
12613
|
+
bundleText,
|
|
12614
|
+
bytes: Buffer.byteLength(bundleText, "utf8"),
|
|
12615
|
+
mode: null,
|
|
12616
|
+
stack: sniffBundleStack(bundleText),
|
|
12617
|
+
manifest: null,
|
|
12618
|
+
stderr: "",
|
|
12619
|
+
premade: true
|
|
12620
|
+
};
|
|
12621
|
+
}
|
|
12622
|
+
const python = findPython();
|
|
12623
|
+
if (!python) {
|
|
12624
|
+
throw new Error(PYTHON_MISSING_MESSAGE);
|
|
12625
|
+
}
|
|
12626
|
+
const script = resolveCtxpackScript();
|
|
12627
|
+
const outDir = mkdtempSync(join3(tmpdir(), "kinosec-ctxpack-"));
|
|
12628
|
+
try {
|
|
12629
|
+
const args = [...python.args, script, source, "--budget", String(options.budget), "-o", outDir];
|
|
12630
|
+
if (options.zipPassword) {
|
|
12631
|
+
args.push("--zip-password", options.zipPassword);
|
|
12632
|
+
}
|
|
12633
|
+
const proc = spawnSync(python.cmd, args, { encoding: "utf8" });
|
|
12634
|
+
if (proc.error) {
|
|
12635
|
+
throw new Error(`Failed to run ctxpack via ${python.cmd}: ${proc.error.message}`);
|
|
12636
|
+
}
|
|
12637
|
+
if (proc.status !== 0) {
|
|
12638
|
+
const detail = (proc.stderr || "").trim();
|
|
12639
|
+
throw new Error(
|
|
12640
|
+
`ctxpack failed to pack --source (exit ${proc.status ?? "unknown"}).` + (detail ? `
|
|
12641
|
+
${detail}` : "") + (looksLikeAesZip(detail) ? '\nHint: only classic ZipCrypto zips are supported (HTB handouts use "hackthebox"), not WinZip/AES-256. Re-create the archive with classic encryption, or extract it and pass the directory.' : "")
|
|
12642
|
+
);
|
|
12643
|
+
}
|
|
12644
|
+
const bundlePath = join3(outDir, "bundle.txt");
|
|
12645
|
+
if (!existsSync3(bundlePath)) {
|
|
12646
|
+
throw new Error(`ctxpack produced no bundle (${bundlePath} missing).`);
|
|
12647
|
+
}
|
|
12648
|
+
const bundleText = readFileSync3(bundlePath, "utf8");
|
|
12649
|
+
let manifest = null;
|
|
12650
|
+
try {
|
|
12651
|
+
manifest = JSON.parse(readFileSync3(join3(outDir, "manifest.json"), "utf8"));
|
|
12652
|
+
} catch {
|
|
12653
|
+
manifest = null;
|
|
12654
|
+
}
|
|
12655
|
+
const stack = manifest?.signals?.stack ? String(manifest.signals.stack) : null;
|
|
12656
|
+
const mode = manifest?.mode ? String(manifest.mode) : null;
|
|
12657
|
+
return {
|
|
12658
|
+
bundleText,
|
|
12659
|
+
bytes: Buffer.byteLength(bundleText, "utf8"),
|
|
12660
|
+
mode,
|
|
12661
|
+
stack,
|
|
12662
|
+
manifest,
|
|
12663
|
+
stderr: proc.stderr || "",
|
|
12664
|
+
premade: false
|
|
12665
|
+
};
|
|
12666
|
+
} finally {
|
|
12667
|
+
try {
|
|
12668
|
+
rmSync(outDir, { recursive: true, force: true });
|
|
12669
|
+
} catch {
|
|
12670
|
+
}
|
|
12671
|
+
}
|
|
12672
|
+
}
|
|
12673
|
+
function looksLikeAesZip(stderr) {
|
|
12674
|
+
const lc = stderr.toLowerCase();
|
|
12675
|
+
return lc.includes("compression type 99") || lc.includes("aes") || lc.includes("winzip");
|
|
12676
|
+
}
|
|
12677
|
+
function sniffBundleStack(text) {
|
|
12678
|
+
const lc = text.toLowerCase();
|
|
12679
|
+
if (/"next"|next\.config|\/_next\//.test(lc)) return "nextjs";
|
|
12680
|
+
if (/"express"/.test(lc)) return "express";
|
|
12681
|
+
if (/from flask|import flask/.test(lc)) return "flask";
|
|
12682
|
+
if (/<\?php/.test(lc)) return "php";
|
|
12683
|
+
return null;
|
|
12684
|
+
}
|
|
12685
|
+
function matchLiveStack(lowercased) {
|
|
12686
|
+
if (lowercased.includes("x-powered-by: next.js") || lowercased.includes("/_next/") || lowercased.includes("__next_data__")) {
|
|
12687
|
+
return "nextjs";
|
|
12688
|
+
}
|
|
12689
|
+
if (lowercased.includes("x-powered-by: express")) return "express";
|
|
12690
|
+
if (lowercased.includes("x-powered-by: php") || lowercased.includes("phpsessid")) return "php";
|
|
12691
|
+
if (lowercased.includes("server: werkzeug")) return "flask";
|
|
12692
|
+
if (lowercased.includes("csrftoken")) return "django";
|
|
12693
|
+
return null;
|
|
12694
|
+
}
|
|
12695
|
+
var COMPATIBLE_STACKS = /* @__PURE__ */ new Set([
|
|
12696
|
+
"nextjs/react",
|
|
12697
|
+
"react/nextjs",
|
|
12698
|
+
"nextjs/node",
|
|
12699
|
+
"node/nextjs",
|
|
12700
|
+
"express/node",
|
|
12701
|
+
"node/express"
|
|
12702
|
+
]);
|
|
12703
|
+
function stacksConflict(bundleStack, liveStack) {
|
|
12704
|
+
if (!bundleStack || !liveStack) return false;
|
|
12705
|
+
if (bundleStack === liveStack) return false;
|
|
12706
|
+
if (COMPATIBLE_STACKS.has(`${bundleStack}/${liveStack}`)) return false;
|
|
12707
|
+
return true;
|
|
12708
|
+
}
|
|
12709
|
+
async function probeLiveStack(host, timeoutMs = 8e3) {
|
|
12710
|
+
for (const scheme of ["https", "http"]) {
|
|
12711
|
+
let res;
|
|
12712
|
+
const controller = new AbortController();
|
|
12713
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
12714
|
+
try {
|
|
12715
|
+
res = await fetch(`${scheme}://${host}/`, { signal: controller.signal, redirect: "follow" });
|
|
12716
|
+
} catch {
|
|
12717
|
+
continue;
|
|
12718
|
+
} finally {
|
|
12719
|
+
clearTimeout(timer);
|
|
12720
|
+
}
|
|
12721
|
+
const headerBlob = [...res.headers].map(([key, value]) => `${key}: ${value}`).join("\n");
|
|
12722
|
+
let body = "";
|
|
12723
|
+
try {
|
|
12724
|
+
body = (await res.text()).slice(0, 4096);
|
|
12725
|
+
} catch {
|
|
12726
|
+
}
|
|
12727
|
+
return matchLiveStack(`${headerBlob}
|
|
12728
|
+
${body}`.toLowerCase());
|
|
12729
|
+
}
|
|
12730
|
+
return null;
|
|
12731
|
+
}
|
|
12732
|
+
|
|
12564
12733
|
// src/commands/pentest.ts
|
|
12734
|
+
var DEFAULT_SOURCE_BUDGET = 4e4;
|
|
12735
|
+
function reportPackResult(result, budget) {
|
|
12736
|
+
const size = `${result.bytes.toLocaleString()} bytes`;
|
|
12737
|
+
if (result.premade) {
|
|
12738
|
+
console.error(`Graybox: using pre-made source bundle (${size}).`);
|
|
12739
|
+
} else {
|
|
12740
|
+
for (const line of result.stderr.split("\n")) {
|
|
12741
|
+
const trimmed = line.trim();
|
|
12742
|
+
if (trimmed.startsWith("ctxpack: mode=")) {
|
|
12743
|
+
console.error(`Graybox: ${trimmed.slice("ctxpack: ".length)} [${size}]`);
|
|
12744
|
+
}
|
|
12745
|
+
}
|
|
12746
|
+
}
|
|
12747
|
+
if (result.bytes > SOURCE_BYTES_WARN) {
|
|
12748
|
+
printWarning(
|
|
12749
|
+
`Source bundle is ${size} \u2014 that's a lot of prompt context. It will still be delivered (the server ships oversize source out-of-band), but large bundles raise per-scan cost and can dilute the agent's focus. Lower --budget (currently ${budget}) or trim --source to tighten it.`
|
|
12750
|
+
);
|
|
12751
|
+
}
|
|
12752
|
+
}
|
|
12753
|
+
function ensurePasteBundle(result) {
|
|
12754
|
+
if (result.premade || result.mode === "paste") return;
|
|
12755
|
+
if (result.mode === "retrieve") {
|
|
12756
|
+
printWarning(
|
|
12757
|
+
"ctxpack fell back to retrieve mode: the source is too large to paste within --budget, so the bundle is a path outline with no code \u2014 useless as graybox context."
|
|
12758
|
+
);
|
|
12759
|
+
console.error(
|
|
12760
|
+
"Not sending it. Raise --budget to re-pack the source as a full paste bundle, or trim --source to the exploit-relevant files (or run a normal black-box scan without --source)."
|
|
12761
|
+
);
|
|
12762
|
+
process.exit(1);
|
|
12763
|
+
}
|
|
12764
|
+
}
|
|
12765
|
+
function isUserDataTooLarge(err) {
|
|
12766
|
+
return err.status === 413 || /user-?data too large/i.test(err.detail);
|
|
12767
|
+
}
|
|
12565
12768
|
function registerPentestCommand(program3) {
|
|
12566
|
-
const pentest = program3.command("pentest").description("Run and manage pentests").option("-t, --target <domain>", "Target domain to pentest").option("--profile <id>", "Pentest profile ID (alternative to --target)").option("--mode <mode>", "Pentest mode: full, quick, htb, or
|
|
12769
|
+
const pentest = program3.command("pentest").description("Run and manage pentests").option("-t, --target <domain>", "Target domain to pentest").option("--profile <id>", "Pentest profile ID (alternative to --target)").option("--mode <mode>", "Pentest mode: full, quick, htb, defcon, or exploit-only", "full").option("--flag <prefix>", "CTF flag prefix (defcon mode only, default HTB)").option("--context <text>", "CTF focus hint (defcon mode only)").option("-y, --yes", "Accept authorization terms without prompting").option("--no-watch", "Skip auto-watch after starting the pentest").option("--deployment <type>", "Deployment type: cloud or on-prem", "cloud").option("--include <hosts>", "Include only these subdomains (comma-separated)").option("--exclude <hosts>", "Exclude these subdomains (comma-separated)").option("--scope <action>", "Scope action: clear").option("--source <path>", "Graybox source handout: a directory, an archive (.zip/.tar/.tgz), or a pre-made bundle .txt. Its presence enables graybox").option("--budget <tokens>", "Token budget for packing --source (how much source to pack; larger = more context and cost)", String(DEFAULT_SOURCE_BUDGET)).option("--zip-password <pw>", "Password for an encrypted --source .zip (classic ZipCrypto only; HTB handouts use: hackthebox)").option("--force", "Proceed even when the --source stack looks unlike the live target").addHelpText("after", `
|
|
12567
12770
|
Examples:
|
|
12568
12771
|
$ kinosec pentest -t delikado.org
|
|
12569
12772
|
$ kinosec pentest --profile abc123
|
|
12570
12773
|
$ kinosec pentest -t internal.corp --deployment on-prem
|
|
12571
12774
|
$ kinosec pentest -t example.com --include api.example.com,app.example.com
|
|
12572
12775
|
$ kinosec pentest -t example.com --exclude staging.example.com
|
|
12573
|
-
$ kinosec pentest -t example.com --scope clear
|
|
12776
|
+
$ kinosec pentest -t example.com --scope clear
|
|
12777
|
+
$ kinosec pentest -t juice.example.com --mode exploit-only
|
|
12778
|
+
$ kinosec pentest -t target.htb --mode htb --source ./challenge.zip --zip-password hackthebox
|
|
12779
|
+
|
|
12780
|
+
Graybox (--source):
|
|
12781
|
+
Packs the target's source (paste-mode only) and folds it into the agent as authoritative
|
|
12782
|
+
context. Works with --mode full and quick for any user's own asset; htb/defcon additionally
|
|
12783
|
+
require admin access. Large source is delivered out-of-band, so there's no size ceiling to
|
|
12784
|
+
fight \u2014 --budget just controls how much source is packed (bigger bundles add context and cost).
|
|
12785
|
+
The source is sent over HTTPS, used only on the ephemeral scan droplet, and is NOT stored
|
|
12786
|
+
server-side (the training log records just a "[GRAYBOX] N bytes injected" marker, never the source).`).action(async (options) => {
|
|
12574
12787
|
if (!options.target && !options.profile) {
|
|
12575
12788
|
console.error("Provide --target <domain> or --profile <id>.");
|
|
12576
12789
|
process.exit(2);
|
|
@@ -12583,6 +12796,16 @@ Examples:
|
|
|
12583
12796
|
process.exit(2);
|
|
12584
12797
|
}
|
|
12585
12798
|
validateScanMode(options.mode);
|
|
12799
|
+
let sourceBudget = DEFAULT_SOURCE_BUDGET;
|
|
12800
|
+
if (options.source) {
|
|
12801
|
+
sourceBudget = parseInt(options.budget, 10);
|
|
12802
|
+
if (!Number.isInteger(sourceBudget) || sourceBudget <= 0) {
|
|
12803
|
+
console.error("Invalid --budget. Must be a positive integer (tokens).");
|
|
12804
|
+
process.exit(2);
|
|
12805
|
+
}
|
|
12806
|
+
} else if (options.zipPassword || options.force) {
|
|
12807
|
+
console.error("Warning: --zip-password/--force apply only with --source; ignoring.");
|
|
12808
|
+
}
|
|
12586
12809
|
const scopeClear = options.scope === "clear";
|
|
12587
12810
|
if (options.scope && options.scope !== "clear") {
|
|
12588
12811
|
console.error("Invalid --scope value. Must be: clear");
|
|
@@ -12601,6 +12824,15 @@ Examples:
|
|
|
12601
12824
|
const opts = program3.opts();
|
|
12602
12825
|
const { token, apiUrl } = resolveToken(opts);
|
|
12603
12826
|
const client = new KinoSecClient({ apiUrl, token });
|
|
12827
|
+
let packResult = null;
|
|
12828
|
+
if (options.source) {
|
|
12829
|
+
packResult = await packSource(options.source, {
|
|
12830
|
+
budget: sourceBudget,
|
|
12831
|
+
zipPassword: options.zipPassword
|
|
12832
|
+
});
|
|
12833
|
+
reportPackResult(packResult, sourceBudget);
|
|
12834
|
+
ensurePasteBundle(packResult);
|
|
12835
|
+
}
|
|
12604
12836
|
let targetDisplay;
|
|
12605
12837
|
let params;
|
|
12606
12838
|
if (options.profile) {
|
|
@@ -12684,8 +12916,47 @@ Examples:
|
|
|
12684
12916
|
}
|
|
12685
12917
|
displayScope(asset);
|
|
12686
12918
|
}
|
|
12919
|
+
if (packResult?.stack && !options.force) {
|
|
12920
|
+
const liveStack = await probeLiveStack(targetDisplay);
|
|
12921
|
+
if (liveStack && stacksConflict(packResult.stack, liveStack)) {
|
|
12922
|
+
printWarning(
|
|
12923
|
+
`Source handout looks like "${packResult.stack}", but ${targetDisplay} responds like "${liveStack}".`
|
|
12924
|
+
);
|
|
12925
|
+
printWarning("The source may belong to a different app; graybox context could mislead the pentest.");
|
|
12926
|
+
if (options.yes) {
|
|
12927
|
+
console.error("Proceeding anyway (--yes).");
|
|
12928
|
+
} else {
|
|
12929
|
+
console.error("Aborting. Re-run with --force to override, or fix --source.");
|
|
12930
|
+
process.exit(2);
|
|
12931
|
+
}
|
|
12932
|
+
}
|
|
12933
|
+
}
|
|
12687
12934
|
await confirmAuthorization(targetDisplay, Boolean(options.yes));
|
|
12688
|
-
|
|
12935
|
+
if (packResult) {
|
|
12936
|
+
params.source_context = packResult.bundleText;
|
|
12937
|
+
}
|
|
12938
|
+
let result;
|
|
12939
|
+
try {
|
|
12940
|
+
result = await client.scans.create(params);
|
|
12941
|
+
} catch (err) {
|
|
12942
|
+
if (err instanceof ApiError) {
|
|
12943
|
+
if (packResult && isUserDataTooLarge(err)) {
|
|
12944
|
+
printWarning(`Server rejected the scan: ${err.detail}`);
|
|
12945
|
+
console.error(
|
|
12946
|
+
`The graybox source made the provisioning payload too large. Re-pack with a smaller --budget (currently ${sourceBudget}) or trim --source, then retry.`
|
|
12947
|
+
);
|
|
12948
|
+
process.exit(1);
|
|
12949
|
+
}
|
|
12950
|
+
if (err.status === 403 && (options.mode === "htb" || options.mode === "defcon")) {
|
|
12951
|
+
printWarning(`The "${options.mode}" scan mode requires admin access (server returned 403).`);
|
|
12952
|
+
console.error(
|
|
12953
|
+
"Run with --mode full or --mode quick (which support --source for your own assets), or ask your KinoSec admin to enable CTF modes for your account."
|
|
12954
|
+
);
|
|
12955
|
+
process.exit(77);
|
|
12956
|
+
}
|
|
12957
|
+
}
|
|
12958
|
+
throw err;
|
|
12959
|
+
}
|
|
12689
12960
|
const isOnPrem = result.deployment_type === "on_prem";
|
|
12690
12961
|
if (opts.json || opts.output === "json") {
|
|
12691
12962
|
printJson(result);
|
|
@@ -13793,7 +14064,7 @@ Examples:
|
|
|
13793
14064
|
|
|
13794
14065
|
// src/cli.ts
|
|
13795
14066
|
var program2 = new Command();
|
|
13796
|
-
program2.name("kinosec").description("KinoSec CLI \u2014 run pentests from your terminal").version("0.
|
|
14067
|
+
program2.name("kinosec").description("KinoSec CLI \u2014 run pentests from your terminal").version("0.11.1", "-v, --version").option("--token <pat>", "API token (overrides KINOSEC_TOKEN)").option("--output <format>", "Output format: text or json", "text").option("--json", "Shorthand for --output json").option("--no-color", "Disable colors");
|
|
13797
14068
|
registerAuthCommand(program2);
|
|
13798
14069
|
registerMeCommand(program2);
|
|
13799
14070
|
registerCreditsCommand(program2);
|
|
@@ -13802,7 +14073,7 @@ registerAssetCommand(program2);
|
|
|
13802
14073
|
registerFindingCommand(program2);
|
|
13803
14074
|
registerReportCommand(program2);
|
|
13804
14075
|
registerTokenCommand(program2);
|
|
13805
|
-
program2.parseAsync(process.argv).then(() => checkForUpdate("0.
|
|
14076
|
+
program2.parseAsync(process.argv).then(() => checkForUpdate("0.11.1")).catch(handleError);
|
|
13806
14077
|
/*! Bundled license information:
|
|
13807
14078
|
|
|
13808
14079
|
ky/distribution/index.js:
|