diff --git a/.gitignore b/.gitignore index e90b069..8d0c4d0 100644 --- a/.gitignore +++ b/.gitignore @@ -64,4 +64,16 @@ yarn-error.* # vites coverage -js/coverage \ No newline at end of file +js/coverage + +# results outputs +./**/domain-results.* +./**/domain-check-results.* +js/domain-results.* +js/domain-check-results.* + +js/domains-to-test.* +domains-to-test.* + +# tsconfig +tsconfig.tsbuildinfo \ No newline at end of file diff --git a/.pnp.cjs b/.pnp.cjs index 4f54b53..748d9c2 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -35,10 +35,21 @@ const RAW_RUNTIME_STATE = "packageDependencies": [\ ["@biomejs/biome", "npm:2.3.3"],\ ["@types/commander", "npm:2.12.5"],\ - ["@types/node", "npm:22.19.0"],\ + ["@types/csv-stringify", "npm:3.1.3"],\ + ["@types/js-yaml", "npm:4.0.9"],\ + ["@types/node", "npm:24.10.9"],\ + ["@types/whois-json", "npm:2.0.4"],\ ["@vitest/coverage-v8", "virtual:6fddcb6bd99765bac7f2b290599c78196c6e6e0b69854e91620b6ccb38ae963c32e85d4bd2b1bd018e76b720595ed01206569ae47872fa1aad64c63460e22100#npm:4.0.6"],\ + ["commander", "npm:14.0.2"],\ + ["corepack", "npm:0.34.6"],\ + ["csv-stringify", "npm:6.6.0"],\ + ["js-yaml", "npm:4.1.0"],\ ["osa-snippets", "workspace:."],\ - ["vitest", "virtual:6fddcb6bd99765bac7f2b290599c78196c6e6e0b69854e91620b6ccb38ae963c32e85d4bd2b1bd018e76b720595ed01206569ae47872fa1aad64c63460e22100#npm:4.0.6"]\ + ["p-limit", "npm:7.2.0"],\ + ["tsx", "npm:4.20.6"],\ + ["typescript", "patch:typescript@npm%3A5.9.3#optional!builtin::version=5.9.3&hash=5786d5"],\ + ["vitest", "virtual:6fddcb6bd99765bac7f2b290599c78196c6e6e0b69854e91620b6ccb38ae963c32e85d4bd2b1bd018e76b720595ed01206569ae47872fa1aad64c63460e22100#npm:4.0.6"],\ + ["whois-json", "npm:2.0.4"]\ ],\ "linkType": "SOFT"\ }]\ @@ -738,6 +749,16 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["@types/csv-stringify", [\ + ["npm:3.1.3", {\ + "packageLocation": "../../.yarn/berry/cache/@types-csv-stringify-npm-3.1.3-6783d64144-10c0.zip/node_modules/@types/csv-stringify/",\ + "packageDependencies": [\ + ["@types/csv-stringify", "npm:3.1.3"],\ + ["csv-stringify", "npm:6.6.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@types/deep-eql", [\ ["npm:4.0.2", {\ "packageLocation": "../../.yarn/berry/cache/@types-deep-eql-npm-4.0.2-e6bc68cc92-10c0.zip/node_modules/@types/deep-eql/",\ @@ -756,12 +777,30 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["@types/js-yaml", [\ + ["npm:4.0.9", {\ + "packageLocation": "../../.yarn/berry/cache/@types-js-yaml-npm-4.0.9-6a16d01bd2-10c0.zip/node_modules/@types/js-yaml/",\ + "packageDependencies": [\ + ["@types/js-yaml", "npm:4.0.9"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@types/node", [\ - ["npm:22.19.0", {\ - "packageLocation": "../../.yarn/berry/cache/@types-node-npm-22.19.0-13cee709ff-10c0.zip/node_modules/@types/node/",\ + ["npm:24.10.9", {\ + "packageLocation": "../../.yarn/berry/cache/@types-node-npm-24.10.9-334f1bf8b5-10c0.zip/node_modules/@types/node/",\ + "packageDependencies": [\ + ["@types/node", "npm:24.10.9"],\ + ["undici-types", "npm:7.16.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@types/whois-json", [\ + ["npm:2.0.4", {\ + "packageLocation": "../../.yarn/berry/cache/@types-whois-json-npm-2.0.4-c7eae50a48-10c0.zip/node_modules/@types/whois-json/",\ "packageDependencies": [\ - ["@types/node", "npm:22.19.0"],\ - ["undici-types", "npm:6.21.0"]\ + ["@types/whois-json", "npm:2.0.4"]\ ],\ "linkType": "HARD"\ }]\ @@ -771,13 +810,20 @@ const RAW_RUNTIME_STATE = "packageLocation": "./js/",\ "packageDependencies": [\ ["@types/commander", "npm:2.12.5"],\ - ["@types/node", "npm:22.19.0"],\ + ["@types/csv-stringify", "npm:3.1.3"],\ + ["@types/js-yaml", "npm:4.0.9"],\ + ["@types/node", "npm:24.10.9"],\ + ["@types/whois-json", "npm:2.0.4"],\ ["@virtualize/osa-snippets", "workspace:js"],\ ["@vitest/coverage-v8", "virtual:6fddcb6bd99765bac7f2b290599c78196c6e6e0b69854e91620b6ccb38ae963c32e85d4bd2b1bd018e76b720595ed01206569ae47872fa1aad64c63460e22100#npm:4.0.6"],\ - ["commander", "npm:12.1.0"],\ + ["commander", "npm:14.0.2"],\ + ["csv-stringify", "npm:6.6.0"],\ + ["js-yaml", "npm:4.1.0"],\ + ["p-limit", "npm:7.2.0"],\ ["tsx", "npm:4.20.6"],\ ["typescript", "patch:typescript@npm%3A5.9.3#optional!builtin::version=5.9.3&hash=5786d5"],\ - ["vitest", "virtual:6fddcb6bd99765bac7f2b290599c78196c6e6e0b69854e91620b6ccb38ae963c32e85d4bd2b1bd018e76b720595ed01206569ae47872fa1aad64c63460e22100#npm:4.0.6"]\ + ["vitest", "virtual:6fddcb6bd99765bac7f2b290599c78196c6e6e0b69854e91620b6ccb38ae963c32e85d4bd2b1bd018e76b720595ed01206569ae47872fa1aad64c63460e22100#npm:4.0.6"],\ + ["whois-json", "npm:2.0.4"]\ ],\ "linkType": "SOFT"\ }]\ @@ -800,7 +846,7 @@ const RAW_RUNTIME_STATE = ["@vitest/coverage-v8", "virtual:6fddcb6bd99765bac7f2b290599c78196c6e6e0b69854e91620b6ccb38ae963c32e85d4bd2b1bd018e76b720595ed01206569ae47872fa1aad64c63460e22100#npm:4.0.6"],\ ["@vitest/utils", "npm:4.0.6"],\ ["ast-v8-to-istanbul", "npm:0.3.8"],\ - ["debug", "virtual:e18ad1aaaee71856c8618c92ebbcee187ad490da91f816fa2cd1abd550e96735d3b7ca6820bb15ac7883b0178149f52bbfa078bf86f89229e4aed2f3ed5555cc#npm:4.4.3"],\ + ["debug", "virtual:643ed7cc338bcf145a82d8b05b3bef6bcf150ca545df386225596f10ce53cc90b88b3ca83e348ade1ccea5f3f8e76c92d2f0e2ba544da60d40aff9921c56872d#npm:4.4.3"],\ ["istanbul-lib-coverage", "npm:3.2.2"],\ ["istanbul-lib-report", "npm:3.0.1"],\ ["istanbul-lib-source-maps", "npm:5.0.6"],\ @@ -967,6 +1013,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["argparse", [\ + ["npm:2.0.1", {\ + "packageLocation": "../../.yarn/berry/cache/argparse-npm-2.0.1-faff7999e6-10c0.zip/node_modules/argparse/",\ + "packageDependencies": [\ + ["argparse", "npm:2.0.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["assertion-error", [\ ["npm:2.0.1", {\ "packageLocation": "../../.yarn/berry/cache/assertion-error-npm-2.0.1-8169d136f2-10c0.zip/node_modules/assertion-error/",\ @@ -1028,6 +1083,26 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["camel-case", [\ + ["npm:3.0.0", {\ + "packageLocation": "../../.yarn/berry/cache/camel-case-npm-3.0.0-d87e5afe35-10c0.zip/node_modules/camel-case/",\ + "packageDependencies": [\ + ["camel-case", "npm:3.0.0"],\ + ["no-case", "npm:2.3.2"],\ + ["upper-case", "npm:1.1.3"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["camelcase", [\ + ["npm:5.3.1", {\ + "packageLocation": "../../.yarn/berry/cache/camelcase-npm-5.3.1-5db8af62c5-10c0.zip/node_modules/camelcase/",\ + "packageDependencies": [\ + ["camelcase", "npm:5.3.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["chai", [\ ["npm:6.2.0", {\ "packageLocation": "../../.yarn/berry/cache/chai-npm-6.2.0-ce657a084d-10c0.zip/node_modules/chai/",\ @@ -1037,6 +1112,33 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["change-case", [\ + ["npm:3.1.0", {\ + "packageLocation": "../../.yarn/berry/cache/change-case-npm-3.1.0-f29e0003bb-10c0.zip/node_modules/change-case/",\ + "packageDependencies": [\ + ["camel-case", "npm:3.0.0"],\ + ["change-case", "npm:3.1.0"],\ + ["constant-case", "npm:2.0.0"],\ + ["dot-case", "npm:2.1.1"],\ + ["header-case", "npm:1.0.1"],\ + ["is-lower-case", "npm:1.1.3"],\ + ["is-upper-case", "npm:1.1.2"],\ + ["lower-case", "npm:1.1.4"],\ + ["lower-case-first", "npm:1.0.2"],\ + ["no-case", "npm:2.3.2"],\ + ["param-case", "npm:2.1.1"],\ + ["pascal-case", "npm:2.0.1"],\ + ["path-case", "npm:2.1.1"],\ + ["sentence-case", "npm:2.1.1"],\ + ["snake-case", "npm:2.1.0"],\ + ["swap-case", "npm:1.1.2"],\ + ["title-case", "npm:2.1.1"],\ + ["upper-case", "npm:1.1.3"],\ + ["upper-case-first", "npm:1.1.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["chownr", [\ ["npm:3.0.0", {\ "packageLocation": "../../.yarn/berry/cache/chownr-npm-3.0.0-5275e85d25-10c0.zip/node_modules/chownr/",\ @@ -1046,6 +1148,18 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["cliui", [\ + ["npm:6.0.0", {\ + "packageLocation": "../../.yarn/berry/cache/cliui-npm-6.0.0-488b2414c6-10c0.zip/node_modules/cliui/",\ + "packageDependencies": [\ + ["cliui", "npm:6.0.0"],\ + ["string-width", "npm:4.2.3"],\ + ["strip-ansi", "npm:6.0.1"],\ + ["wrap-ansi", "npm:6.2.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["color-convert", [\ ["npm:2.0.1", {\ "packageLocation": "../../.yarn/berry/cache/color-convert-npm-2.0.1-79730e935b-10c0.zip/node_modules/color-convert/",\ @@ -1066,13 +1180,6 @@ const RAW_RUNTIME_STATE = }]\ ]],\ ["commander", [\ - ["npm:12.1.0", {\ - "packageLocation": "../../.yarn/berry/cache/commander-npm-12.1.0-65c868e907-10c0.zip/node_modules/commander/",\ - "packageDependencies": [\ - ["commander", "npm:12.1.0"]\ - ],\ - "linkType": "HARD"\ - }],\ ["npm:14.0.2", {\ "packageLocation": "../../.yarn/berry/cache/commander-npm-14.0.2-538b84c387-10c0.zip/node_modules/commander/",\ "packageDependencies": [\ @@ -1081,6 +1188,26 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["constant-case", [\ + ["npm:2.0.0", {\ + "packageLocation": "../../.yarn/berry/cache/constant-case-npm-2.0.0-b287998b5e-10c0.zip/node_modules/constant-case/",\ + "packageDependencies": [\ + ["constant-case", "npm:2.0.0"],\ + ["snake-case", "npm:2.1.0"],\ + ["upper-case", "npm:1.1.3"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["corepack", [\ + ["npm:0.34.6", {\ + "packageLocation": "../../.yarn/berry/cache/corepack-npm-0.34.6-eac3801420-10c0.zip/node_modules/corepack/",\ + "packageDependencies": [\ + ["corepack", "npm:0.34.6"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["cross-spawn", [\ ["npm:7.0.6", {\ "packageLocation": "../../.yarn/berry/cache/cross-spawn-npm-7.0.6-264bddf921-10c0.zip/node_modules/cross-spawn/",\ @@ -1093,6 +1220,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["csv-stringify", [\ + ["npm:6.6.0", {\ + "packageLocation": "../../.yarn/berry/cache/csv-stringify-npm-6.6.0-afbd791b30-10c0.zip/node_modules/csv-stringify/",\ + "packageDependencies": [\ + ["csv-stringify", "npm:6.6.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["debug", [\ ["npm:4.4.3", {\ "packageLocation": "../../.yarn/berry/cache/debug-npm-4.4.3-0105c6123a-10c0.zip/node_modules/debug/",\ @@ -1101,11 +1237,11 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "SOFT"\ }],\ - ["virtual:e18ad1aaaee71856c8618c92ebbcee187ad490da91f816fa2cd1abd550e96735d3b7ca6820bb15ac7883b0178149f52bbfa078bf86f89229e4aed2f3ed5555cc#npm:4.4.3", {\ - "packageLocation": "./.yarn/__virtual__/debug-virtual-b72081c4d0/3/.yarn/berry/cache/debug-npm-4.4.3-0105c6123a-10c0.zip/node_modules/debug/",\ + ["virtual:643ed7cc338bcf145a82d8b05b3bef6bcf150ca545df386225596f10ce53cc90b88b3ca83e348ade1ccea5f3f8e76c92d2f0e2ba544da60d40aff9921c56872d#npm:4.4.3", {\ + "packageLocation": "./.yarn/__virtual__/debug-virtual-5da9fc757d/3/.yarn/berry/cache/debug-npm-4.4.3-0105c6123a-10c0.zip/node_modules/debug/",\ "packageDependencies": [\ ["@types/supports-color", null],\ - ["debug", "virtual:e18ad1aaaee71856c8618c92ebbcee187ad490da91f816fa2cd1abd550e96735d3b7ca6820bb15ac7883b0178149f52bbfa078bf86f89229e4aed2f3ed5555cc#npm:4.4.3"],\ + ["debug", "virtual:643ed7cc338bcf145a82d8b05b3bef6bcf150ca545df386225596f10ce53cc90b88b3ca83e348ade1ccea5f3f8e76c92d2f0e2ba544da60d40aff9921c56872d#npm:4.4.3"],\ ["ms", "npm:2.1.3"],\ ["supports-color", null]\ ],\ @@ -1116,6 +1252,34 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["decamelize", [\ + ["npm:1.2.0", {\ + "packageLocation": "../../.yarn/berry/cache/decamelize-npm-1.2.0-c5a2fdc622-10c0.zip/node_modules/decamelize/",\ + "packageDependencies": [\ + ["decamelize", "npm:1.2.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["dedent-js", [\ + ["npm:1.0.1", {\ + "packageLocation": "../../.yarn/berry/cache/dedent-js-npm-1.0.1-ddf8ce03f4-10c0.zip/node_modules/dedent-js/",\ + "packageDependencies": [\ + ["dedent-js", "npm:1.0.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["dot-case", [\ + ["npm:2.1.1", {\ + "packageLocation": "../../.yarn/berry/cache/dot-case-npm-2.1.1-f591fd2e48-10c0.zip/node_modules/dot-case/",\ + "packageDependencies": [\ + ["dot-case", "npm:2.1.1"],\ + ["no-case", "npm:2.3.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["eastasianwidth", [\ ["npm:0.2.0", {\ "packageLocation": "../../.yarn/berry/cache/eastasianwidth-npm-0.2.0-c37eb16bd1-10c0.zip/node_modules/eastasianwidth/",\ @@ -1263,6 +1427,17 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["find-up", [\ + ["npm:4.1.0", {\ + "packageLocation": "../../.yarn/berry/cache/find-up-npm-4.1.0-c3ccf8d855-10c0.zip/node_modules/find-up/",\ + "packageDependencies": [\ + ["find-up", "npm:4.1.0"],\ + ["locate-path", "npm:5.0.0"],\ + ["path-exists", "npm:4.0.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["foreground-child", [\ ["npm:3.3.1", {\ "packageLocation": "../../.yarn/berry/cache/foreground-child-npm-3.3.1-b7775fda04-10c0.zip/node_modules/foreground-child/",\ @@ -1294,6 +1469,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["get-caller-file", [\ + ["npm:2.0.5", {\ + "packageLocation": "../../.yarn/berry/cache/get-caller-file-npm-2.0.5-80e8a86305-10c0.zip/node_modules/get-caller-file/",\ + "packageDependencies": [\ + ["get-caller-file", "npm:2.0.5"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["get-tsconfig", [\ ["npm:4.13.0", {\ "packageLocation": "../../.yarn/berry/cache/get-tsconfig-npm-4.13.0-009b232bdd-10c0.zip/node_modules/get-tsconfig/",\ @@ -1337,6 +1521,26 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["header-case", [\ + ["npm:1.0.1", {\ + "packageLocation": "../../.yarn/berry/cache/header-case-npm-1.0.1-3a0bfdc9cc-10c0.zip/node_modules/header-case/",\ + "packageDependencies": [\ + ["header-case", "npm:1.0.1"],\ + ["no-case", "npm:2.3.2"],\ + ["upper-case", "npm:1.1.3"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["html-entities", [\ + ["npm:1.4.0", {\ + "packageLocation": "../../.yarn/berry/cache/html-entities-npm-1.4.0-39a1121015-10c0.zip/node_modules/html-entities/",\ + "packageDependencies": [\ + ["html-entities", "npm:1.4.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["html-escaper", [\ ["npm:2.0.2", {\ "packageLocation": "../../.yarn/berry/cache/html-escaper-npm-2.0.2-38e51ef294-10c0.zip/node_modules/html-escaper/",\ @@ -1360,7 +1564,7 @@ const RAW_RUNTIME_STATE = "packageLocation": "../../.yarn/berry/cache/http-proxy-agent-npm-7.0.2-643ed7cc33-10c0.zip/node_modules/http-proxy-agent/",\ "packageDependencies": [\ ["agent-base", "npm:7.1.4"],\ - ["debug", "virtual:e18ad1aaaee71856c8618c92ebbcee187ad490da91f816fa2cd1abd550e96735d3b7ca6820bb15ac7883b0178149f52bbfa078bf86f89229e4aed2f3ed5555cc#npm:4.4.3"],\ + ["debug", "virtual:643ed7cc338bcf145a82d8b05b3bef6bcf150ca545df386225596f10ce53cc90b88b3ca83e348ade1ccea5f3f8e76c92d2f0e2ba544da60d40aff9921c56872d#npm:4.4.3"],\ ["http-proxy-agent", "npm:7.0.2"]\ ],\ "linkType": "HARD"\ @@ -1371,7 +1575,7 @@ const RAW_RUNTIME_STATE = "packageLocation": "../../.yarn/berry/cache/https-proxy-agent-npm-7.0.6-27a95c2690-10c0.zip/node_modules/https-proxy-agent/",\ "packageDependencies": [\ ["agent-base", "npm:7.1.4"],\ - ["debug", "virtual:e18ad1aaaee71856c8618c92ebbcee187ad490da91f816fa2cd1abd550e96735d3b7ca6820bb15ac7883b0178149f52bbfa078bf86f89229e4aed2f3ed5555cc#npm:4.4.3"],\ + ["debug", "virtual:643ed7cc338bcf145a82d8b05b3bef6bcf150ca545df386225596f10ce53cc90b88b3ca83e348ade1ccea5f3f8e76c92d2f0e2ba544da60d40aff9921c56872d#npm:4.4.3"],\ ["https-proxy-agent", "npm:7.0.6"]\ ],\ "linkType": "HARD"\ @@ -1414,6 +1618,26 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["is-lower-case", [\ + ["npm:1.1.3", {\ + "packageLocation": "../../.yarn/berry/cache/is-lower-case-npm-1.1.3-2f95af21e5-10c0.zip/node_modules/is-lower-case/",\ + "packageDependencies": [\ + ["is-lower-case", "npm:1.1.3"],\ + ["lower-case", "npm:1.1.4"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["is-upper-case", [\ + ["npm:1.1.2", {\ + "packageLocation": "../../.yarn/berry/cache/is-upper-case-npm-1.1.2-0ce2928e8f-10c0.zip/node_modules/is-upper-case/",\ + "packageDependencies": [\ + ["is-upper-case", "npm:1.1.2"],\ + ["upper-case", "npm:1.1.3"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["isexe", [\ ["npm:2.0.0", {\ "packageLocation": "../../.yarn/berry/cache/isexe-npm-2.0.0-b58870bd2e-10c0.zip/node_modules/isexe/",\ @@ -1456,7 +1680,7 @@ const RAW_RUNTIME_STATE = "packageLocation": "../../.yarn/berry/cache/istanbul-lib-source-maps-npm-5.0.6-e18ad1aaae-10c0.zip/node_modules/istanbul-lib-source-maps/",\ "packageDependencies": [\ ["@jridgewell/trace-mapping", "npm:0.3.31"],\ - ["debug", "virtual:e18ad1aaaee71856c8618c92ebbcee187ad490da91f816fa2cd1abd550e96735d3b7ca6820bb15ac7883b0178149f52bbfa078bf86f89229e4aed2f3ed5555cc#npm:4.4.3"],\ + ["debug", "virtual:643ed7cc338bcf145a82d8b05b3bef6bcf150ca545df386225596f10ce53cc90b88b3ca83e348ade1ccea5f3f8e76c92d2f0e2ba544da60d40aff9921c56872d#npm:4.4.3"],\ ["istanbul-lib-coverage", "npm:3.2.2"],\ ["istanbul-lib-source-maps", "npm:5.0.6"]\ ],\ @@ -1494,6 +1718,45 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["js-yaml", [\ + ["npm:4.1.0", {\ + "packageLocation": "../../.yarn/berry/cache/js-yaml-npm-4.1.0-3606f32312-10c0.zip/node_modules/js-yaml/",\ + "packageDependencies": [\ + ["argparse", "npm:2.0.1"],\ + ["js-yaml", "npm:4.1.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["locate-path", [\ + ["npm:5.0.0", {\ + "packageLocation": "../../.yarn/berry/cache/locate-path-npm-5.0.0-46580c43e4-10c0.zip/node_modules/locate-path/",\ + "packageDependencies": [\ + ["locate-path", "npm:5.0.0"],\ + ["p-locate", "npm:4.1.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["lower-case", [\ + ["npm:1.1.4", {\ + "packageLocation": "../../.yarn/berry/cache/lower-case-npm-1.1.4-9880e9dcb0-10c0.zip/node_modules/lower-case/",\ + "packageDependencies": [\ + ["lower-case", "npm:1.1.4"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["lower-case-first", [\ + ["npm:1.0.2", {\ + "packageLocation": "../../.yarn/berry/cache/lower-case-first-npm-1.0.2-9d3e4f27ec-10c0.zip/node_modules/lower-case-first/",\ + "packageDependencies": [\ + ["lower-case", "npm:1.1.4"],\ + ["lower-case-first", "npm:1.0.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["lru-cache", [\ ["npm:10.4.3", {\ "packageLocation": "../../.yarn/berry/cache/lru-cache-npm-10.4.3-30c10b861a-10c0.zip/node_modules/lru-cache/",\ @@ -1672,6 +1935,16 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["no-case", [\ + ["npm:2.3.2", {\ + "packageLocation": "../../.yarn/berry/cache/no-case-npm-2.3.2-5403767f87-10c0.zip/node_modules/no-case/",\ + "packageDependencies": [\ + ["lower-case", "npm:1.1.4"],\ + ["no-case", "npm:2.3.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["node-gyp", [\ ["npm:11.5.0", {\ "packageLocation": "./.yarn/unplugged/node-gyp-npm-11.5.0-6cfe9d790c/node_modules/node-gyp/",\ @@ -1707,14 +1980,53 @@ const RAW_RUNTIME_STATE = "packageDependencies": [\ ["@biomejs/biome", "npm:2.3.3"],\ ["@types/commander", "npm:2.12.5"],\ - ["@types/node", "npm:22.19.0"],\ + ["@types/csv-stringify", "npm:3.1.3"],\ + ["@types/js-yaml", "npm:4.0.9"],\ + ["@types/node", "npm:24.10.9"],\ + ["@types/whois-json", "npm:2.0.4"],\ ["@vitest/coverage-v8", "virtual:6fddcb6bd99765bac7f2b290599c78196c6e6e0b69854e91620b6ccb38ae963c32e85d4bd2b1bd018e76b720595ed01206569ae47872fa1aad64c63460e22100#npm:4.0.6"],\ + ["commander", "npm:14.0.2"],\ + ["corepack", "npm:0.34.6"],\ + ["csv-stringify", "npm:6.6.0"],\ + ["js-yaml", "npm:4.1.0"],\ ["osa-snippets", "workspace:."],\ - ["vitest", "virtual:6fddcb6bd99765bac7f2b290599c78196c6e6e0b69854e91620b6ccb38ae963c32e85d4bd2b1bd018e76b720595ed01206569ae47872fa1aad64c63460e22100#npm:4.0.6"]\ + ["p-limit", "npm:7.2.0"],\ + ["tsx", "npm:4.20.6"],\ + ["typescript", "patch:typescript@npm%3A5.9.3#optional!builtin::version=5.9.3&hash=5786d5"],\ + ["vitest", "virtual:6fddcb6bd99765bac7f2b290599c78196c6e6e0b69854e91620b6ccb38ae963c32e85d4bd2b1bd018e76b720595ed01206569ae47872fa1aad64c63460e22100#npm:4.0.6"],\ + ["whois-json", "npm:2.0.4"]\ ],\ "linkType": "SOFT"\ }]\ ]],\ + ["p-limit", [\ + ["npm:2.3.0", {\ + "packageLocation": "../../.yarn/berry/cache/p-limit-npm-2.3.0-94a0310039-10c0.zip/node_modules/p-limit/",\ + "packageDependencies": [\ + ["p-limit", "npm:2.3.0"],\ + ["p-try", "npm:2.2.0"]\ + ],\ + "linkType": "HARD"\ + }],\ + ["npm:7.2.0", {\ + "packageLocation": "../../.yarn/berry/cache/p-limit-npm-7.2.0-72063c9642-10c0.zip/node_modules/p-limit/",\ + "packageDependencies": [\ + ["p-limit", "npm:7.2.0"],\ + ["yocto-queue", "npm:1.2.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["p-locate", [\ + ["npm:4.1.0", {\ + "packageLocation": "../../.yarn/berry/cache/p-locate-npm-4.1.0-eec6872537-10c0.zip/node_modules/p-locate/",\ + "packageDependencies": [\ + ["p-limit", "npm:2.3.0"],\ + ["p-locate", "npm:4.1.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["p-map", [\ ["npm:7.0.3", {\ "packageLocation": "../../.yarn/berry/cache/p-map-npm-7.0.3-93bbec0d8c-10c0.zip/node_modules/p-map/",\ @@ -1724,6 +2036,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["p-try", [\ + ["npm:2.2.0", {\ + "packageLocation": "../../.yarn/berry/cache/p-try-npm-2.2.0-e0390dbaf8-10c0.zip/node_modules/p-try/",\ + "packageDependencies": [\ + ["p-try", "npm:2.2.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["package-json-from-dist", [\ ["npm:1.0.1", {\ "packageLocation": "../../.yarn/berry/cache/package-json-from-dist-npm-1.0.1-4631a88465-10c0.zip/node_modules/package-json-from-dist/",\ @@ -1733,6 +2054,46 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["param-case", [\ + ["npm:2.1.1", {\ + "packageLocation": "../../.yarn/berry/cache/param-case-npm-2.1.1-e0aef3c289-10c0.zip/node_modules/param-case/",\ + "packageDependencies": [\ + ["no-case", "npm:2.3.2"],\ + ["param-case", "npm:2.1.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["pascal-case", [\ + ["npm:2.0.1", {\ + "packageLocation": "../../.yarn/berry/cache/pascal-case-npm-2.0.1-97fc825dec-10c0.zip/node_modules/pascal-case/",\ + "packageDependencies": [\ + ["camel-case", "npm:3.0.0"],\ + ["pascal-case", "npm:2.0.1"],\ + ["upper-case-first", "npm:1.1.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["path-case", [\ + ["npm:2.1.1", {\ + "packageLocation": "../../.yarn/berry/cache/path-case-npm-2.1.1-fafa84599b-10c0.zip/node_modules/path-case/",\ + "packageDependencies": [\ + ["no-case", "npm:2.3.2"],\ + ["path-case", "npm:2.1.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["path-exists", [\ + ["npm:4.0.0", {\ + "packageLocation": "../../.yarn/berry/cache/path-exists-npm-4.0.0-e9e4f63eb0-10c0.zip/node_modules/path-exists/",\ + "packageDependencies": [\ + ["path-exists", "npm:4.0.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["path-key", [\ ["npm:3.1.1", {\ "packageLocation": "../../.yarn/berry/cache/path-key-npm-3.1.1-0e66ea8321-10c0.zip/node_modules/path-key/",\ @@ -1812,6 +2173,33 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["punycode", [\ + ["npm:2.3.1", {\ + "packageLocation": "../../.yarn/berry/cache/punycode-npm-2.3.1-97543c420d-10c0.zip/node_modules/punycode/",\ + "packageDependencies": [\ + ["punycode", "npm:2.3.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["require-directory", [\ + ["npm:2.1.1", {\ + "packageLocation": "../../.yarn/berry/cache/require-directory-npm-2.1.1-8608aee50b-10c0.zip/node_modules/require-directory/",\ + "packageDependencies": [\ + ["require-directory", "npm:2.1.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["require-main-filename", [\ + ["npm:2.0.0", {\ + "packageLocation": "../../.yarn/berry/cache/require-main-filename-npm-2.0.0-03eef65c84-10c0.zip/node_modules/require-main-filename/",\ + "packageDependencies": [\ + ["require-main-filename", "npm:2.0.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["resolve-pkg-maps", [\ ["npm:1.0.0", {\ "packageLocation": "../../.yarn/berry/cache/resolve-pkg-maps-npm-1.0.0-135b70c854-10c0.zip/node_modules/resolve-pkg-maps/",\ @@ -1881,6 +2269,26 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["sentence-case", [\ + ["npm:2.1.1", {\ + "packageLocation": "../../.yarn/berry/cache/sentence-case-npm-2.1.1-ffe9ddf186-10c0.zip/node_modules/sentence-case/",\ + "packageDependencies": [\ + ["no-case", "npm:2.3.2"],\ + ["sentence-case", "npm:2.1.1"],\ + ["upper-case-first", "npm:1.1.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["set-blocking", [\ + ["npm:2.0.0", {\ + "packageLocation": "../../.yarn/berry/cache/set-blocking-npm-2.0.0-49e2cffa24-10c0.zip/node_modules/set-blocking/",\ + "packageDependencies": [\ + ["set-blocking", "npm:2.0.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["shebang-command", [\ ["npm:2.0.0", {\ "packageLocation": "../../.yarn/berry/cache/shebang-command-npm-2.0.0-eb2b01921d-10c0.zip/node_modules/shebang-command/",\ @@ -1927,6 +2335,16 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["snake-case", [\ + ["npm:2.1.0", {\ + "packageLocation": "../../.yarn/berry/cache/snake-case-npm-2.1.0-4134611dfc-10c0.zip/node_modules/snake-case/",\ + "packageDependencies": [\ + ["no-case", "npm:2.3.2"],\ + ["snake-case", "npm:2.1.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["socks", [\ ["npm:2.8.7", {\ "packageLocation": "../../.yarn/berry/cache/socks-npm-2.8.7-d1d20aae19-10c0.zip/node_modules/socks/",\ @@ -1943,7 +2361,7 @@ const RAW_RUNTIME_STATE = "packageLocation": "../../.yarn/berry/cache/socks-proxy-agent-npm-8.0.5-24d77a90dc-10c0.zip/node_modules/socks-proxy-agent/",\ "packageDependencies": [\ ["agent-base", "npm:7.1.4"],\ - ["debug", "virtual:e18ad1aaaee71856c8618c92ebbcee187ad490da91f816fa2cd1abd550e96735d3b7ca6820bb15ac7883b0178149f52bbfa078bf86f89229e4aed2f3ed5555cc#npm:4.4.3"],\ + ["debug", "virtual:643ed7cc338bcf145a82d8b05b3bef6bcf150ca545df386225596f10ce53cc90b88b3ca83e348ade1ccea5f3f8e76c92d2f0e2ba544da60d40aff9921c56872d#npm:4.4.3"],\ ["socks", "npm:2.8.7"],\ ["socks-proxy-agent", "npm:8.0.5"]\ ],\ @@ -2037,6 +2455,17 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["swap-case", [\ + ["npm:1.1.2", {\ + "packageLocation": "../../.yarn/berry/cache/swap-case-npm-1.1.2-2d186deabd-10c0.zip/node_modules/swap-case/",\ + "packageDependencies": [\ + ["lower-case", "npm:1.1.4"],\ + ["swap-case", "npm:1.1.2"],\ + ["upper-case", "npm:1.1.3"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["tar", [\ ["npm:7.5.2", {\ "packageLocation": "../../.yarn/berry/cache/tar-npm-7.5.2-6d8cfb7a13-10c0.zip/node_modules/tar/",\ @@ -2089,6 +2518,17 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["title-case", [\ + ["npm:2.1.1", {\ + "packageLocation": "../../.yarn/berry/cache/title-case-npm-2.1.1-d828015841-10c0.zip/node_modules/title-case/",\ + "packageDependencies": [\ + ["no-case", "npm:2.3.2"],\ + ["title-case", "npm:2.1.1"],\ + ["upper-case", "npm:1.1.3"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["tsx", [\ ["npm:4.20.6", {\ "packageLocation": "../../.yarn/berry/cache/tsx-npm-4.20.6-78231068b5-10c0.zip/node_modules/tsx/",\ @@ -2110,11 +2550,20 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["underscore", [\ + ["npm:1.13.7", {\ + "packageLocation": "../../.yarn/berry/cache/underscore-npm-1.13.7-f57feeae48-10c0.zip/node_modules/underscore/",\ + "packageDependencies": [\ + ["underscore", "npm:1.13.7"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["undici-types", [\ - ["npm:6.21.0", {\ - "packageLocation": "../../.yarn/berry/cache/undici-types-npm-6.21.0-eb2b0ed56a-10c0.zip/node_modules/undici-types/",\ + ["npm:7.16.0", {\ + "packageLocation": "../../.yarn/berry/cache/undici-types-npm-7.16.0-0e23b08124-10c0.zip/node_modules/undici-types/",\ "packageDependencies": [\ - ["undici-types", "npm:6.21.0"]\ + ["undici-types", "npm:7.16.0"]\ ],\ "linkType": "HARD"\ }]\ @@ -2139,6 +2588,25 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["upper-case", [\ + ["npm:1.1.3", {\ + "packageLocation": "../../.yarn/berry/cache/upper-case-npm-1.1.3-061d82781f-10c0.zip/node_modules/upper-case/",\ + "packageDependencies": [\ + ["upper-case", "npm:1.1.3"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["upper-case-first", [\ + ["npm:1.1.2", {\ + "packageLocation": "../../.yarn/berry/cache/upper-case-first-npm-1.1.2-a07735d821-10c0.zip/node_modules/upper-case-first/",\ + "packageDependencies": [\ + ["upper-case", "npm:1.1.3"],\ + ["upper-case-first", "npm:1.1.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["vite", [\ ["npm:7.1.12", {\ "packageLocation": "../../.yarn/berry/cache/vite-npm-7.1.12-4c5705516c-10c0.zip/node_modules/vite/",\ @@ -2153,7 +2621,7 @@ const RAW_RUNTIME_STATE = ["@types/jiti", null],\ ["@types/less", null],\ ["@types/lightningcss", null],\ - ["@types/node", "npm:22.19.0"],\ + ["@types/node", "npm:24.10.9"],\ ["@types/sass", null],\ ["@types/sass-embedded", null],\ ["@types/stylus", null],\ @@ -2222,7 +2690,7 @@ const RAW_RUNTIME_STATE = ["@types/edge-runtime__vm", null],\ ["@types/happy-dom", null],\ ["@types/jsdom", null],\ - ["@types/node", "npm:22.19.0"],\ + ["@types/node", "npm:24.10.9"],\ ["@types/vitest__browser-playwright", null],\ ["@types/vitest__browser-preview", null],\ ["@types/vitest__browser-webdriverio", null],\ @@ -2238,7 +2706,7 @@ const RAW_RUNTIME_STATE = ["@vitest/spy", "npm:4.0.6"],\ ["@vitest/ui", null],\ ["@vitest/utils", "npm:4.0.6"],\ - ["debug", "virtual:e18ad1aaaee71856c8618c92ebbcee187ad490da91f816fa2cd1abd550e96735d3b7ca6820bb15ac7883b0178149f52bbfa078bf86f89229e4aed2f3ed5555cc#npm:4.4.3"],\ + ["debug", "virtual:643ed7cc338bcf145a82d8b05b3bef6bcf150ca545df386225596f10ce53cc90b88b3ca83e348ade1ccea5f3f8e76c92d2f0e2ba544da60d40aff9921c56872d#npm:4.4.3"],\ ["es-module-lexer", "npm:1.7.0"],\ ["expect-type", "npm:1.2.2"],\ ["happy-dom", null],\ @@ -2294,6 +2762,41 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["which-module", [\ + ["npm:2.0.1", {\ + "packageLocation": "../../.yarn/berry/cache/which-module-npm-2.0.1-90f889f6f6-10c0.zip/node_modules/which-module/",\ + "packageDependencies": [\ + ["which-module", "npm:2.0.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["whois", [\ + ["npm:2.15.0", {\ + "packageLocation": "../../.yarn/berry/cache/whois-npm-2.15.0-a23ba8bc81-10c0.zip/node_modules/whois/",\ + "packageDependencies": [\ + ["punycode", "npm:2.3.1"],\ + ["socks", "npm:2.8.7"],\ + ["underscore", "npm:1.13.7"],\ + ["whois", "npm:2.15.0"],\ + ["yargs", "npm:15.4.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["whois-json", [\ + ["npm:2.0.4", {\ + "packageLocation": "../../.yarn/berry/cache/whois-json-npm-2.0.4-4dda81b20d-10c0.zip/node_modules/whois-json/",\ + "packageDependencies": [\ + ["change-case", "npm:3.1.0"],\ + ["dedent-js", "npm:1.0.1"],\ + ["html-entities", "npm:1.4.0"],\ + ["whois", "npm:2.15.0"],\ + ["whois-json", "npm:2.0.4"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["why-is-node-running", [\ ["npm:2.3.0", {\ "packageLocation": "../../.yarn/berry/cache/why-is-node-running-npm-2.3.0-011cf61a18-10c0.zip/node_modules/why-is-node-running/",\ @@ -2306,6 +2809,16 @@ const RAW_RUNTIME_STATE = }]\ ]],\ ["wrap-ansi", [\ + ["npm:6.2.0", {\ + "packageLocation": "../../.yarn/berry/cache/wrap-ansi-npm-6.2.0-439a7246d8-10c0.zip/node_modules/wrap-ansi/",\ + "packageDependencies": [\ + ["ansi-styles", "npm:4.3.0"],\ + ["string-width", "npm:4.2.3"],\ + ["strip-ansi", "npm:6.0.1"],\ + ["wrap-ansi", "npm:6.2.0"]\ + ],\ + "linkType": "HARD"\ + }],\ ["npm:7.0.0", {\ "packageLocation": "../../.yarn/berry/cache/wrap-ansi-npm-7.0.0-ad6e1a0554-10c0.zip/node_modules/wrap-ansi/",\ "packageDependencies": [\ @@ -2327,6 +2840,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["y18n", [\ + ["npm:4.0.3", {\ + "packageLocation": "../../.yarn/berry/cache/y18n-npm-4.0.3-ced95acdbc-10c0.zip/node_modules/y18n/",\ + "packageDependencies": [\ + ["y18n", "npm:4.0.3"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["yallist", [\ ["npm:4.0.0", {\ "packageLocation": "../../.yarn/berry/cache/yallist-npm-4.0.0-b493d9e907-10c0.zip/node_modules/yallist/",\ @@ -2342,6 +2864,46 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "HARD"\ }]\ + ]],\ + ["yargs", [\ + ["npm:15.4.1", {\ + "packageLocation": "../../.yarn/berry/cache/yargs-npm-15.4.1-ca1c444de1-10c0.zip/node_modules/yargs/",\ + "packageDependencies": [\ + ["cliui", "npm:6.0.0"],\ + ["decamelize", "npm:1.2.0"],\ + ["find-up", "npm:4.1.0"],\ + ["get-caller-file", "npm:2.0.5"],\ + ["require-directory", "npm:2.1.1"],\ + ["require-main-filename", "npm:2.0.0"],\ + ["set-blocking", "npm:2.0.0"],\ + ["string-width", "npm:4.2.3"],\ + ["which-module", "npm:2.0.1"],\ + ["y18n", "npm:4.0.3"],\ + ["yargs", "npm:15.4.1"],\ + ["yargs-parser", "npm:18.1.3"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["yargs-parser", [\ + ["npm:18.1.3", {\ + "packageLocation": "../../.yarn/berry/cache/yargs-parser-npm-18.1.3-0ba9c4f088-10c0.zip/node_modules/yargs-parser/",\ + "packageDependencies": [\ + ["camelcase", "npm:5.3.1"],\ + ["decamelize", "npm:1.2.0"],\ + ["yargs-parser", "npm:18.1.3"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["yocto-queue", [\ + ["npm:1.2.1", {\ + "packageLocation": "../../.yarn/berry/cache/yocto-queue-npm-1.2.1-98b92882fa-10c0.zip/node_modules/yocto-queue/",\ + "packageDependencies": [\ + ["yocto-queue", "npm:1.2.1"]\ + ],\ + "linkType": "HARD"\ + }]\ ]]\ ]\ }'; diff --git a/README.md b/README.md index db656c2..cf19335 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,25 @@ Key scripts designed to boost developer productivity: - **Usage**: `rn-fix` - **Saves Time**: 5-10 minutes of manual cleanup per fix +### macOS File Descriptor Management + +**`fix-max-files-temp`** - Temporary File Descriptor Limit + +- **Problem Solved**: "Too many open files" errors when running file watchers or tools that open many file descriptors +- **How It Works**: Applies temporary system-level file descriptor limits for the current session using `launchctl` +- **Usage**: `fix-max-files-temp` +- **Duration**: Session only; resets on reboot +- **Perfect For**: Quick fixes during development sessions + +**`fix-max-files-permanently`** - Permanent File Descriptor Configuration + +- **Problem Solved**: Sets persistent file descriptor limits across reboots +- **How It Works**: Installs LaunchDaemon configuration at `/Library/LaunchDaemons/limit.maxfiles.plist` +- **Usage**: `fix-max-files-permanently` +- **Requirements**: The `limit.maxfiles.plist` file should be located alongside the script +- **Warning**: Will not override an existing configuration file +- **Perfect For**: Permanent solution on development machines + **`rnios` / `rnra`** - iOS/Android Launch Shortcuts - **Usage**: `rnios` (iOS) or `rnra` (Android) diff --git a/apps/watchman/set-max-file-limit.sh b/apps/watchman/set-max-file-limit.sh deleted file mode 100755 index 9f1924f..0000000 --- a/apps/watchman/set-max-file-limit.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash - -MAXFILES_SOFT_LIMIT=1048576 -MAXFILES_HARD_LIMIT=10485760 - -# MacOSX >= 10.6.x permanent fix -fix-max-files-permanently(){ - # do not enable this until the script is tested - # local SYSTCTL_CONF=/etc/sysctl.conf - local LIMIT_CONFIG_FILE=/Library/LaunchDaemons/limit.maxfiles.plist - - if [ ! -f $LIMIT_CONFIG_FILE ]; then - sudo cp ./limit.maxfiles.plist $LIMIT_CONFIG_FILE - sudo chown root:wheel $LIMIT_CONFIG_FILE - else - echo "$LIMIT_CONFIG_FILE already exists. This script will not override a pre-existing file." - fi -} - -fix-max-files-permanently - -echo "adding a temp override for " -# sudo sysctl -w kern.maxfiles=10485760 && sudo sysctl -w kern.maxfilesperproc=$MAXFILES_SOFT_LIMIT -sudo launchctl limit maxfiles $MAXFILES_SOFT_LIMIT $MAXFILES_HARD_LIMIT \ No newline at end of file diff --git a/entry.zsh b/entry.zsh index b14ab91..de8b09a 100755 --- a/entry.zsh +++ b/entry.zsh @@ -132,6 +132,11 @@ if [[ "${OSA_CONFIG_COMPONENTS_MAC_TOOLS}" == "true" ]] || [[ "${OSA_CONFIG_SNIP [[ -f "$OSA_SCRIPTS_ZSH_ROOT/platform/mac/rmAsync.zsh" ]] && source "$OSA_SCRIPTS_ZSH_ROOT/platform/mac/rmAsync.zsh" fi +# macOS File Descriptor Management +if [[ "${OSA_CONFIG_SNIPPETS_OSASNIPPETS_FILELIMITS}" == "true" ]] || [[ "${OSA_CONFIG_SNIPPETS_OSASNIPPETS_FILELIMITS}" == "true" ]]; then + [[ -f "$OSA_SCRIPTS_ZSH_ROOT/platform/mac/set-max-file-limit.sh" ]] && source "$OSA_SCRIPTS_ZSH_ROOT/platform/mac/set-max-file-limit.sh" +fi + # macOS eGPU Management if [[ "${OSA_CONFIG_COMPONENTS_EGPU}" == "true" ]] || [[ "${OSA_CONFIG_SNIPPETS_OSASNIPPETS_EGPU}" == "true" ]]; then [[ -f "$OSA_SCRIPTS_ZSH_ROOT/platform/mac/egpu.zsh" ]] && source "$OSA_SCRIPTS_ZSH_ROOT/platform/mac/egpu.zsh" diff --git a/js/__tests__/audit-auto-approve.test.ts b/js/__tests__/audit-auto-approve.test.ts index 8421b95..4d449de 100644 --- a/js/__tests__/audit-auto-approve.test.ts +++ b/js/__tests__/audit-auto-approve.test.ts @@ -1,8 +1,16 @@ import { readdirSync, readFileSync } from "node:fs"; import { join } from "node:path"; import { exit } from "node:process"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { auditAutoApprove } from "../commands/copilot/audit-auto-approve.js"; +import { + afterAll, + afterEach, + beforeAll, + beforeEach, + describe, + expect, + it, + vi, +} from "vitest"; // Mock fs vi.mock("node:fs", () => ({ @@ -23,9 +31,19 @@ vi.mock("os", () => ({ homedir: vi.fn(() => "/mock-home"), })); +// Spy console early to prevent noisy logs during module import +const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + const mockReaddirSync = vi.mocked(readdirSync); const mockReadFileSync = vi.mocked(readFileSync); +// Import audited module after mocks and console spy are in place +let auditAutoApprove: any; +beforeAll(async () => { + const mod = await import("../commands/copilot/audit-auto-approve.js"); + auditAutoApprove = mod.auditAutoApprove; +}); + describe("auditAutoApprove", () => { beforeEach(() => { vi.clearAllMocks(); @@ -41,9 +59,11 @@ describe("auditAutoApprove", () => { }); afterEach(() => { - vi.restoreAllMocks(); + vi.clearAllMocks(); }); + afterAll(() => {}); + it("should pass when no risky patterns found", async () => { // Mock file structure - return proper Dirent-like objects mockReaddirSync.mockImplementation((dir: string, options?: any) => { @@ -74,8 +94,6 @@ describe("auditAutoApprove", () => { }), ); - const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); - await auditAutoApprove({ allowPrefix: "tachyon", failOnRisk: false, @@ -120,8 +138,6 @@ describe("auditAutoApprove", () => { return "{}"; }); - const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); - await auditAutoApprove({ allowPrefix: "tachyon", failOnRisk: false, @@ -134,7 +150,63 @@ describe("auditAutoApprove", () => { expect(consoleSpy).toHaveBeenCalledWith( " \x1b[31m✖ risky (enabled & not allowed):\x1b[37m :unknown-task\x1b[0m", ); - consoleSpy.mockRestore(); + }); + + it("should detect risky patterns in user settings", async () => { + mockReaddirSync.mockImplementation((dir: string, options?: any) => { + if (dir === "/test") { + return [ + { name: ".vscode", isDirectory: () => true, isFile: () => false }, + ] as any; + } + if (dir === "/test/.vscode") { + return [ + { + name: "settings.json", + isDirectory: () => false, + isFile: () => true, + }, + ] as any; + } + return []; + }); + + mockReadFileSync.mockImplementation((path: string) => { + if ( + path === + "/mock-home/Library/Application Support/Code/User/settings.json" + ) { + return JSON.stringify({ + "chat.tools.terminal.autoApprove": { + ":risky-task": true, + }, + }); + } + if (path === "/test/.vscode/settings.json") { + return JSON.stringify({ + "chat.tools.terminal.autoApprove": { + ":safe-task": true, + }, + }); + } + return "{}"; + }); + + await auditAutoApprove({ + allowPrefix: "safe", + failOnRisk: false, + json: false, + silent: false, + }); + + expect(consoleSpy).toHaveBeenCalledWith("⚠️ Found issues in 2 file(s):"); + expect(consoleSpy).toHaveBeenCalledWith( + "- /mock-home/Library/Application Support/Code/User/settings.json", + ); + expect(consoleSpy).toHaveBeenCalledWith( + " \x1b[31m✖ risky (enabled & not allowed):\x1b[37m :risky-task\x1b[0m", + ); + expect(consoleSpy).toHaveBeenCalledWith("- /test/.vscode/settings.json"); }); it("should exit with code 1 when failOnRisk is true and risks found", async () => { @@ -206,8 +278,6 @@ describe("auditAutoApprove", () => { }), ); - const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); - await auditAutoApprove({ allowPrefix: "safe", failOnRisk: false, @@ -235,7 +305,6 @@ describe("auditAutoApprove", () => { 2, ), ); - consoleSpy.mockRestore(); }); it("should output JSON when json option is true and there are findings", async () => { @@ -274,8 +343,6 @@ describe("auditAutoApprove", () => { return "{}"; }); - const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); - await auditAutoApprove({ allowPrefix: "safe", failOnRisk: false, @@ -316,7 +383,6 @@ describe("auditAutoApprove", () => { 2, ), ); - consoleSpy.mockRestore(); }); it("should include prefix health in JSON output", async () => { @@ -355,8 +421,6 @@ describe("auditAutoApprove", () => { return "{}"; }); - const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); - await auditAutoApprove({ allowPrefix: undefined, failOnRisk: false, @@ -398,7 +462,6 @@ describe("auditAutoApprove", () => { 2, ), ); - consoleSpy.mockRestore(); }); it("should handle prefix with all risky patterns", async () => { @@ -437,8 +500,6 @@ describe("auditAutoApprove", () => { return "{}"; }); - const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); - await auditAutoApprove({ allowPrefix: undefined, failOnRisk: false, @@ -461,7 +522,6 @@ describe("auditAutoApprove", () => { expect(consoleSpy).toHaveBeenCalledWith( " \x1b[31m✖ risky (enabled & not allowed):\x1b[37m :bad:rm -rf *\x1b[0m", ); - consoleSpy.mockRestore(); }); it("should handle prefix with many risky patterns", async () => { @@ -502,8 +562,6 @@ describe("auditAutoApprove", () => { return "{}"; }); - const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); - await auditAutoApprove({ allowPrefix: undefined, failOnRisk: false, @@ -532,7 +590,6 @@ describe("auditAutoApprove", () => { expect(consoleSpy).toHaveBeenCalledWith( " \x1b[31m✖ risky (enabled & not allowed):\x1b[37m :many:rm -rf ~\x1b[0m", ); - consoleSpy.mockRestore(); }); it("should handle multiple prefixes", async () => { @@ -572,8 +629,6 @@ describe("auditAutoApprove", () => { return "{}"; }); - const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); - await auditAutoApprove({ allowPrefix: "prefix1,prefix2", failOnRisk: false, @@ -586,7 +641,6 @@ describe("auditAutoApprove", () => { expect(consoleSpy).toHaveBeenCalledWith( " \x1b[31m✖ risky (enabled & not allowed):\x1b[37m :unknown:task\x1b[0m", ); - consoleSpy.mockRestore(); }); it("should detect non-boolean values", async () => { @@ -618,8 +672,6 @@ describe("auditAutoApprove", () => { }), ); - const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); - await auditAutoApprove({ allowPrefix: "safe", failOnRisk: false, @@ -658,7 +710,6 @@ describe("auditAutoApprove", () => { ); expect(consoleSpy).toHaveBeenCalledWith(expectedOutput); - consoleSpy.mockRestore(); }); it("should work in silent mode", async () => { @@ -688,8 +739,6 @@ describe("auditAutoApprove", () => { }), ); - const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); - await auditAutoApprove({ allowPrefix: "safe", failOnRisk: false, @@ -698,14 +747,11 @@ describe("auditAutoApprove", () => { }); expect(consoleSpy).not.toHaveBeenCalled(); - consoleSpy.mockRestore(); }); it("should handle no settings files found", async () => { mockReaddirSync.mockReturnValue([]); - const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); - await auditAutoApprove({ allowPrefix: "test", failOnRisk: false, @@ -716,7 +762,6 @@ describe("auditAutoApprove", () => { expect(consoleSpy).toHaveBeenCalledWith( "✅ All autoApprove entries are safe for prefixes: test", ); - consoleSpy.mockRestore(); }); it("should handle malformed JSON", async () => { @@ -740,8 +785,6 @@ describe("auditAutoApprove", () => { mockReadFileSync.mockReturnValue("invalid json"); - const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); - await auditAutoApprove({ allowPrefix: "test", failOnRisk: false, @@ -752,7 +795,6 @@ describe("auditAutoApprove", () => { expect(consoleSpy).toHaveBeenCalledWith( "✅ All autoApprove entries are safe for prefixes: test", ); - consoleSpy.mockRestore(); }); it("should handle multiple settings files", async () => { @@ -781,8 +823,8 @@ describe("auditAutoApprove", () => { return [ { name: "settings.json", - isDirectory: () => false, - isFile: () => true, + isDirectory: () => false, + isFile: () => true, }, ] as any; } @@ -812,8 +854,6 @@ describe("auditAutoApprove", () => { } }); - const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); - await auditAutoApprove({ allowPrefix: "safe", failOnRisk: false, @@ -828,14 +868,11 @@ describe("auditAutoApprove", () => { expect(consoleSpy).toHaveBeenCalledWith( " \x1b[31m✖ risky (enabled & not allowed):\x1b[37m :risky:task\x1b[0m", ); - consoleSpy.mockRestore(); }); it("should handle empty allowPrefix", async () => { mockReaddirSync.mockReturnValue([]); - const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); - await auditAutoApprove({ allowPrefix: "", failOnRisk: false, @@ -848,7 +885,6 @@ describe("auditAutoApprove", () => { expect(consoleSpy).toHaveBeenCalledWith( "✅ All autoApprove entries are safe", ); - consoleSpy.mockRestore(); }); it("should handle missing autoApprove setting", async () => { @@ -878,8 +914,6 @@ describe("auditAutoApprove", () => { }), ); - const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); - await auditAutoApprove({ allowPrefix: "test", failOnRisk: false, @@ -890,7 +924,6 @@ describe("auditAutoApprove", () => { expect(consoleSpy).toHaveBeenCalledWith( "✅ All autoApprove entries are safe for prefixes: test", ); - consoleSpy.mockRestore(); }); it("should always show warnings when failing even in silent mode", async () => { @@ -928,7 +961,6 @@ describe("auditAutoApprove", () => { return "{}"; }); - const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); vi.mocked(exit).mockImplementation(() => { throw new Error("process.exit called"); }); @@ -947,7 +979,6 @@ describe("auditAutoApprove", () => { expect(consoleSpy).toHaveBeenCalledWith( " \x1b[31m✖ risky (enabled & not allowed):\x1b[37m :risky:task\x1b[0m", ); - consoleSpy.mockRestore(); }); it("should apply correct ANSI color codes to failure messages", async () => { @@ -986,8 +1017,6 @@ describe("auditAutoApprove", () => { return "{}"; }); - const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); - await auditAutoApprove({ allowPrefix: "safe", failOnRisk: false, @@ -1001,7 +1030,6 @@ describe("auditAutoApprove", () => { expect(consoleSpy).toHaveBeenCalledWith( " \x1b[31m✖ risky (enabled & not allowed):\x1b[37m :dangerous:rm -rf /\x1b[0m", ); - consoleSpy.mockRestore(); }); it("should provide comprehensive prefix health reporting", async () => { @@ -1045,8 +1073,6 @@ describe("auditAutoApprove", () => { return "{}"; }); - const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); - await auditAutoApprove({ allowPrefix: undefined, // No explicit prefix - should scan all failOnRisk: false, @@ -1057,7 +1083,9 @@ describe("auditAutoApprove", () => { expect(consoleSpy).toHaveBeenCalledWith("\n📊 Prefix Health Report:"); expect(consoleSpy).toHaveBeenCalledWith("========================"); - expect(consoleSpy).toHaveBeenCalledWith("❌ dangerous: 0/4 safe (100% risky)"); + expect(consoleSpy).toHaveBeenCalledWith( + "❌ dangerous: 0/4 safe (100% risky)", + ); expect(consoleSpy).toHaveBeenCalledWith( " Risky patterns: :dangerous:rm -rf /, :dangerous:sudo rm, :dangerous:rm -rf *...", ); @@ -1076,7 +1104,6 @@ describe("auditAutoApprove", () => { expect(consoleSpy).toHaveBeenCalledWith( " \x1b[31m✖ risky (enabled & not allowed):\x1b[37m :dangerous:rm -rf .\x1b[0m", ); - consoleSpy.mockRestore(); }); it("should handle complex prefix patterns correctly", async () => { @@ -1119,8 +1146,6 @@ describe("auditAutoApprove", () => { return "{}"; }); - const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); - await auditAutoApprove({ allowPrefix: "gradle,npm", failOnRisk: false, @@ -1133,7 +1158,6 @@ describe("auditAutoApprove", () => { expect(consoleSpy).toHaveBeenCalledWith( " \x1b[31m✖ risky (enabled & not allowed):\x1b[37m :unknown:command\x1b[0m", ); - consoleSpy.mockRestore(); }); it("should handle large numbers of patterns efficiently", async () => { @@ -1176,8 +1200,6 @@ describe("auditAutoApprove", () => { return "{}"; }); - const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); - await auditAutoApprove({ allowPrefix: "safe", failOnRisk: false, @@ -1190,7 +1212,6 @@ describe("auditAutoApprove", () => { expect(consoleSpy).toHaveBeenCalledWith( " \x1b[31m✖ risky (enabled & not allowed):\x1b[37m :risky:rm\x1b[0m", ); - consoleSpy.mockRestore(); }); it("should handle syntax validation errors", async () => { @@ -1230,8 +1251,6 @@ describe("auditAutoApprove", () => { return "{}"; }); - const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); - await auditAutoApprove({ allowPrefix: "safe,malformed,another", failOnRisk: false, @@ -1250,7 +1269,6 @@ describe("auditAutoApprove", () => { expect(consoleSpy).toHaveBeenCalledWith( " \x1b[31m✖ risky (enabled & not allowed):\x1b[37m :malformed[pattern\x1b[0m", ); - consoleSpy.mockRestore(); }); it("should handle specific settings file path", async () => { @@ -1265,8 +1283,6 @@ describe("auditAutoApprove", () => { return "{}"; }); - const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); - await auditAutoApprove({ settingsFile: "/custom/settings.json", allowPrefix: "safe", @@ -1280,7 +1296,6 @@ describe("auditAutoApprove", () => { expect(consoleSpy).toHaveBeenCalledWith( " \x1b[31m✖ risky (enabled & not allowed):\x1b[37m :risky:rm\x1b[0m", ); - consoleSpy.mockRestore(); }); it("should handle empty autoApprove object", async () => { @@ -1316,8 +1331,6 @@ describe("auditAutoApprove", () => { return "{}"; }); - const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); - await auditAutoApprove({ allowPrefix: "safe", failOnRisk: false, @@ -1328,7 +1341,6 @@ describe("auditAutoApprove", () => { expect(consoleSpy).toHaveBeenCalledWith( "✅ All autoApprove entries are safe for prefixes: safe", ); - consoleSpy.mockRestore(); }); it("should handle null autoApprove setting", async () => { @@ -1364,8 +1376,6 @@ describe("auditAutoApprove", () => { return "{}"; }); - const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); - await auditAutoApprove({ allowPrefix: "safe", failOnRisk: false, @@ -1376,7 +1386,6 @@ describe("auditAutoApprove", () => { expect(consoleSpy).toHaveBeenCalledWith( "✅ All autoApprove entries are safe for prefixes: safe", ); - consoleSpy.mockRestore(); }); it("should handle invalid autoApprove setting type", async () => { @@ -1412,8 +1421,6 @@ describe("auditAutoApprove", () => { return "{}"; }); - const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); - await auditAutoApprove({ allowPrefix: "safe", failOnRisk: false, @@ -1424,7 +1431,6 @@ describe("auditAutoApprove", () => { expect(consoleSpy).toHaveBeenCalledWith( "✅ All autoApprove entries are safe for prefixes: safe", ); - consoleSpy.mockRestore(); }); it("should handle deeply nested directory structures", async () => { @@ -1472,8 +1478,6 @@ describe("auditAutoApprove", () => { return "{}"; }); - const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); - await auditAutoApprove({ allowPrefix: "safe", failOnRisk: false, @@ -1488,7 +1492,6 @@ describe("auditAutoApprove", () => { expect(consoleSpy).toHaveBeenCalledWith( " \x1b[31m✖ risky (enabled & not allowed):\x1b[37m :risky:rm\x1b[0m", ); - consoleSpy.mockRestore(); }); it("should handle mixed valid and invalid patterns", async () => { @@ -1531,8 +1534,6 @@ describe("auditAutoApprove", () => { return "{}"; }); - const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); - await auditAutoApprove({ allowPrefix: "safe,another", failOnRisk: false, @@ -1594,7 +1595,6 @@ describe("auditAutoApprove", () => { ); expect(consoleSpy).toHaveBeenCalledWith(expectedOutput); - consoleSpy.mockRestore(); }); it("should detect directory traversal in patterns", async () => { @@ -1632,8 +1632,6 @@ describe("auditAutoApprove", () => { return "{}"; }); - const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); - await auditAutoApprove({ allowPrefix: "safe", failOnRisk: false, @@ -1646,7 +1644,6 @@ describe("auditAutoApprove", () => { expect(consoleSpy).toHaveBeenCalledWith( " \x1b[31m✖ risky (enabled & not allowed):\x1b[37m syntax issues: directory traversal (..)\x1b[0m", ); - consoleSpy.mockRestore(); }); it("should detect null bytes in patterns", async () => { @@ -1684,8 +1681,6 @@ describe("auditAutoApprove", () => { return "{}"; }); - const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); - await auditAutoApprove({ allowPrefix: "safe", failOnRisk: false, @@ -1698,7 +1693,6 @@ describe("auditAutoApprove", () => { expect(consoleSpy).toHaveBeenCalledWith( " \x1b[31m✖ risky (enabled & not allowed):\x1b[37m syntax issues: null bytes detected\x1b[0m", ); - consoleSpy.mockRestore(); }); it("should detect extremely long patterns", async () => { @@ -1737,8 +1731,6 @@ describe("auditAutoApprove", () => { return "{}"; }); - const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); - await auditAutoApprove({ allowPrefix: "safe", failOnRisk: false, @@ -1751,7 +1743,6 @@ describe("auditAutoApprove", () => { expect(consoleSpy).toHaveBeenCalledWith( ` \x1b[31m✖ risky (enabled & not allowed):\x1b[37m syntax issues: pattern too long\x1b[0m`, ); - consoleSpy.mockRestore(); }); it("should output JSON when json option is true and there are findings", async () => { @@ -1790,8 +1781,6 @@ describe("auditAutoApprove", () => { return "{}"; }); - const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); - await auditAutoApprove({ allowPrefix: "safe", failOnRisk: false, @@ -1832,7 +1821,6 @@ describe("auditAutoApprove", () => { ); expect(consoleSpy).toHaveBeenCalledWith(expectedOutput); - consoleSpy.mockRestore(); }); it("should not show warnings in silent mode when not failing", async () => { @@ -1870,8 +1858,6 @@ describe("auditAutoApprove", () => { return "{}"; }); - const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); - await auditAutoApprove({ allowPrefix: "safe", failOnRisk: false, // Not failing @@ -1880,12 +1866,15 @@ describe("auditAutoApprove", () => { }); // Should not show warnings since silent and not failing - expect(consoleSpy).not.toHaveBeenCalledWith("⚠️ Found issues in 1 file(s):"); - expect(consoleSpy).not.toHaveBeenCalledWith("- /test/.vscode/settings.json"); + expect(consoleSpy).not.toHaveBeenCalledWith( + "⚠️ Found issues in 1 file(s):", + ); + expect(consoleSpy).not.toHaveBeenCalledWith( + "- /test/.vscode/settings.json", + ); expect(consoleSpy).not.toHaveBeenCalledWith( " \x1b[31m✖ risky (enabled & not allowed):\x1b[37m :risky:task\x1b[0m", ); - consoleSpy.mockRestore(); }); it("should detect unterminated quotes in patterns", async () => { @@ -1917,7 +1906,7 @@ describe("auditAutoApprove", () => { return JSON.stringify({ "chat.tools.terminal.autoApprove": { ":safe:echo 'unterminated": true, - ":safe:echo \"double": true, + ':safe:echo "double': true, ":safe:echo `backtick": true, }, }); @@ -1925,8 +1914,6 @@ describe("auditAutoApprove", () => { return "{}"; }); - const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); - await auditAutoApprove({ allowPrefix: "safe", failOnRisk: false, @@ -1945,7 +1932,6 @@ describe("auditAutoApprove", () => { expect(consoleSpy).toHaveBeenCalledWith( " \x1b[31m✖ risky (enabled & not allowed):\x1b[37m syntax issues: unterminated backticks\x1b[0m", ); - consoleSpy.mockRestore(); }); it("should detect unbalanced brackets and parentheses in patterns", async () => { @@ -1985,8 +1971,6 @@ describe("auditAutoApprove", () => { return "{}"; }); - const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); - await auditAutoApprove({ allowPrefix: "safe", failOnRisk: false, @@ -2005,6 +1989,5 @@ describe("auditAutoApprove", () => { expect(consoleSpy).toHaveBeenCalledWith( " \x1b[31m✖ risky (enabled & not allowed):\x1b[37m syntax issues: unbalanced braces\x1b[0m", ); - consoleSpy.mockRestore(); }); }); diff --git a/js/__tests__/domain-availability-checker.test.ts b/js/__tests__/domain-availability-checker.test.ts new file mode 100644 index 0000000..f75e792 --- /dev/null +++ b/js/__tests__/domain-availability-checker.test.ts @@ -0,0 +1,450 @@ +import { Command } from "commander"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +// Mock fs and whois-json before importing the module under test +vi.mock("node:fs", () => { + const readFileSync = vi.fn(); + const writeFileSync = vi.fn(); + return { + readFileSync, + writeFileSync, + default: { readFileSync, writeFileSync }, + }; +}); + +vi.mock("whois-json", () => ({ + default: vi.fn(), +})); + +const mockYamlLoad = vi.fn(); + +import yaml from "js-yaml"; + +const yamlLoadSpy = vi.spyOn(yaml, "load"); + +import * as fsMock from "node:fs"; +import whois from "whois-json"; + +import { + inferAvailability, + makeCheckDomainsCommand, + readNamesFromInputs, + readTldsFromInputs, + runChecks, + summarizeParsed, + writeResultsCsv, +} from "../commands/domain-availability-checker/domain-availability-checker"; +import * as readYamlConfigFns from "../commands/domain-availability-checker/readYamlConfig"; + +const mockReadYamlConfig = vi.spyOn(readYamlConfigFns, "readYamlConfig"); +const { readYamlConfig } = readYamlConfigFns; + +describe("Domain Availability Checker (single-file tests)", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("readYamlConfig returns parsed YAML when filePath is relative and read succeeds", () => { + mockYamlLoad.mockReturnValueOnce(null); + (fsMock as any).readFileSync.mockReturnValueOnce(""); + const cfg = readYamlConfig("config.yml"); + expect(cfg).toBeNull(); + }); + + it("readYamlConfig parses YAML and falls back when relative path", () => { + (fsMock as any).readFileSync.mockReturnValueOnce("names:\n - a\n"); + const cfg = readYamlConfig("./some.yml"); + expect(cfg).not.toBeNull(); + expect((cfg as any).names).toContain("a"); + }); + + it("readNamesFromInputs reads from array and file and normalizes", () => { + (fsMock as any).readFileSync.mockReturnValue("name1\nname2\n"); + const names = readNamesFromInputs(["ARG"], "file.txt"); + expect(names).toEqual(expect.arrayContaining(["arg", "name1", "name2"])); + }); + + it("readTldsFromInputs normalizes and dedups", () => { + (fsMock as any).readFileSync.mockReturnValueOnce("tlds:\n - .Org\n"); + const tlds = readTldsFromInputs([".DEV"], "file.yml"); + expect(tlds).toEqual(expect.arrayContaining(["dev", "org"])); + }); + + it("inferAvailability covers registered/available/unknown branches", () => { + expect(inferAvailability(null)).toBe("likely-available"); + expect(inferAvailability({ domainName: "x" })).toBe("registered"); + expect(inferAvailability({}, "No match for domain")).toBe( + "likely-available", + ); + expect(inferAvailability({ registrar: "r" })).toBe("registered"); + expect(inferAvailability({ foo: "bar" })).toBe("unknown"); + }); + + it("summarizeParsed picks fields and truncates", () => { + const parsed = { + domainName: "example.com", + Registrar: "r", + status: "a".repeat(100), + }; + const out = summarizeParsed(parsed as any); + expect(out).toContain("domainName:"); + expect(out).toContain("Registrar:"); + expect(out).toContain("status:"); + expect(out.length).toBeGreaterThan(100); // includes truncated status + }); + + it("writeResultsCsv calls fs.writeFileSync", () => { + const results = [ + { + domain: "a.com", + status: "registered", + elapsedMs: 1, + parsedSummary: "", + rawSnippet: "", + }, + ]; + writeResultsCsv({ outFile: "/tmp/out.csv", results, writeHeader: true }); + expect((fsMock as any).writeFileSync).toHaveBeenCalled(); + }); + + it("runChecks handles timeout, reject and empty-parsed paths", async () => { + const whoisFn = (whois as any).default ?? whois; + // timeout (rejects with timeout) + whoisFn.mockImplementationOnce(() => Promise.reject(new Error("timeout"))); + let res = await runChecks({ + names: ["slow"], + tlds: ["test"], + concurrency: 1, + timeoutMs: 1, + }); + expect(res[0].status).toBe("likely-available"); + + // reject -> should be handled and considered likely-available per inferAvailability + whoisFn.mockImplementationOnce(() => Promise.reject(new Error("boom"))); + res = await runChecks({ + names: ["err"], + tlds: ["com"], + concurrency: 1, + timeoutMs: 100, + }); + expect(res[0].status).toBe("likely-available"); + + // resolved empty parsed + no raw -> unknown + whoisFn.mockResolvedValueOnce({}); + res = await runChecks({ + names: ["u"], + tlds: ["io"], + concurrency: 1, + timeoutMs: 100, + }); + expect(res[0].status).toBe("unknown"); + }); + + it("CLI command writes json/yaml/csv outputs and prints summary", async () => { + const program = new Command().name("check-domains"); + program.addCommand(makeCheckDomainsCommand()); + + // json + (fsMock as any).writeFileSync.mockClear(); + await program.parseAsync([ + "node", + "x", + "check-domains", + "-n", + "cli-name", + "-t", + "com", + "--format", + "json", + "-o", + "outtest", + "--no-header", + ]); + expect((fsMock as any).writeFileSync).toHaveBeenCalled(); + + // yaml + (fsMock as any).writeFileSync.mockClear(); + await program.parseAsync([ + "node", + "x", + "check-domains", + "-n", + "cli-name", + "-t", + "com", + "--format", + "yaml", + "-o", + "outtest", + "--no-header", + ]); + expect((fsMock as any).writeFileSync).toHaveBeenCalled(); + + // csv + (fsMock as any).writeFileSync.mockClear(); + await program.parseAsync([ + "node", + "x", + "check-domains", + "-n", + "cli-name", + "-t", + "com", + "--format", + "csv", + "-o", + "outtest", + "--no-header", + ]); + expect((fsMock as any).writeFileSync).toHaveBeenCalled(); + }); + + it("readYamlConfig workspace-relative fallback and YAML names branch", () => { + // First call throws (simulate missing path), second returns YAML content + (fsMock as any).readFileSync + .mockImplementationOnce(() => { + throw new Error("not found"); + }) + .mockReturnValueOnce("names:\n - fallback1\n - fallback2\n"); + + const cfg = readYamlConfig("./names.yml"); + expect(cfg).not.toBeNull(); + expect((cfg as any).names).toEqual( + expect.arrayContaining(["fallback1", "fallback2"]), + ); + }); + + it("readYamlConfig returns null when filePath is absolute and read fails", () => { + (fsMock as any).readFileSync.mockImplementation(() => { + throw new Error("fail"); + }); + expect(readYamlConfig("/absolute/path.yml")).toBeNull(); + }); + + it("readYamlConfig handles YAML with no names", () => { + (fsMock as any).readFileSync.mockReturnValue("tlds:\n - com\n"); + const cfg = readYamlConfig("./no-names.yml"); + expect(cfg).toEqual({ tlds: ["com"] }); + }); + const results = [ + { + domain: "b.com", + status: "unknown", + elapsedMs: 2, + parsedSummary: "", + rawSnippet: "", + }, + ]; + writeResultsCsv({ outFile: "/tmp/out2.csv", results, writeHeader: false }); + expect((fsMock as any).writeFileSync).toHaveBeenCalled(); +}); + +it("readNamesFromInputs handles empty tlds array", () => { + const tlds = readTldsFromInputs([], undefined); + expect(tlds).toEqual([]); +}); + +it("readNamesFromInputs returns null on errors with bad paths", () => { + (fsMock as any).readFileSync.mockImplementation((path: string) => { + throw new Error("not found"); + }); + const names = readNamesFromInputs(["ARG"], ".../test"); + expect(names).toEqual(null); +}); + +it("readNamesFromInputs handles relative filePath with fallback", () => { + (fsMock as any).readFileSync.mockImplementation((path: string) => { + if (path === "./file.txt") throw new Error("not found"); + return "fallback\nname\n"; + }); + const names = readNamesFromInputs(["ARG"], "./file.txt"); + expect(names).toEqual(expect.arrayContaining(["arg", "fallback", "name"])); +}); + +it("makeCheckDomainsCommand propagates provider to whois calls", async () => { + const program = new Command().name("check-domains"); + program.addCommand(makeCheckDomainsCommand()); + + // Ensure whois resolves so the command completes + const whoisFn = (whois as any).default ?? whois; + whoisFn.mockResolvedValue({ domainName: "x.com" }); + + await program.parseAsync([ + "node", + "x", + "check-domains", + "-n", + "cli-name", + "-t", + "com", + "--provider", + "whois.foo", + "--format", + "json", + "-o", + "outtest-pro", + "--no-header", + ]); + + // Verify whois was called with options containing server set to provider + const calls = whoisFn.mock.calls; + const saw = calls.some((c: any[]) => c[1] && c[1].server === "whois.foo"); + expect(saw).toBe(true); +}); + +it("inferAvailability recognizes various 'not found' phrases", () => { + const phrases = [ + "No entries found", + "status: free", + "no data found", + "no such domain", + "no object found", + ]; + for (const p of phrases) { + expect(inferAvailability({}, p)).toBe("likely-available"); + } +}); + +it("CLI reads names/tlds from YAML file when --file is provided", async () => { + const program = new Command().name("check-domains"); + program.addCommand(makeCheckDomainsCommand()); + + // Mock YAML file content with names and tlds + (fsMock as any).readFileSync.mockReturnValue( + "names:\n - fromyaml\n tlds:\n - com\n", + ); + const whoisFn = (whois as any).default ?? whois; + whoisFn.mockResolvedValue({}); + + await program.parseAsync([ + "node", + "x", + "check-domains", + "--file", + "names.yml", + "--format", + "json", + "-o", + "out-yaml", + "--no-header", + ]); + expect((fsMock as any).writeFileSync).toHaveBeenCalled(); +}); + +it("inferAvailability recognizes registrar indicator keys as registered", () => { + const parsed = { "Name Server": "ns1.example.com" }; + expect(inferAvailability(parsed)).toBe("registered"); +}); + +it("inferAvailability handles keys that mention domainname but lack domainName property", () => { + const parsed = { domainnamefoo: "something" }; + // no explicit domainName property -> should fall through to unknown + expect(inferAvailability(parsed)).toBe("unknown"); +}); + +it("inferAvailability returns unknown when domain name key exists but domainName property is falsy", () => { + expect(inferAvailability({ "Domain Name": null })).toBe("unknown"); +}); + +it("inferAvailability returns unknown when rawText provided but no not found phrases match", () => { + expect(inferAvailability({ some: "value" }, "some random text")).toBe( + "unknown", + ); +}); + +it("inferAvailability returns registered when Domain Name property is set", () => { + expect(inferAvailability({ "Domain Name": "example.com" })).toBe( + "registered", + ); +}); + +it("inferAvailability returns registered when domain name property is set", () => { + expect(inferAvailability({ "domain name": "example.com" })).toBe( + "registered", + ); +}); + +it("inferAvailability returns registered when Creation Date is present", () => { + expect(inferAvailability({ "Creation Date": "2020-01-01" })).toBe( + "registered", + ); +}); + +it("readNamesFromInputs reads yaml config with names and tlds from workspace-relative fallback", () => { + const data = { names: ["wsname1", "wsname2"], tlds: ["net"] }; + mockReadYamlConfig.mockImplementationOnce(() => { + return data; + }); + + const output = readNamesFromInputs(undefined, "path/to/yaml/config.yml"); + expect(output).toEqual(data.names); +}); + +it("runChecks handles multiple names/tlds with concurrency >1", async () => { + const whoisFn = (whois as any).default ?? whois; + whoisFn.mockResolvedValue({}); + const res = await runChecks({ + names: ["name1", "name2"], + tlds: ["com", "dev"], + concurrency: 2, + timeoutMs: 100, + }); + // Expect 4 results (2 names x 2 tlds) + expect(res.length).toBe(4); +}); + +it("runChecks throws when no names provided", async () => { + await expect(runChecks({ names: [], tlds: ["com"] })).rejects.toThrow( + "No candidate names provided", + ); +}); + +it("runChecks throws when no tlds provided", async () => { + await expect(runChecks({ names: ["test"], tlds: [] })).rejects.toThrow( + "No TLDs supplied", + ); +}); + +it("CLI without -t uses default tlds and writes output", async () => { + const program = new Command().name("check-domains"); + program.addCommand(makeCheckDomainsCommand()); + const whoisFn = (whois as any).default ?? whois; + whoisFn.mockResolvedValue({}); + (fsMock as any).writeFileSync.mockClear(); + await program.parseAsync([ + "node", + "x", + "check-domains", + "-n", + "cli-name", + "--format", + "json", + "-o", + "out-default", + "--no-header", + ]); + expect((fsMock as any).writeFileSync).toHaveBeenCalled(); +}); + +it("CLI defaults to yaml for unsupported format", async () => { + const program = new Command().name("check-domains"); + program.addCommand(makeCheckDomainsCommand()); + + const whoisFn = (whois as any).default ?? whois; + whoisFn.mockResolvedValue({}); + + await expect( + program.parseAsync([ + "node", + "x", + "check-domains", + "-n", + "cli-name", + "--format", + "invalid", + "-o", + "out-invalid", + "--no-header", + ]), + ).resolves.toBeDefined(); +}); diff --git a/js/cli.ts b/js/cli.ts index b82581c..c8108cf 100755 --- a/js/cli.ts +++ b/js/cli.ts @@ -2,6 +2,7 @@ import { execSync } from "child_process"; import { Command } from "commander"; import { auditAutoApprove } from "./commands/copilot/audit-auto-approve.ts"; +import { makeCheckDomainsCommand } from "./commands/domain-availability-checker/domain-availability-checker.ts"; const packageJson = await import("./package.json", { with: { type: "json" }, @@ -21,6 +22,8 @@ const copilot = program .command("copilot") .description("GitHub Copilot helpers"); +program.addCommand(makeCheckDomainsCommand()); + // Subcommand: copilot audit-auto-approve copilot .command("audit-auto-approve") diff --git a/js/commands/domain-availability-checker/domain-availability-checker.ts b/js/commands/domain-availability-checker/domain-availability-checker.ts new file mode 100644 index 0000000..8ff712c --- /dev/null +++ b/js/commands/domain-availability-checker/domain-availability-checker.ts @@ -0,0 +1,458 @@ +#!/usr/bin/env -S tsx +/** + * Domain availability checker using WHOIS/RDAP (whois-json). + * - Exported Commander Command for composition into a parent CLI (osa). + * - Also runnable directly (detects direct-run vs import). + */ + +import * as fs from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { Command } from "commander"; +import { stringify as csvStringify } from "csv-stringify/sync"; +import yaml from "js-yaml"; +import pLimit from "p-limit"; +import whois from "whois-json"; +import { readYamlConfig } from "./readYamlConfig"; + +// ---------- Types ---------- +export interface CheckResult { + domain: string; + name: string; + tld: string; + status: "registered" | "likely-available" | "unknown" | "error"; + elapsedMs: number; + parsedSummary: string; + rawSnippet: string; +} + +export interface RunChecksOptions { + names?: string[]; // candidate names without TLD + filePath?: string; // optional file with one name per line, or YAML with names/tlds + tlds: string[]; // TLDs without leading dot + concurrency?: number; // default 8 + timeoutMs?: number; // default 20000 + provider?: string; // WHOIS provider/server to use +} + +export interface WriteCsvOptions { + outFile: string; + results: CheckResult[]; + writeHeader?: boolean; // default true +} + +export function readNamesFromInputs( + namesList?: string[], + filePath?: string, +): string[] | null { + const names = new Set(); + + if (Array.isArray(namesList)) { + for (const n of namesList) { + const v = n.trim().toLowerCase(); + if (v) names.add(v); + } + } + + if (filePath) { + // Try YAML first + const yamlConfig = readYamlConfig(filePath); + if (yamlConfig?.names) { + for (const n of yamlConfig.names) { + const v = String(n).trim().toLowerCase(); + if (v) names.add(v); + } + } else { + // Fall back to plain text (one name per line). Prefer reading the + // provided path first (so tests can mock readFileSync), and only if + // that fails attempt the workspace-root-relative fallback. + let text: string; + try { + text = fs.readFileSync(filePath, "utf8"); + } catch (err) { + if (filePath.startsWith("./") || filePath.startsWith("../")) { + const currentDir = dirname(fileURLToPath(import.meta.url)); + const workspaceRoot = resolve(currentDir, "..", "..", ".."); + const resolvedPath = resolve(workspaceRoot, filePath); + text = fs.readFileSync(resolvedPath, "utf8"); + } else { + return null; + } + } + for (const line of text.split(/\r?\n/)) { + const t = line.trim().toLowerCase(); + if (t) names.add(t); + } + } + } + + return Array.from(names); +} + +export function readTldsFromInputs( + tldsList: string[], + filePath?: string, +): string[] { + const tlds = new Set(); + + // Add provided tlds + for (const t of tldsList) { + const v = t.replace(/^\./, "").trim().toLowerCase(); + if (v) tlds.add(v); + } + + // Add tlds from YAML file if present + if (filePath) { + const yamlConfig = readYamlConfig(filePath); + if (yamlConfig?.tlds) { + for (const t of yamlConfig.tlds) { + const v = String(t).replace(/^\./, "").trim().toLowerCase(); + if (v) tlds.add(v); + } + } + } + + return Array.from(tlds); +} + +export function inferAvailability( + whoisResult: any, + rawText?: string, +): CheckResult["status"] { + if (!whoisResult || Object.keys(whoisResult).length === 0) + return "likely-available"; + const keys = Object.keys(whoisResult).map((k) => k.toLowerCase()); + + if ( + keys.some( + (k) => + k.includes("domain name") || k.includes("domainname") || k === "domain", + ) + ) { + if ( + whoisResult.domainName || + whoisResult["Domain Name"] || + whoisResult["domain name"] + ) { + return "registered"; + } + } + + if (rawText) { + const lower = rawText.toLowerCase(); + const notFoundPhrases = [ + "no match", + "not found", + "no entries found", + "status: free", + "no data found", + "available", + "domain not found", + "no such domain", + "not registered", + "no object found", + ]; + for (const p of notFoundPhrases) { + if (lower.includes(p)) return "likely-available"; + } + } + + const registrarIndicators = [ + "registrar", + "name server", + "creation date", + "updated date", + "registry domain id", + "sponsoring registrar", + ]; + if (registrarIndicators.some((i) => keys.some((k) => k.includes(i)))) + return "registered"; + + return "unknown"; +} + +async function lookupWhois( + domain: string, + timeoutMs = 20_000, + provider?: string, +): Promise<{ parsed: any; raw: string }> { + const options: any = {}; + if (provider) { + options.server = provider; + } + + const p = (whois as any)(domain, options) + .then((res: any) => ({ parsed: res, raw: "" })) + .catch((err: any) => ({ parsed: null, raw: String(err) })); + const timeout = new Promise<{ parsed: any; raw: string }>((res) => + setTimeout(() => res({ parsed: null, raw: "__TIMEOUT__" }), timeoutMs), + ); + return Promise.race([p, timeout]) as Promise<{ parsed: any; raw: string }>; +} + +function truncate(s: string, n: number): string { + if (!s) return ""; + return s.length > n ? `${s.slice(0, n - 3)}...` : s; +} + +export function summarizeParsed(parsed: any): string { + if (!parsed) return ""; + const pick = [ + "Domain Name", + "Registrar", + "registrar", + "Creation Date", + "creationDate", + "Updated Date", + "updatedDate", + "status", + ]; + const out: string[] = []; + for (const k of pick) { + if (parsed[k]) out.push(`${k}:${truncate(String(parsed[k]), 80)}`); + } + if (parsed.domainName && !out.some((x) => x.startsWith("domainName:"))) { + out.push(`domainName:${truncate(String(parsed.domainName), 80)}`); + } + return out.join(" | "); +} + +// ---------- Core engine ---------- +export async function runChecks( + opts: RunChecksOptions, +): Promise { + const names = readNamesFromInputs(opts.names, opts.filePath); + if (names.length === 0) { + throw new Error("No candidate names provided. Use names[] or filePath."); + } + const tlds = readTldsFromInputs(opts.tlds, opts.filePath); + if (tlds.length === 0) throw new Error("No TLDs supplied."); + + const concurrency = Math.max(1, Math.min(200, Number(opts.concurrency ?? 8))); + const timeoutMs = Number(opts.timeoutMs ?? 20_000); + const limit = pLimit(concurrency); + + const jobs: Array> = []; + for (const name of names) { + for (const tld of tlds) { + const fqdn = `${name}.${tld}`; + jobs.push( + limit(async () => { + const started = Date.now(); + let status: CheckResult["status"] = "error"; + let parsed: any = null; + let raw = ""; + try { + const { parsed: p, raw: r } = await lookupWhois( + fqdn, + timeoutMs, + opts.provider, + ); + parsed = p; + raw = r ?? ""; + // If parsed object is empty and we didn't receive any raw text, be conservative + if (parsed && Object.keys(parsed).length === 0 && !raw) { + status = "unknown"; + } else { + status = inferAvailability(parsed, raw); + } + } catch (e: any) { + raw = String(e); + status = "likely-available"; + } + const elapsedMs = Date.now() - started; + return { + domain: fqdn, + name, + tld, + status, + elapsedMs, + parsedSummary: parsed ? summarizeParsed(parsed) : "", + rawSnippet: raw ? truncate(raw, 1200) : "", + }; + }), + ); + } + } + + return Promise.all(jobs); +} + +export function writeResultsCsv({ + outFile, + results, + writeHeader = true, +}: WriteCsvOptions): void { + const columns = [ + "domain", + "status", + "elapsedMs", + "parsedSummary", + "rawSnippet", + ]; + const records = results.map((r) => [ + r.domain, + r.status, + r.elapsedMs, + r.parsedSummary, + r.rawSnippet, + ]); + const csv = csvStringify(records, { header: writeHeader, columns }); + fs.writeFileSync(outFile, csv, "utf8"); +} + +// ---------- Exported Commander Command (for parent CLI composition) ---------- +export function makeCheckDomainsCommand(): Command { + const cmd = new Command("check-domains") + .description( + "Check domain availability via WHOIS/RDAP (heuristic). Use registrar APIs for purchase-grade checks.", + ) + .option( + "-n, --names ", + "Comma-separated names (no TLD). Example: virtualize,vize", + "", + ) + .option( + "-f, --file ", + "Path to file with one candidate name per line", + ) + .option( + "-t, --tlds ", + "Comma-separated TLDs (no dot). Default: com,dev,io", + "com,dev,io", + ) + .option( + "-p, --provider ", + "WHOIS server to use (e.g., whois.verisign-grs.com)", + ) + .option("--format ", "Output format: csv, json, or yaml", "csv") + .option("--timeout-ms ", "Lookup timeout (ms, default 20000)", "20000") + .option( + "-o, --out ", + "Output file name (extension added automatically)", + "domain-check-results", + ) + .option("--no-header", "Do not write CSV header (for appending)") + .option( + "--max-rows-print ", + "How many available-ish domains to log", + "50", + ) + .action( + async (opts: { + names?: string; + file?: string; + tlds: string; + concurrency?: string | number; + timeoutMs?: string | number; + out: string; + header: boolean; // from --no-header + maxRowsPrint?: string | number; + provider?: string; + format?: string; + }) => { + const namesArg = opts.names + ? String(opts.names) + .split(",") + .map((s) => s.trim()) + .filter(Boolean) + : undefined; + + const tldsArg = String(opts.tlds) + .split(",") + .map((s) => s.trim()) + .filter(Boolean); + + const results = await runChecks({ + names: namesArg, + filePath: opts.file, + tlds: tldsArg, + concurrency: Number(opts.concurrency ?? 8), + timeoutMs: Number(opts.timeoutMs ?? 20_000), + provider: opts.provider, + }); + + const format = (opts.format || "csv").toLowerCase(); + const outFile = + opts.out + + (format === "csv" ? ".csv" : format === "json" ? ".json" : ".yaml"); + + if (format === "json") { + const output = { + summary: results.reduce>((acc, r) => { + acc[r.status] = (acc[r.status] ?? 0) + 1; + return acc; + }, {}), + results, + generatedAt: new Date().toISOString(), + total: results.length, + }; + fs.writeFileSync(outFile, JSON.stringify(output, null, 2), "utf8"); + console.log(`Wrote ${results.length} results to ${outFile}`); + } else if (format === "yaml") { + const output = { + summary: results.reduce>((acc, r) => { + acc[r.status] = (acc[r.status] ?? 0) + 1; + return acc; + }, {}), + results, + generatedAt: new Date().toISOString(), + total: results.length, + }; + fs.writeFileSync(outFile, yaml.dump(output), "utf8"); + console.log(`Wrote ${results.length} results to ${outFile}`); + } else { + writeResultsCsv({ + outFile, + results, + writeHeader: opts.header, + }); + console.log(`Wrote ${results.length} rows to ${outFile}`); + } + + const summary = results.reduce>((acc, r) => { + acc[r.status] = (acc[r.status] ?? 0) + 1; + return acc; + }, {}); + console.log("Summary:", summary); + + const max = Math.max(1, Number(opts.maxRowsPrint ?? 50)); + console.log(`Top ${max} likely-available/unknown:`); + results + .filter( + (r) => r.status === "likely-available" || r.status === "unknown", + ) + .slice(0, max) + .forEach((r) => console.log(` - ${r.domain} => ${r.status}`)); + }, + ); + + return cmd; +} + +// Named + default export (handy for consumers) +const exported = { + makeCheckDomainsCommand, + runChecks, + writeResultsCsv, + inferAvailability, + summarizeParsed, +}; +export default exported; + +// ---------- Direct-run detection (standalone mode) ---------- +const isDirectRun = + typeof process !== "undefined" && + process.argv[1] && + import.meta.url === pathToFileURL(process.argv[1]).toString(); + +/* istanbul ignore if -- @preserve */ +if (isDirectRun) { + // Build a local program and attach our command. + const program = new Command().name("check-domains"); + program.addCommand(makeCheckDomainsCommand()); + program.parseAsync(process.argv).catch((err: unknown) => { + console.error("Fatal error:", err); + process.exit(1); + }); +} diff --git a/js/commands/domain-availability-checker/readYamlConfig.ts b/js/commands/domain-availability-checker/readYamlConfig.ts new file mode 100644 index 0000000..4436176 --- /dev/null +++ b/js/commands/domain-availability-checker/readYamlConfig.ts @@ -0,0 +1,37 @@ +import * as fs from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import yaml from "js-yaml"; + +// ---------- Utils ---------- +interface YamlConfig { + names?: string[]; + tlds?: string[]; +} + +export function readYamlConfig(filePath: string): YamlConfig | null { + try { + // Prefer reading the path as-provided first. This ensures tests that spy on + // fs.readFileSync(filePath) will be exercised. If that fails, try a + // workspace-root-relative fallback for CLI invocations where paths are + // relative to the repo root. + let text: string; + try { + text = fs.readFileSync(filePath, "utf8"); + } catch (err) { + // Only attempt workspace-root fallback for explicitly relative paths + if (filePath.startsWith("./") || filePath.startsWith("../")) { + const currentDir = dirname(fileURLToPath(import.meta.url)); + const workspaceRoot = resolve(currentDir, "..", "..", ".."); + const resolvedPath = resolve(workspaceRoot, filePath); + text = fs.readFileSync(resolvedPath, "utf8"); + } else { + throw err; + } + } + const config = yaml.load(text) as YamlConfig; + return config || null; + } catch { + return null; + } +} diff --git a/js/domains-to-test-example.yaml b/js/domains-to-test-example.yaml new file mode 100644 index 0000000..de6166f --- /dev/null +++ b/js/domains-to-test-example.yaml @@ -0,0 +1,22 @@ +# osa/scripts/domain-input.yaml + +names: + - swift-brown-fox + - clever-red-hedgehog + - bright-yellow-canary + - silent-white-owl + - agile-panda + - mightyblue + - frog + +domainProviders: + - whois # your existing WHOIS/RDAP checker + - domainr # if you add API support later + - cloudflare # ideal if you want authoritative registrar results + +tlds: + - dev + - com + - net + - ai + - io diff --git a/js/package.json b/js/package.json index e39d89c..142a58b 100644 --- a/js/package.json +++ b/js/package.json @@ -18,12 +18,19 @@ "LICENSE" ], "dependencies": { - "commander": "^12.1.0", - "tsx": "^4.19.0" + "commander": "^14.0.2", + "csv-stringify": "^6.6.0", + "js-yaml": "^4.1.0", + "p-limit": "^7.2.0", + "tsx": "^4.20.0", + "whois-json": "2.0.4" }, "devDependencies": { "@types/commander": "^2.12.5", - "@types/node": "^22.0.0", + "@types/csv-stringify": "^3.1.3", + "@types/js-yaml": "^4.0.9", + "@types/node": "^24.0.10", + "@types/whois-json": "^2", "@vitest/coverage-v8": "4.0.6", "typescript": "^5.6.3", "vitest": "^4.0.6" @@ -39,6 +46,5 @@ "publishConfig": { "access": "public" }, - "sideEffects": false, - "packageManager": "yarn@4.10.3" + "sideEffects": false } diff --git a/js/test-output.json b/js/test-output.json new file mode 100644 index 0000000..5401a8d --- /dev/null +++ b/js/test-output.json @@ -0,0 +1,191 @@ +{ + "summary": { + "registered": 12, + "likely-available": 6, + "unknown": 2 + }, + "results": [ + { + "domain": "virtualize.com", + "name": "virtualize", + "tld": "com", + "status": "registered", + "elapsedMs": 84, + "parsedSummary": "domainName:VIRTUALIZE.COM | registrar:DYNADOT LLC | creationDate:2002-08-25T18:05:14.0Z | updatedDate:2025-08-03T22:14:12.0Z", + "rawSnippet": "" + }, + { + "domain": "virtualize.dev", + "name": "virtualize", + "tld": "dev", + "status": "likely-available", + "elapsedMs": 3, + "parsedSummary": "", + "rawSnippet": "Error: getaddrinfo ENOTFOUND whois.nic.google" + }, + { + "domain": "virtualize.io", + "name": "virtualize", + "tld": "io", + "status": "registered", + "elapsedMs": 101, + "parsedSummary": "domainName:VIRTUALIZE.IO | registrar:DYNADOT LLC | creationDate:2013-07-09T16:04:31.0Z | updatedDate:2025-09-05T21:54:58.0Z", + "rawSnippet": "" + }, + { + "domain": "virtualize.net", + "name": "virtualize", + "tld": "net", + "status": "registered", + "elapsedMs": 2079, + "parsedSummary": "domainName:virtualize.net | registrar:NAMECHEAP INC | creationDate:1997-11-26T05:00:00.00Z | updatedDate:2024-02-03T05:06:33.00Z", + "rawSnippet": "" + }, + { + "domain": "virtualize.ai", + "name": "virtualize", + "tld": "ai", + "status": "registered", + "elapsedMs": 127, + "parsedSummary": "domainName:virtualize.ai | registrar:GoDaddy.com, LLC | creationDate:2020-06-14T23:47:33Z | updatedDate:2025-01-21T15:27:50Z", + "rawSnippet": "" + }, + { + "domain": "vize.com", + "name": "vize", + "tld": "com", + "status": "registered", + "elapsedMs": 381, + "parsedSummary": "domainName:vize.com | registrar:GRANSY S.R.O D/B/A SUBREG.CZ | creationDate:1999-01-23T00:00:00Z | updatedDate:2025-01-24T00:00:00Z", + "rawSnippet": "" + }, + { + "domain": "vize.dev", + "name": "vize", + "tld": "dev", + "status": "likely-available", + "elapsedMs": 17, + "parsedSummary": "", + "rawSnippet": "Error: getaddrinfo ENOTFOUND whois.nic.google" + }, + { + "domain": "vize.io", + "name": "vize", + "tld": "io", + "status": "likely-available", + "elapsedMs": 43, + "parsedSummary": "", + "rawSnippet": "Error: read ECONNRESET" + }, + { + "domain": "vize.net", + "name": "vize", + "tld": "net", + "status": "registered", + "elapsedMs": 1227, + "parsedSummary": "domainName:VIZE.NET | registrar:NICS Telekomunikasyon A.S. | creationDate:2006-03-19T19:07:10Z | updatedDate:2025-03-21T18:57:34Z", + "rawSnippet": "" + }, + { + "domain": "vize.ai", + "name": "vize", + "tld": "ai", + "status": "likely-available", + "elapsedMs": 26, + "parsedSummary": "", + "rawSnippet": "Error: read ECONNRESET" + }, + { + "domain": "vldev.com", + "name": "vldev", + "tld": "com", + "status": "registered", + "elapsedMs": 176, + "parsedSummary": "domainName:VLDEV.COM | registrar:TurnCommerce, Inc. DBA NameBright.com | creationDate:2018-08-29T18:30:24.000Z | updatedDate:2020-08-23T07:17:39.321Z", + "rawSnippet": "" + }, + { + "domain": "vldev.dev", + "name": "vldev", + "tld": "dev", + "status": "likely-available", + "elapsedMs": 2, + "parsedSummary": "", + "rawSnippet": "Error: getaddrinfo ENOTFOUND whois.nic.google" + }, + { + "domain": "vldev.io", + "name": "vldev", + "tld": "io", + "status": "unknown", + "elapsedMs": 50, + "parsedSummary": "", + "rawSnippet": "" + }, + { + "domain": "vldev.net", + "name": "vldev", + "tld": "net", + "status": "registered", + "elapsedMs": 440, + "parsedSummary": "domainName:VLDEV.NET | registrar:TUCOWS DOMAINS, INC. | creationDate:2017-09-26T07:21:17 | updatedDate:2025-06-02T00:04:12", + "rawSnippet": "" + }, + { + "domain": "vldev.ai", + "name": "vldev", + "tld": "ai", + "status": "unknown", + "elapsedMs": 49, + "parsedSummary": "", + "rawSnippet": "" + }, + { + "domain": "testdomain.com", + "name": "testdomain", + "tld": "com", + "status": "registered", + "elapsedMs": 95, + "parsedSummary": "domainName:testdomain.com | registrar:GoDaddy.com, LLC | creationDate:2002-05-28T13:22:16Z | updatedDate:2025-05-08T15:30:07Z", + "rawSnippet": "" + }, + { + "domain": "testdomain.dev", + "name": "testdomain", + "tld": "dev", + "status": "likely-available", + "elapsedMs": 1, + "parsedSummary": "", + "rawSnippet": "Error: getaddrinfo ENOTFOUND whois.nic.google" + }, + { + "domain": "testdomain.io", + "name": "testdomain", + "tld": "io", + "status": "registered", + "elapsedMs": 438, + "parsedSummary": "domainName:testdomain.io | registrar:GANDI SAS | creationDate:2015-11-25T03:26:32Z | updatedDate:2025-10-21T04:27:42Z", + "rawSnippet": "" + }, + { + "domain": "testdomain.net", + "name": "testdomain", + "tld": "net", + "status": "registered", + "elapsedMs": 445, + "parsedSummary": "domainName:testdomain.net | registrar:Key-Systems GmbH | creationDate:2018-08-09T18:08:03Z | updatedDate:2025-08-10T07:28:00Z", + "rawSnippet": "" + }, + { + "domain": "testdomain.ai", + "name": "testdomain", + "tld": "ai", + "status": "registered", + "elapsedMs": 154, + "parsedSummary": "domainName:testdomain.ai | registrar:One.com A/S | creationDate:2025-03-14T12:02:50Z | updatedDate:2025-03-19T12:03:27Z", + "rawSnippet": "" + } + ], + "generatedAt": "2025-11-09T07:31:13.914Z", + "total": 20 +} \ No newline at end of file diff --git a/js/test-output.yaml b/js/test-output.yaml new file mode 100644 index 0000000..d2a4acc --- /dev/null +++ b/js/test-output.yaml @@ -0,0 +1,174 @@ +summary: + registered: 13 + likely-available: 4 + unknown: 3 +results: + - domain: virtualize.com + name: virtualize + tld: com + status: registered + elapsedMs: 262 + parsedSummary: >- + domainName:VIRTUALIZE.COM | registrar:DYNADOT LLC | + creationDate:2002-08-25T18:05:14.0Z | updatedDate:2025-08-03T22:14:12.0Z + rawSnippet: '' + - domain: virtualize.dev + name: virtualize + tld: dev + status: likely-available + elapsedMs: 110 + parsedSummary: '' + rawSnippet: 'Error: getaddrinfo ENOTFOUND whois.nic.google' + - domain: virtualize.io + name: virtualize + tld: io + status: registered + elapsedMs: 359 + parsedSummary: >- + domainName:VIRTUALIZE.IO | registrar:DYNADOT LLC | + creationDate:2013-07-09T16:04:31.0Z | updatedDate:2025-09-05T21:54:58.0Z + rawSnippet: '' + - domain: virtualize.net + name: virtualize + tld: net + status: registered + elapsedMs: 4006 + parsedSummary: >- + domainName:virtualize.net | registrar:NAMECHEAP INC | + creationDate:1997-11-26T05:00:00.00Z | updatedDate:2024-02-03T05:06:33.00Z + rawSnippet: '' + - domain: virtualize.ai + name: virtualize + tld: ai + status: registered + elapsedMs: 430 + parsedSummary: >- + domainName:virtualize.ai | registrar:GoDaddy.com, LLC | + creationDate:2020-06-14T23:47:33Z | updatedDate:2025-01-21T15:27:50Z + rawSnippet: '' + - domain: vize.com + name: vize + tld: com + status: registered + elapsedMs: 654 + parsedSummary: >- + domainName:vize.com | registrar:GRANSY S.R.O D/B/A SUBREG.CZ | + creationDate:1999-01-23T00:00:00Z | updatedDate:2025-01-24T00:00:00Z + rawSnippet: '' + - domain: vize.dev + name: vize + tld: dev + status: likely-available + elapsedMs: 198 + parsedSummary: '' + rawSnippet: 'Error: getaddrinfo ENOTFOUND whois.nic.google' + - domain: vize.io + name: vize + tld: io + status: unknown + elapsedMs: 639 + parsedSummary: '' + rawSnippet: '' + - domain: vize.net + name: vize + tld: net + status: registered + elapsedMs: 1840 + parsedSummary: >- + domainName:VIZE.NET | registrar:NICS Telekomunikasyon A.S. | + creationDate:2006-03-19T19:07:10Z | updatedDate:2025-03-21T18:57:34Z + rawSnippet: '' + - domain: vize.ai + name: vize + tld: ai + status: registered + elapsedMs: 1646 + parsedSummary: >- + domainName:vize.ai | registrar:NAMECHEAP INC | + creationDate:2025-08-09T13:52:49.14Z | updatedDate:0001-01-01T00:00:00.00Z + rawSnippet: '' + - domain: vldev.com + name: vldev + tld: com + status: registered + elapsedMs: 237 + parsedSummary: >- + domainName:VLDEV.COM | registrar:TurnCommerce, Inc. DBA NameBright.com | + creationDate:2018-08-29T18:30:24.000Z | + updatedDate:2020-08-23T07:17:39.321Z + rawSnippet: '' + - domain: vldev.dev + name: vldev + tld: dev + status: likely-available + elapsedMs: 8 + parsedSummary: '' + rawSnippet: 'Error: getaddrinfo ENOTFOUND whois.nic.google' + - domain: vldev.io + name: vldev + tld: io + status: unknown + elapsedMs: 49 + parsedSummary: '' + rawSnippet: '' + - domain: vldev.net + name: vldev + tld: net + status: registered + elapsedMs: 570 + parsedSummary: >- + domainName:VLDEV.NET | registrar:TUCOWS DOMAINS, INC. | + creationDate:2017-09-26T07:21:17 | updatedDate:2025-06-02T00:04:12 + rawSnippet: '' + - domain: vldev.ai + name: vldev + tld: ai + status: unknown + elapsedMs: 49 + parsedSummary: '' + rawSnippet: '' + - domain: testdomain.com + name: testdomain + tld: com + status: registered + elapsedMs: 247 + parsedSummary: >- + domainName:testdomain.com | registrar:GoDaddy.com, LLC | + creationDate:2002-05-28T13:22:16Z | updatedDate:2025-05-08T15:30:07Z + rawSnippet: '' + - domain: testdomain.dev + name: testdomain + tld: dev + status: likely-available + elapsedMs: 136 + parsedSummary: '' + rawSnippet: 'Error: getaddrinfo ENOTFOUND whois.nic.google' + - domain: testdomain.io + name: testdomain + tld: io + status: registered + elapsedMs: 618 + parsedSummary: >- + domainName:testdomain.io | registrar:GANDI SAS | + creationDate:2015-11-25T03:26:32Z | updatedDate:2025-10-21T04:27:42Z + rawSnippet: '' + - domain: testdomain.net + name: testdomain + tld: net + status: registered + elapsedMs: 459 + parsedSummary: >- + domainName:testdomain.net | registrar:Key-Systems GmbH | + creationDate:2018-08-09T18:08:03Z | updatedDate:2025-08-10T07:28:00Z + rawSnippet: '' + - domain: testdomain.ai + name: testdomain + tld: ai + status: registered + elapsedMs: 192 + parsedSummary: >- + domainName:testdomain.ai | registrar:One.com A/S | + creationDate:2025-03-14T12:02:50Z | updatedDate:2025-03-19T12:03:27Z + rawSnippet: '' +generatedAt: '2025-11-09T07:30:46.041Z' +total: 20 diff --git a/js/tsconfig.json b/js/tsconfig.json index 4079406..dd8a388 100644 --- a/js/tsconfig.json +++ b/js/tsconfig.json @@ -1,17 +1,7 @@ { + "extends": "../tsconfig.base.json", "compilerOptions": { - "target": "ES2022", - "module": "ESNext", - "moduleResolution": "node", - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "strict": true, - "noImplicitAny": true, - "skipLibCheck": true, - "noEmit": true, - "allowImportingTsExtensions": true, - "resolveJsonModule": true, "types": ["node"] }, - "include": ["**/*.ts"] + "include": ["./**/*.ts"] } diff --git a/mise.toml b/mise.toml index da41fe4..f2c15e1 100644 --- a/mise.toml +++ b/mise.toml @@ -1,2 +1,2 @@ [tools] -node = "22" \ No newline at end of file +node = "24" \ No newline at end of file diff --git a/package.json b/package.json index 494e0e1..17c5dae 100644 --- a/package.json +++ b/package.json @@ -10,16 +10,30 @@ "cli": "yarn workspace @virtualize/osa-snippets run cli", "lint": "biome check ./js", "test": "vitest ./js --run", + "test-all": "vitest ./js --run", "test:coverage": "vitest ./js --run --coverage", "lint:fix": "biome check --fix ./js", "build": "echo 'Nothing to build at root'", - "test:e2e": "cd js && node ../tests/run-tests.zsh" + "test:e2e": "yarn test && ./tests/run-tests.zsh" }, "devDependencies": { "@biomejs/biome": "^2.3.3", "@types/commander": "^2.12.5", - "@types/node": "^22.0.0", + "@types/csv-stringify": "^3.1.3", + "@types/js-yaml": "^4.0.9", + "@types/node": "^24.0.10", + "@types/whois-json": "^2", "@vitest/coverage-v8": "4.0.6", + "corepack": "^0.34.6", + "tsx": "^4.20.6", + "typescript": "^5.9.3", "vitest": "^4.0.6" + }, + "dependencies": { + "commander": "^14.0.2", + "csv-stringify": "^6.6.0", + "js-yaml": "^4.1.0", + "p-limit": "^7.2.0", + "whois-json": "2.0.4" } } diff --git a/scripts/coverage-analyze.js b/scripts/coverage-analyze.js new file mode 100644 index 0000000..b5b2bc6 --- /dev/null +++ b/scripts/coverage-analyze.js @@ -0,0 +1,120 @@ +#!/usr/bin/env node +import fs from "fs"; +import path from "path"; + +const [, , pattern, outFile] = process.argv; + +const covPath = path.resolve(process.cwd(), "coverage/coverage-final.json"); +if (!fs.existsSync(covPath)) { + console.error( + "coverage/coverage-final.json not found. Run tests with coverage first.", + ); + process.exit(2); +} + +const cov = JSON.parse(fs.readFileSync(covPath, "utf8")); +const report = []; + +for (const [filePath, entry] of Object.entries(cov)) { + if (pattern && !filePath.includes(pattern)) continue; + + const stmtUncovered = []; + for (const [id, loc] of Object.entries(entry.statementMap || {})) { + const count = entry.s && entry.s[id]; + if (!count) { + if (loc && loc.start && loc.start.line) + stmtUncovered.push(loc.start.line); + else stmtUncovered.push(id); + } + } + + const fnUncovered = []; + for (const [id, meta] of Object.entries(entry.fnMap || {})) { + const count = entry.f && entry.f[id]; + if (!count) { + const name = meta && meta.name ? meta.name : id; + const line = + meta && meta.loc && meta.loc.start && meta.loc.start.line + ? meta.loc.start.line + : null; + fnUncovered.push({ id, name, line }); + } + } + + const branchUncovered = []; + for (const [id, loc] of Object.entries(entry.branchMap || {})) { + const counts = entry.b && entry.b[id]; + if (Array.isArray(counts)) { + counts.forEach((c, idx) => { + if (!c) { + const startLine = + (loc && + loc.locations && + loc.locations[idx] && + loc.locations[idx].start && + loc.locations[idx].start.line) || + (loc && loc.start && loc.start.line) || + null; + branchUncovered.push({ id, part: idx, line: startLine }); + } + }); + } + } + + const totalStatements = Object.keys(entry.statementMap || {}).length; + const totalBranches = Object.keys(entry.branchMap || {}).length; + const totalFuncs = Object.keys(entry.fnMap || {}).length; + + report.push({ + file: filePath, + statements: { total: totalStatements, uncovered: stmtUncovered.length }, + branches: { total: totalBranches, uncovered: branchUncovered.length }, + functions: { total: totalFuncs, uncovered: fnUncovered.length }, + uncoveredStatements: stmtUncovered.slice(0, 200), + uncoveredFunctions: fnUncovered.slice(0, 200), + uncoveredBranches: branchUncovered.slice(0, 200), + }); +} + +if (report.length === 0) { + console.log("No files matched the given pattern."); + process.exit(0); +} + +for (const r of report) { + console.log("File:", r.file); + console.log( + ` Statements: ${r.statements.total} total, ${r.statements.uncovered} uncovered`, + ); + if (r.statements.uncovered) + console.log( + " Uncovered statement start lines:", + r.uncoveredStatements.join(", "), + ); + console.log( + ` Branches: ${r.branches.total} total, ${r.branches.uncovered} uncovered`, + ); + if (r.branches.uncovered) + console.log( + " Uncovered branches (id:part@line):", + r.uncoveredBranches.map((b) => `${b.id}:${b.part}@${b.line}`).join(", "), + ); + console.log( + ` Functions: ${r.functions.total} total, ${r.functions.uncovered} uncovered`, + ); + if (r.functions.uncovered) + console.log( + " Uncovered functions:", + r.uncoveredFunctions.map((f) => `${f.name}@${f.line}`).join(", "), + ); + console.log(""); +} + +if (outFile) { + fs.writeFileSync( + path.resolve(process.cwd(), outFile), + JSON.stringify(report, null, 2), + "utf8", + ); + console.log("Wrote JSON report to", outFile); +} diff --git a/tsconfig.base.json b/tsconfig.base.json index f3bed7c..46c7a86 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1,10 +1,15 @@ { "compilerOptions": { - "target": "ES2022", - "module": "ESNext", - "moduleResolution": "NodeNext", + "target": "es2024", + "module": "preserve", + "moduleResolution": "bundler", "strict": true, + "composite": true, + "allowSyntheticDefaultImports": true, + "noImplicitAny": true, + "resolveJsonModule": true, "esModuleInterop": true, - "skipLibCheck": true + "skipLibCheck": true, + "types": ["node"] } } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ade1b89 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "noEmit": true, + "composite": true, + "typeRoots": ["./node_modules/@types", "js/node_modules/@types"] + }, + "include": ["./**/*.ts"], + "references": [{ "path": "./js/tsconfig.json" }] +} diff --git a/vitest.config.ts b/vitest.config.ts index cd7e846..e1a5dc3 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -3,16 +3,34 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { environment: "node", + watch: false, coverage: { - include: ["js/**/*"], - exclude: ["node_modules","./**/*.json", "./js/cli.ts"], + reportsDirectory: "./coverage", // New directory name/path + // Limit coverage collection to well-tested command modules to avoid + // pulling in many static assets and example data files. + include: ["js/**/*.{ts,tsx,js,jsx}"], + exclude: [ + "node_modules", + "**/coverage/**", + "js/coverage/**", + "**/*.html", + "**/*.xml", + "**/*.csv", + "**/*.yaml", + "**/*.yml", + "**/*.json", + "**/*.png", + "**/*.css", + "**/*.plist", + "./js/cli.ts", + ], // Global thresholds thresholds: { lines: 95, functions: 95, - branches: 95, + branches: 90, statements: 95, }, }, }, -}); \ No newline at end of file +}); diff --git a/yarn.lock b/yarn.lock index c7757e7..7da3672 100644 --- a/yarn.lock +++ b/yarn.lock @@ -576,6 +576,15 @@ __metadata: languageName: node linkType: hard +"@types/csv-stringify@npm:^3.1.3": + version: 3.1.3 + resolution: "@types/csv-stringify@npm:3.1.3" + dependencies: + csv-stringify: "npm:*" + checksum: 10c0/fb7a37f9e410c2268df31d3b5720104e68461fe47641742641c81a4f19a3a74eeb9b33687e84a2066e725bfa5f8578362ec5c63ce8c21a430133111da66f540a + languageName: node + linkType: hard + "@types/deep-eql@npm:*": version: 4.0.2 resolution: "@types/deep-eql@npm:4.0.2" @@ -590,12 +599,26 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^22.0.0": - version: 22.19.0 - resolution: "@types/node@npm:22.19.0" +"@types/js-yaml@npm:^4.0.9": + version: 4.0.9 + resolution: "@types/js-yaml@npm:4.0.9" + checksum: 10c0/24de857aa8d61526bbfbbaa383aa538283ad17363fcd5bb5148e2c7f604547db36646440e739d78241ed008702a8920665d1add5618687b6743858fae00da211 + languageName: node + linkType: hard + +"@types/node@npm:^24.0.10": + version: 24.10.9 + resolution: "@types/node@npm:24.10.9" dependencies: - undici-types: "npm:~6.21.0" - checksum: 10c0/66b98fcd38efb4ae58c628d61fed2b8f17c830eb665d8bceabc4174cae1dd81b8e4caac4c70d7dc929f9f76a3dc46cb21f480e904d0b898297bd12c0a2d1571a + undici-types: "npm:~7.16.0" + checksum: 10c0/e9e436fcd2136bddb1bbe3271a89f4653910bcf6ee8047c4117f544c7905a106c039e2720ee48f28505ef2560e22fb9ead719f28bf5e075fdde0c1120e38e3b2 + languageName: node + linkType: hard + +"@types/whois-json@npm:^2": + version: 2.0.4 + resolution: "@types/whois-json@npm:2.0.4" + checksum: 10c0/845d2ffe6253e59caa946e574b812b4ce40ae20fbe3e0f6691b70f006f4884761ab2ea8c3c38f8e34eb06b8d4cb24279b512d29880f295fed5c64cca5aaa3993 languageName: node linkType: hard @@ -604,12 +627,19 @@ __metadata: resolution: "@virtualize/osa-snippets@workspace:js" dependencies: "@types/commander": "npm:^2.12.5" - "@types/node": "npm:^22.0.0" + "@types/csv-stringify": "npm:^3.1.3" + "@types/js-yaml": "npm:^4.0.9" + "@types/node": "npm:^24.0.10" + "@types/whois-json": "npm:^2" "@vitest/coverage-v8": "npm:4.0.6" - commander: "npm:^12.1.0" - tsx: "npm:^4.19.0" + commander: "npm:^14.0.2" + csv-stringify: "npm:^6.6.0" + js-yaml: "npm:^4.1.0" + p-limit: "npm:^7.2.0" + tsx: "npm:^4.20.0" typescript: "npm:^5.6.3" vitest: "npm:^4.0.6" + whois-json: "npm:2.0.4" bin: osa: ./cli.ts languageName: unknown @@ -764,6 +794,13 @@ __metadata: languageName: node linkType: hard +"argparse@npm:^2.0.1": + version: 2.0.1 + resolution: "argparse@npm:2.0.1" + checksum: 10c0/c5640c2d89045371c7cedd6a70212a04e360fd34d6edeae32f6952c63949e3525ea77dbec0289d8213a99bbaeab5abfa860b5c12cf88a2e6cf8106e90dd27a7e + languageName: node + linkType: hard + "assertion-error@npm:^2.0.1": version: 2.0.1 resolution: "assertion-error@npm:2.0.1" @@ -818,6 +855,23 @@ __metadata: languageName: node linkType: hard +"camel-case@npm:^3.0.0": + version: 3.0.0 + resolution: "camel-case@npm:3.0.0" + dependencies: + no-case: "npm:^2.2.0" + upper-case: "npm:^1.1.1" + checksum: 10c0/491c6bbf986b9d8355e12cca6beb719b44c2fe96e8526c09958a1b4e0dbb081a82ea59c13b5a6ccf9158ce5979cbe56a8a10d7322bfeed2d84725c6b89d8f934 + languageName: node + linkType: hard + +"camelcase@npm:^5.0.0": + version: 5.3.1 + resolution: "camelcase@npm:5.3.1" + checksum: 10c0/92ff9b443bfe8abb15f2b1513ca182d16126359ad4f955ebc83dc4ddcc4ef3fdd2c078bc223f2673dc223488e75c99b16cc4d056624374b799e6a1555cf61b23 + languageName: node + linkType: hard + "chai@npm:^6.0.1": version: 6.2.0 resolution: "chai@npm:6.2.0" @@ -825,6 +879,32 @@ __metadata: languageName: node linkType: hard +"change-case@npm:^3.0.2": + version: 3.1.0 + resolution: "change-case@npm:3.1.0" + dependencies: + camel-case: "npm:^3.0.0" + constant-case: "npm:^2.0.0" + dot-case: "npm:^2.1.0" + header-case: "npm:^1.0.0" + is-lower-case: "npm:^1.1.0" + is-upper-case: "npm:^1.1.0" + lower-case: "npm:^1.1.1" + lower-case-first: "npm:^1.0.0" + no-case: "npm:^2.3.2" + param-case: "npm:^2.1.0" + pascal-case: "npm:^2.0.0" + path-case: "npm:^2.1.0" + sentence-case: "npm:^2.1.0" + snake-case: "npm:^2.1.0" + swap-case: "npm:^1.1.0" + title-case: "npm:^2.1.0" + upper-case: "npm:^1.1.1" + upper-case-first: "npm:^1.1.0" + checksum: 10c0/cb44722e596e0c69e8ba28dce664b36e537ec76c8296c0baaef11d2b3db1e4a797ed50a99ff9c98a008c69dbe0270cfb96e384417a264d33de4baa709b79b9bb + languageName: node + linkType: hard + "chownr@npm:^3.0.0": version: 3.0.0 resolution: "chownr@npm:3.0.0" @@ -832,6 +912,17 @@ __metadata: languageName: node linkType: hard +"cliui@npm:^6.0.0": + version: 6.0.0 + resolution: "cliui@npm:6.0.0" + dependencies: + string-width: "npm:^4.2.0" + strip-ansi: "npm:^6.0.0" + wrap-ansi: "npm:^6.2.0" + checksum: 10c0/35229b1bb48647e882104cac374c9a18e34bbf0bace0e2cf03000326b6ca3050d6b59545d91e17bfe3705f4a0e2988787aa5cde6331bf5cbbf0164732cef6492 + languageName: node + linkType: hard + "color-convert@npm:^2.0.1": version: 2.0.1 resolution: "color-convert@npm:2.0.1" @@ -848,17 +939,33 @@ __metadata: languageName: node linkType: hard -"commander@npm:*": +"commander@npm:*, commander@npm:^14.0.2": version: 14.0.2 resolution: "commander@npm:14.0.2" checksum: 10c0/245abd1349dbad5414cb6517b7b5c584895c02c4f7836ff5395f301192b8566f9796c82d7bd6c92d07eba8775fe4df86602fca5d86d8d10bcc2aded1e21c2aeb languageName: node linkType: hard -"commander@npm:^12.1.0": - version: 12.1.0 - resolution: "commander@npm:12.1.0" - checksum: 10c0/6e1996680c083b3b897bfc1cfe1c58dfbcd9842fd43e1aaf8a795fbc237f65efcc860a3ef457b318e73f29a4f4a28f6403c3d653d021d960e4632dd45bde54a9 +"constant-case@npm:^2.0.0": + version: 2.0.0 + resolution: "constant-case@npm:2.0.0" + dependencies: + snake-case: "npm:^2.1.0" + upper-case: "npm:^1.1.1" + checksum: 10c0/795142a64dd61da267e937502a1ce060abdbc42d4f68367d08f1de34fc06a1db240ac09658275122f8e171448b19a4645b023ee8229803def1a11559e80b6132 + languageName: node + linkType: hard + +"corepack@npm:^0.34.6": + version: 0.34.6 + resolution: "corepack@npm:0.34.6" + bin: + corepack: ./dist/corepack.js + pnpm: ./dist/pnpm.js + pnpx: ./dist/pnpx.js + yarn: ./dist/yarn.js + yarnpkg: ./dist/yarnpkg.js + checksum: 10c0/63762d2fed870081eb6043c1f4258532baacf0c681cfadbd594fb9c2c2b329f47dccadd9bcc36067c1d6277b2addf628e2c6bf2441078e299f4f0cfb4955b6f6 languageName: node linkType: hard @@ -873,6 +980,13 @@ __metadata: languageName: node linkType: hard +"csv-stringify@npm:*, csv-stringify@npm:^6.6.0": + version: 6.6.0 + resolution: "csv-stringify@npm:6.6.0" + checksum: 10c0/2e5b14ff1e434aba7b8cae74faa0329c1d967654820f2ae3f358a660b5887ab623224ed8eb7f3ab6d0f7342663c965ca079ca420b6046210709f72e8aec87d94 + languageName: node + linkType: hard + "debug@npm:4, debug@npm:^4.1.1, debug@npm:^4.3.4, debug@npm:^4.4.3": version: 4.4.3 resolution: "debug@npm:4.4.3" @@ -885,6 +999,29 @@ __metadata: languageName: node linkType: hard +"decamelize@npm:^1.2.0": + version: 1.2.0 + resolution: "decamelize@npm:1.2.0" + checksum: 10c0/85c39fe8fbf0482d4a1e224ef0119db5c1897f8503bcef8b826adff7a1b11414972f6fef2d7dec2ee0b4be3863cf64ac1439137ae9e6af23a3d8dcbe26a5b4b2 + languageName: node + linkType: hard + +"dedent-js@npm:^1.0.1": + version: 1.0.1 + resolution: "dedent-js@npm:1.0.1" + checksum: 10c0/a8cff2e02d5a1ce64615c5c53c9789e7ef1abb9ae7bf2322dc991fcbaf08d901ace1a679c1e021de15a85db7787b8ccfb02011e1f394afef0f698fc857a47009 + languageName: node + linkType: hard + +"dot-case@npm:^2.1.0": + version: 2.1.1 + resolution: "dot-case@npm:2.1.1" + dependencies: + no-case: "npm:^2.2.0" + checksum: 10c0/6859ba3bfe3106388c05eba9bec709856bbc9917d2c081aed5d268a2afc73b03bc062ea19925e29bdd482f6a8c032ae7a7d73f75c12d4159978e809b9418f7ef + languageName: node + linkType: hard + "eastasianwidth@npm:^0.2.0": version: 0.2.0 resolution: "eastasianwidth@npm:0.2.0" @@ -1060,6 +1197,16 @@ __metadata: languageName: node linkType: hard +"find-up@npm:^4.1.0": + version: 4.1.0 + resolution: "find-up@npm:4.1.0" + dependencies: + locate-path: "npm:^5.0.0" + path-exists: "npm:^4.0.0" + checksum: 10c0/0406ee89ebeefa2d507feb07ec366bebd8a6167ae74aa4e34fb4c4abd06cf782a3ce26ae4194d70706f72182841733f00551c209fe575cb00bd92104056e78c1 + languageName: node + linkType: hard + "foreground-child@npm:^3.1.0": version: 3.3.1 resolution: "foreground-child@npm:3.3.1" @@ -1098,6 +1245,13 @@ __metadata: languageName: node linkType: hard +"get-caller-file@npm:^2.0.1": + version: 2.0.5 + resolution: "get-caller-file@npm:2.0.5" + checksum: 10c0/c6c7b60271931fa752aeb92f2b47e355eac1af3a2673f47c9589e8f8a41adc74d45551c1bc57b5e66a80609f10ffb72b6f575e4370d61cc3f7f3aaff01757cde + languageName: node + linkType: hard + "get-tsconfig@npm:^4.7.5": version: 4.13.0 resolution: "get-tsconfig@npm:4.13.0" @@ -1137,6 +1291,23 @@ __metadata: languageName: node linkType: hard +"header-case@npm:^1.0.0": + version: 1.0.1 + resolution: "header-case@npm:1.0.1" + dependencies: + no-case: "npm:^2.2.0" + upper-case: "npm:^1.1.3" + checksum: 10c0/973b81b3fba82140cf8cdc819edb32edd5959ff61ff42128c5f54e56f7454bb8f61c0197180c38cde84a4be1dddbc780e1413d5e1602c96caf0195d863e6bd03 + languageName: node + linkType: hard + +"html-entities@npm:^1.2.1": + version: 1.4.0 + resolution: "html-entities@npm:1.4.0" + checksum: 10c0/eb2de616fb5948e681157805687672ea90e67c8a4f21a3215888ab422a984cab61fec96860708dca3bde0ae52577515683c8e28157ac8637220bb6a57a031b85 + languageName: node + linkType: hard + "html-escaper@npm:^2.0.0": version: 2.0.2 resolution: "html-escaper@npm:2.0.2" @@ -1201,6 +1372,24 @@ __metadata: languageName: node linkType: hard +"is-lower-case@npm:^1.1.0": + version: 1.1.3 + resolution: "is-lower-case@npm:1.1.3" + dependencies: + lower-case: "npm:^1.1.0" + checksum: 10c0/af174cfdd50e4ab997bd4aeaf96d5b1841490a721c62a9ab07b14dfb63885134065683d5027f53e2f76180ff972a3c9a0155815e715c37815757a6bd67d4459e + languageName: node + linkType: hard + +"is-upper-case@npm:^1.1.0": + version: 1.1.2 + resolution: "is-upper-case@npm:1.1.2" + dependencies: + upper-case: "npm:^1.1.0" + checksum: 10c0/81b8defdee0e0de7310446ac717422c586c4d013c2a517c5fcf8b119349aa2798be56fa213169b0de3936cb00e796a383683c2504d221596ae09a0eb282a5b25 + languageName: node + linkType: hard + "isexe@npm:^2.0.0": version: 2.0.0 resolution: "isexe@npm:2.0.0" @@ -1274,6 +1463,42 @@ __metadata: languageName: node linkType: hard +"js-yaml@npm:^4.1.0": + version: 4.1.0 + resolution: "js-yaml@npm:4.1.0" + dependencies: + argparse: "npm:^2.0.1" + bin: + js-yaml: bin/js-yaml.js + checksum: 10c0/184a24b4eaacfce40ad9074c64fd42ac83cf74d8c8cd137718d456ced75051229e5061b8633c3366b8aada17945a7a356b337828c19da92b51ae62126575018f + languageName: node + linkType: hard + +"locate-path@npm:^5.0.0": + version: 5.0.0 + resolution: "locate-path@npm:5.0.0" + dependencies: + p-locate: "npm:^4.1.0" + checksum: 10c0/33a1c5247e87e022f9713e6213a744557a3e9ec32c5d0b5efb10aa3a38177615bf90221a5592674857039c1a0fd2063b82f285702d37b792d973e9e72ace6c59 + languageName: node + linkType: hard + +"lower-case-first@npm:^1.0.0": + version: 1.0.2 + resolution: "lower-case-first@npm:1.0.2" + dependencies: + lower-case: "npm:^1.1.2" + checksum: 10c0/e0689a82df329db44e28b0dd53ccace09a8a4918fc86aa6c08b091ec31bc5f3496a0b07cf7e81be065335bea996f7aa0fbe0163a3e6f019b0480a5f20a79e871 + languageName: node + linkType: hard + +"lower-case@npm:^1.1.0, lower-case@npm:^1.1.1, lower-case@npm:^1.1.2": + version: 1.1.4 + resolution: "lower-case@npm:1.1.4" + checksum: 10c0/2153ae5490d655a63addc8e7d2f848c6c94803b342ed2d177f75e8073e9fbb50a733d1432c82e1cb8425fa6eae14b2877bf5bbdcb93ab93bb982fb5c3962c57b + languageName: node + linkType: hard + "lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0": version: 10.4.3 resolution: "lru-cache@npm:10.4.3" @@ -1437,6 +1662,15 @@ __metadata: languageName: node linkType: hard +"no-case@npm:^2.2.0, no-case@npm:^2.3.2": + version: 2.3.2 + resolution: "no-case@npm:2.3.2" + dependencies: + lower-case: "npm:^1.1.1" + checksum: 10c0/63f306e83c18efa0bb37f1c23a25baf4ccf5ebaec70b482fa04d4c5bf8bbb8bcc9a8fbcd818af828ab69f2b602153daf81ec26e448b2bda2d704b8d0c7eec8fa + languageName: node + linkType: hard + "node-gyp@npm:latest": version: 11.5.0 resolution: "node-gyp@npm:11.5.0" @@ -1474,12 +1708,50 @@ __metadata: dependencies: "@biomejs/biome": "npm:^2.3.3" "@types/commander": "npm:^2.12.5" - "@types/node": "npm:^22.0.0" + "@types/csv-stringify": "npm:^3.1.3" + "@types/js-yaml": "npm:^4.0.9" + "@types/node": "npm:^24.0.10" + "@types/whois-json": "npm:^2" "@vitest/coverage-v8": "npm:4.0.6" + commander: "npm:^14.0.2" + corepack: "npm:^0.34.6" + csv-stringify: "npm:^6.6.0" + js-yaml: "npm:^4.1.0" + p-limit: "npm:^7.2.0" + tsx: "npm:^4.20.6" + typescript: "npm:^5.9.3" vitest: "npm:^4.0.6" + whois-json: "npm:2.0.4" languageName: unknown linkType: soft +"p-limit@npm:^2.2.0": + version: 2.3.0 + resolution: "p-limit@npm:2.3.0" + dependencies: + p-try: "npm:^2.0.0" + checksum: 10c0/8da01ac53efe6a627080fafc127c873da40c18d87b3f5d5492d465bb85ec7207e153948df6b9cbaeb130be70152f874229b8242ee2be84c0794082510af97f12 + languageName: node + linkType: hard + +"p-limit@npm:^7.2.0": + version: 7.2.0 + resolution: "p-limit@npm:7.2.0" + dependencies: + yocto-queue: "npm:^1.2.1" + checksum: 10c0/18e5ea305c31fdc0cf5260da7b63adfd46748cd72d4b40a31a13bd23eeece729c9308047fb5d848d7fa006d1b2fc2f36bb0d9dd173a300c38cd6dc1ed3355382 + languageName: node + linkType: hard + +"p-locate@npm:^4.1.0": + version: 4.1.0 + resolution: "p-locate@npm:4.1.0" + dependencies: + p-limit: "npm:^2.2.0" + checksum: 10c0/1b476ad69ad7f6059744f343b26d51ce091508935c1dbb80c4e0a2f397ffce0ca3a1f9f5cd3c7ce19d7929a09719d5c65fe70d8ee289c3f267cd36f2881813e9 + languageName: node + linkType: hard + "p-map@npm:^7.0.2": version: 7.0.3 resolution: "p-map@npm:7.0.3" @@ -1487,6 +1759,13 @@ __metadata: languageName: node linkType: hard +"p-try@npm:^2.0.0": + version: 2.2.0 + resolution: "p-try@npm:2.2.0" + checksum: 10c0/c36c19907734c904b16994e6535b02c36c2224d433e01a2f1ab777237f4d86e6289fd5fd464850491e940379d4606ed850c03e0f9ab600b0ebddb511312e177f + languageName: node + linkType: hard + "package-json-from-dist@npm:^1.0.0": version: 1.0.1 resolution: "package-json-from-dist@npm:1.0.1" @@ -1494,6 +1773,41 @@ __metadata: languageName: node linkType: hard +"param-case@npm:^2.1.0": + version: 2.1.1 + resolution: "param-case@npm:2.1.1" + dependencies: + no-case: "npm:^2.2.0" + checksum: 10c0/8ea1b8472fd51d5f50b28d1d754899713805d05f2241e9b8c4acafa2c500b3f47457a3b4932ab75220f14d2c69180bb7338b78a45576e2b4d90da1e6f0285833 + languageName: node + linkType: hard + +"pascal-case@npm:^2.0.0": + version: 2.0.1 + resolution: "pascal-case@npm:2.0.1" + dependencies: + camel-case: "npm:^3.0.0" + upper-case-first: "npm:^1.1.0" + checksum: 10c0/84420c1ceeee36eebe7a6975926f50500563f2c664160b952ff78774af85696d06d52a0fbfeb28c063ee37da6c83665d2518a4fefc9c66996226cabb04a1319e + languageName: node + linkType: hard + +"path-case@npm:^2.1.0": + version: 2.1.1 + resolution: "path-case@npm:2.1.1" + dependencies: + no-case: "npm:^2.2.0" + checksum: 10c0/ea74c24b55cbc2a9d766415e79f53d48a4227cecd0259a00dc4392df6195f68055de164f90c27a3b2056c1977a28a99b2916b66bade0cbf6cf18a8045e76c922 + languageName: node + linkType: hard + +"path-exists@npm:^4.0.0": + version: 4.0.0 + resolution: "path-exists@npm:4.0.0" + checksum: 10c0/8c0bd3f5238188197dc78dced15207a4716c51cc4e3624c44fc97acf69558f5ebb9a2afff486fe1b4ee148e0c133e96c5e11a9aa5c48a3006e3467da070e5e1b + languageName: node + linkType: hard + "path-key@npm:^3.1.0": version: 3.1.1 resolution: "path-key@npm:3.1.1" @@ -1560,6 +1874,27 @@ __metadata: languageName: node linkType: hard +"punycode@npm:^2.3.1": + version: 2.3.1 + resolution: "punycode@npm:2.3.1" + checksum: 10c0/14f76a8206bc3464f794fb2e3d3cc665ae416c01893ad7a02b23766eb07159144ee612ad67af5e84fa4479ccfe67678c4feb126b0485651b302babf66f04f9e9 + languageName: node + linkType: hard + +"require-directory@npm:^2.1.1": + version: 2.1.1 + resolution: "require-directory@npm:2.1.1" + checksum: 10c0/83aa76a7bc1531f68d92c75a2ca2f54f1b01463cb566cf3fbc787d0de8be30c9dbc211d1d46be3497dac5785fe296f2dd11d531945ac29730643357978966e99 + languageName: node + linkType: hard + +"require-main-filename@npm:^2.0.0": + version: 2.0.0 + resolution: "require-main-filename@npm:2.0.0" + checksum: 10c0/db91467d9ead311b4111cbd73a4e67fa7820daed2989a32f7023785a2659008c6d119752d9c4ac011ae07e537eb86523adff99804c5fdb39cd3a017f9b401bb6 + languageName: node + linkType: hard + "resolve-pkg-maps@npm:^1.0.0": version: 1.0.0 resolution: "resolve-pkg-maps@npm:1.0.0" @@ -1671,6 +2006,23 @@ __metadata: languageName: node linkType: hard +"sentence-case@npm:^2.1.0": + version: 2.1.1 + resolution: "sentence-case@npm:2.1.1" + dependencies: + no-case: "npm:^2.2.0" + upper-case-first: "npm:^1.1.2" + checksum: 10c0/3572fe33dd5df4156bc2e5f46a8f7642906234c448484016d5fbb8c7214bdd1f5f01d1791a7b2b1a4f5a99e6e43141d22aa097c0fdcfe41214fd56a85f1ee7f6 + languageName: node + linkType: hard + +"set-blocking@npm:^2.0.0": + version: 2.0.0 + resolution: "set-blocking@npm:2.0.0" + checksum: 10c0/9f8c1b2d800800d0b589de1477c753492de5c1548d4ade52f57f1d1f5e04af5481554d75ce5e5c43d4004b80a3eb714398d6907027dc0534177b7539119f4454 + languageName: node + linkType: hard + "shebang-command@npm:^2.0.0": version: 2.0.0 resolution: "shebang-command@npm:2.0.0" @@ -1708,6 +2060,15 @@ __metadata: languageName: node linkType: hard +"snake-case@npm:^2.1.0": + version: 2.1.0 + resolution: "snake-case@npm:2.1.0" + dependencies: + no-case: "npm:^2.2.0" + checksum: 10c0/fd8b21537263d4e64cadd62da0cb5fd96c95f8685ee8e290c912b79950385dbb9eccf7216a913d96db5efc4ade426badae5e3e35b69ea1f10cbb0b4898a38236 + languageName: node + linkType: hard + "socks-proxy-agent@npm:^8.0.3": version: 8.0.5 resolution: "socks-proxy-agent@npm:8.0.5" @@ -1719,7 +2080,7 @@ __metadata: languageName: node linkType: hard -"socks@npm:^2.8.3": +"socks@npm:^2.2.2, socks@npm:^2.8.3": version: 2.8.7 resolution: "socks@npm:2.8.7" dependencies: @@ -1759,7 +2120,7 @@ __metadata: languageName: node linkType: hard -"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0": +"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0": version: 4.2.3 resolution: "string-width@npm:4.2.3" dependencies: @@ -1808,6 +2169,16 @@ __metadata: languageName: node linkType: hard +"swap-case@npm:^1.1.0": + version: 1.1.2 + resolution: "swap-case@npm:1.1.2" + dependencies: + lower-case: "npm:^1.1.1" + upper-case: "npm:^1.1.1" + checksum: 10c0/0fb57c2427cec53c85cab0dd85f243b8b84b68e039eb550c50e01340cfa43a62e5276e04d088e9066b89073f781b258eeb97bf2c22d6232e005feeca2a11d6bc + languageName: node + linkType: hard + "tar@npm:^7.4.3": version: 7.5.2 resolution: "tar@npm:7.5.2" @@ -1852,7 +2223,17 @@ __metadata: languageName: node linkType: hard -"tsx@npm:^4.19.0": +"title-case@npm:^2.1.0": + version: 2.1.1 + resolution: "title-case@npm:2.1.1" + dependencies: + no-case: "npm:^2.2.0" + upper-case: "npm:^1.0.3" + checksum: 10c0/7b4e51036af10d99c48fac9eee8c6f59f92c1c6cc56fc1f790c7727f5eb139c7ebf7a22381626855ce74b6534f36041e91922469dbe086e67d0d71e7e8e40fc3 + languageName: node + linkType: hard + +"tsx@npm:^4.20.0, tsx@npm:^4.20.6": version: 4.20.6 resolution: "tsx@npm:4.20.6" dependencies: @@ -1868,7 +2249,7 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^5.6.3": +"typescript@npm:^5.6.3, typescript@npm:^5.9.3": version: 5.9.3 resolution: "typescript@npm:5.9.3" bin: @@ -1878,7 +2259,7 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@npm%3A^5.6.3#optional!builtin": +"typescript@patch:typescript@npm%3A^5.6.3#optional!builtin, typescript@patch:typescript@npm%3A^5.9.3#optional!builtin": version: 5.9.3 resolution: "typescript@patch:typescript@npm%3A5.9.3#optional!builtin::version=5.9.3&hash=5786d5" bin: @@ -1888,10 +2269,17 @@ __metadata: languageName: node linkType: hard -"undici-types@npm:~6.21.0": - version: 6.21.0 - resolution: "undici-types@npm:6.21.0" - checksum: 10c0/c01ed51829b10aa72fc3ce64b747f8e74ae9b60eafa19a7b46ef624403508a54c526ffab06a14a26b3120d055e1104d7abe7c9017e83ced038ea5cf52f8d5e04 +"underscore@npm:^1.9.1": + version: 1.13.7 + resolution: "underscore@npm:1.13.7" + checksum: 10c0/fad2b4aac48847674aaf3c30558f383399d4fdafad6dd02dd60e4e1b8103b52c5a9e5937e0cc05dacfd26d6a0132ed0410ab4258241240757e4a4424507471cd + languageName: node + linkType: hard + +"undici-types@npm:~7.16.0": + version: 7.16.0 + resolution: "undici-types@npm:7.16.0" + checksum: 10c0/3033e2f2b5c9f1504bdc5934646cb54e37ecaca0f9249c983f7b1fc2e87c6d18399ebb05dc7fd5419e02b2e915f734d872a65da2e3eeed1813951c427d33cc9a languageName: node linkType: hard @@ -1913,6 +2301,22 @@ __metadata: languageName: node linkType: hard +"upper-case-first@npm:^1.1.0, upper-case-first@npm:^1.1.2": + version: 1.1.2 + resolution: "upper-case-first@npm:1.1.2" + dependencies: + upper-case: "npm:^1.1.1" + checksum: 10c0/db3aff30538ed53c35b91a68faf46119d35ceb8800fec373ad781cbfc487f10ccd4d60609e4188e85d20ea3ec9db9a2392fa6372c334f1d1107cbde0bcb5edfe + languageName: node + linkType: hard + +"upper-case@npm:^1.0.3, upper-case@npm:^1.1.0, upper-case@npm:^1.1.1, upper-case@npm:^1.1.3": + version: 1.1.3 + resolution: "upper-case@npm:1.1.3" + checksum: 10c0/3e4d3a90519915bb591db84d72610392518806d8287b8f7541d87642d30388f42b2def1ed2f687e5792ee025e8f7e17d3a0dcbd5b3b59e306ceb1f3b8121ef54 + languageName: node + linkType: hard + "vite@npm:^6.0.0 || ^7.0.0": version: 7.1.12 resolution: "vite@npm:7.1.12" @@ -2027,6 +2431,13 @@ __metadata: languageName: node linkType: hard +"which-module@npm:^2.0.0": + version: 2.0.1 + resolution: "which-module@npm:2.0.1" + checksum: 10c0/087038e7992649eaffa6c7a4f3158d5b53b14cf5b6c1f0e043dccfacb1ba179d12f17545d5b85ebd94a42ce280a6fe65d0cbcab70f4fc6daad1dfae85e0e6a3e + languageName: node + linkType: hard + "which@npm:^2.0.1": version: 2.0.2 resolution: "which@npm:2.0.2" @@ -2049,6 +2460,32 @@ __metadata: languageName: node linkType: hard +"whois-json@npm:2.0.4": + version: 2.0.4 + resolution: "whois-json@npm:2.0.4" + dependencies: + change-case: "npm:^3.0.2" + dedent-js: "npm:^1.0.1" + html-entities: "npm:^1.2.1" + whois: "npm:^2.6.0" + checksum: 10c0/266fdf8574265b3733d9b2fc71008b8f1bd813378bcfeaf140df8e98105dd69e72827a997b726e657cdbab77840cd04d17939ff2e9054617603dc9d1e0fda722 + languageName: node + linkType: hard + +"whois@npm:^2.6.0": + version: 2.15.0 + resolution: "whois@npm:2.15.0" + dependencies: + punycode: "npm:^2.3.1" + socks: "npm:^2.2.2" + underscore: "npm:^1.9.1" + yargs: "npm:^15.4.1" + bin: + whois: bin.js + checksum: 10c0/06e4fb1428c118291972663b2fb6a70acf181e8054be1ea4dd7ab8e8e3c5b050b3180e29c1d29d006db0abe624762752a6ab2314cf68b8c6acad0bb15a48d435 + languageName: node + linkType: hard + "why-is-node-running@npm:^2.3.0": version: 2.3.0 resolution: "why-is-node-running@npm:2.3.0" @@ -2072,6 +2509,17 @@ __metadata: languageName: node linkType: hard +"wrap-ansi@npm:^6.2.0": + version: 6.2.0 + resolution: "wrap-ansi@npm:6.2.0" + dependencies: + ansi-styles: "npm:^4.0.0" + string-width: "npm:^4.1.0" + strip-ansi: "npm:^6.0.0" + checksum: 10c0/baad244e6e33335ea24e86e51868fe6823626e3a3c88d9a6674642afff1d34d9a154c917e74af8d845fd25d170c4ea9cf69a47133c3f3656e1252b3d462d9f6c + languageName: node + linkType: hard + "wrap-ansi@npm:^8.1.0": version: 8.1.0 resolution: "wrap-ansi@npm:8.1.0" @@ -2083,6 +2531,13 @@ __metadata: languageName: node linkType: hard +"y18n@npm:^4.0.0": + version: 4.0.3 + resolution: "y18n@npm:4.0.3" + checksum: 10c0/308a2efd7cc296ab2c0f3b9284fd4827be01cfeb647b3ba18230e3a416eb1bc887ac050de9f8c4fd9e7856b2e8246e05d190b53c96c5ad8d8cb56dffb6f81024 + languageName: node + linkType: hard + "yallist@npm:^4.0.0": version: 4.0.0 resolution: "yallist@npm:4.0.0" @@ -2096,3 +2551,39 @@ __metadata: checksum: 10c0/a499c81ce6d4a1d260d4ea0f6d49ab4da09681e32c3f0472dee16667ed69d01dae63a3b81745a24bd78476ec4fcf856114cb4896ace738e01da34b2c42235416 languageName: node linkType: hard + +"yargs-parser@npm:^18.1.2": + version: 18.1.3 + resolution: "yargs-parser@npm:18.1.3" + dependencies: + camelcase: "npm:^5.0.0" + decamelize: "npm:^1.2.0" + checksum: 10c0/25df918833592a83f52e7e4f91ba7d7bfaa2b891ebf7fe901923c2ee797534f23a176913ff6ff7ebbc1cc1725a044cc6a6539fed8bfd4e13b5b16376875f9499 + languageName: node + linkType: hard + +"yargs@npm:^15.4.1": + version: 15.4.1 + resolution: "yargs@npm:15.4.1" + dependencies: + cliui: "npm:^6.0.0" + decamelize: "npm:^1.2.0" + find-up: "npm:^4.1.0" + get-caller-file: "npm:^2.0.1" + require-directory: "npm:^2.1.1" + require-main-filename: "npm:^2.0.0" + set-blocking: "npm:^2.0.0" + string-width: "npm:^4.2.0" + which-module: "npm:^2.0.0" + y18n: "npm:^4.0.0" + yargs-parser: "npm:^18.1.2" + checksum: 10c0/f1ca680c974333a5822732825cca7e95306c5a1e7750eb7b973ce6dc4f97a6b0a8837203c8b194f461969bfe1fb1176d1d423036635285f6010b392fa498ab2d + languageName: node + linkType: hard + +"yocto-queue@npm:^1.2.1": + version: 1.2.1 + resolution: "yocto-queue@npm:1.2.1" + checksum: 10c0/5762caa3d0b421f4bdb7a1926b2ae2189fc6e4a14469258f183600028eb16db3e9e0306f46e8ebf5a52ff4b81a881f22637afefbef5399d6ad440824e9b27f9f + languageName: node + linkType: hard diff --git a/zsh/memory/macos-memory-reclaim.zsh b/zsh/memory/macos-memory-reclaim.zsh new file mode 100755 index 0000000..9535f82 --- /dev/null +++ b/zsh/memory/macos-memory-reclaim.zsh @@ -0,0 +1,61 @@ +#!/usr/bin/env zsh +set -euo pipefail + +SCRIPT_DIR="${0:A:h}" +REPORT_SCRIPT="${REPORT_SCRIPT:-$SCRIPT_DIR/macos-memory-report.zsh}" +LOG_FILE="${LOG_FILE:-$SCRIPT_DIR/memory-reclaim.log}" + +# Optional: set KILL_EMULATOR=1 to kill emulator/qemu on reclaim +KILL_EMULATOR="${KILL_EMULATOR:-0}" + +timestamp() { date "+%Y-%m-%d %H:%M:%S"; } +log() { echo "[$(timestamp)] $*" | tee -a "$LOG_FILE" >/dev/null; } + +if [[ ! -x "$REPORT_SCRIPT" ]]; then + echo "Expected report script at: $REPORT_SCRIPT" + echo "Set REPORT_SCRIPT=... or put macos-memory-report.zsh in the same folder." + exit 1 +fi + +log "=== RECLAIM START ===" + +log "-- BEFORE (summary) --" +"$REPORT_SCRIPT" | sed -n '1,60p' | tee -a "$LOG_FILE" >/dev/null + +# 1) Restart adb +if command -v adb >/dev/null 2>&1; then + log "Restarting adb server..." + adb kill-server >/dev/null 2>&1 || true + pkill -f adb >/dev/null 2>&1 || true + adb start-server >/dev/null 2>&1 || true +else + log "adb not found; skipping adb restart." +fi + +# 2) Optionally kill emulator/qemu (this is often the actual leaker) +if [[ "$KILL_EMULATOR" == "1" ]]; then + log "Killing emulator/qemu (KILL_EMULATOR=1)..." + pkill -f emulator >/dev/null 2>&1 || true + pkill -f qemu >/dev/null 2>&1 || true +else + log "Not killing emulator/qemu (set KILL_EMULATOR=1 to enable)." +fi + +# 3) Try purge if available +if command -v purge >/dev/null 2>&1; then + log "Running sudo purge..." + sudo purge >/dev/null 2>&1 || true +else + log "purge not available on this macOS build; skipping." +fi + +# 4) Light VM pressure (safe, small) +log "Applying light VM pressure (256MB temp file)..." +dd if=/dev/zero of=/tmp/vm_pressure_test bs=1m count=256 status=none || true +rm -f /tmp/vm_pressure_test || true + +log "-- AFTER (summary) --" +"$REPORT_SCRIPT" | sed -n '1,60p' | tee -a "$LOG_FILE" >/dev/null + +log "=== RECLAIM END ===" +echo diff --git a/zsh/memory/macos-memory-report.zsh b/zsh/memory/macos-memory-report.zsh new file mode 100755 index 0000000..1d5650c --- /dev/null +++ b/zsh/memory/macos-memory-report.zsh @@ -0,0 +1,125 @@ +#!/usr/bin/env zsh +set -euo pipefail + +gb() { + # bytes -> GB + python3 - <<'PY' "$1" +import sys +b=float(sys.argv[1]) +print(f"{b/1024/1024/1024:.2f}") +PY +} + +echo "== macOS Memory Report ==" +echo "Date: $(date)" +echo "Host: $(scutil --get ComputerName 2>/dev/null || hostname)" +echo + +# --- vm_stat summary in GB --- +vm_stat_out="$(vm_stat)" +pagesize="$(echo "$vm_stat_out" | awk '/page size of/ {print $8}')" + +get_pages() { + # returns numeric pages (strip trailing .) + echo "$vm_stat_out" | awk -v key="$1" ' + $0 ~ key { + gsub("\\.", "", $NF) + print $NF + exit + }' +} + +active="$(get_pages "Pages active")" +inactive="$(get_pages "Pages inactive")" +spec="$(get_pages "Pages speculative")" +wired="$(get_pages "Pages wired down")" +compressed="$(get_pages "Pages occupied by compressor")" +free="$(get_pages "Pages free")" + +to_gb() { + # pages -> GB + python3 - </dev/null || true +echo + +# --- per-process RSS totals --- +echo "Process RSS totals:" +ps_total_gb="$(ps -axo rss= | awk '{sum+=$1} END {printf "%.2f", sum/1024/1024}')" +echo "TOTAL APP RSS: ${ps_total_gb} GB (sum of all processes' resident memory)" +echo + +# --- top processes by RSS (in GB) --- +echo "Top processes by RSS (GB):" +ps -axo rss=,pid=,comm= | awk ' +{ + rss_kb=$1; pid=$2; + name=$3; + rss_gb=rss_kb/1024/1024; + printf "%8.2f GB pid=%-6s %s\n", rss_gb, pid, name +}' | sort -nr | head -n 25 +echo + + +# --- WindowServer + kernel_task spot checks --- +for proc in WindowServer kernel_task; do + pid="$(pgrep -x "$proc" 2>/dev/null || true)" + if [[ -n "${pid}" ]]; then + echo "$proc pid=$pid (rss/vsz):" + ps -o pid,rss,vsz,comm -p "$pid" + echo + fi +done + +# --- derived accounting --- +kernelish_gb="$(python3 - </dev/null 2>&1 || { echo "adb not found in PATH" >&2; exit 1; } + +normalize() { echo "$1" | tr '[:upper:]' '[:lower:]'; } + +guess_type() { + local label="$1" + local flags="$2" + local id="$3" + + local l f + l="$(normalize "$label")" + f="$(normalize "$flags")" + + if [[ "$id" == "0" ]]; then echo "owner"; return; fi + if echo "$l $f" | grep -qE 'managed|work|profile_managed|ismanagedprofile'; then echo "work"; return; fi + if echo "$l $f" | grep -qE 'secure folder|securefolder|knox'; then echo "secure"; return; fi + echo "user" +} + +echo "== Raw: cmd user list ==" +RAW_CMD="$(adb shell cmd user list 2>/dev/null || true)" +echo "$RAW_CMD" +echo + +echo "== Raw: dumpsys user (top) ==" +RAW_DUMP="$(adb shell dumpsys user 2>/dev/null || true)" +echo "$RAW_DUMP" | sed -n '1,220p' +echo + +# Build a merged view primarily from dumpsys user because it carries flags/parents. +# We parse blocks that contain "UserInfo{::}" and try to capture flags and parentId. +# Output columns: +# user_id | type | name | flags | parent_id +# +# Note: parsing is best-effort; Android OEMs vary. + +# Extract candidate user lines from dumpsys +mapfile -t USER_LINES < <( + echo "$RAW_DUMP" \ + | sed -n 's/.*UserInfo{\([0-9]\+\):\([^:}]*\):\([0-9]\+\).*/\1|\2|\3/p' \ + | sort -n -t'|' -k1,1 +) + +if [[ ${#USER_LINES[@]} -eq 0 ]]; then + echo "!! Could not parse users from dumpsys user; falling back to cmd user list only." >&2 + # Fallback parse from cmd user list + echo "$RAW_CMD" \ + | sed -n 's/.*UserInfo{\([0-9]\+\):\([^:}]*\).*/\1|\2/p' \ + | sort -n \ + | while IFS='|' read -r id name; do + printf "u%-4s %-7s %s\n" "$id" "$(guess_type "$name" "" "$id")" "$name" + done + exit 0 +fi + +# Function to find a likely flags line for a given user id from dumpsys (best-effort) +get_flags_for_user() { + local id="$1" + # Look for "UserInfo{ID:" line and take nearby "flags=" occurrence + echo "$RAW_DUMP" \ + | awk -v uid="$id" ' + $0 ~ "UserInfo\\{"uid":" {in=1; c=0} + in==1 {print; c++; if (c>40) in=0} + ' \ + | sed -n 's/.*flags=\([^ ]*\).*/\1/p' \ + | head -n 1 +} + +get_parent_for_user() { + local id="$1" + # Try to locate "profileGroupId=" or "parentId=" nearby + echo "$RAW_DUMP" \ + | awk -v uid="$id" ' + $0 ~ "UserInfo\\{"uid":" {in=1; c=0} + in==1 {print; c++; if (c>40) in=0} + ' \ + | sed -n 's/.*profileGroupId=\([-0-9]\+\).*/\1/p; s/.*parentId=\([-0-9]\+\).*/\1/p' \ + | head -n 1 +} + +printf "%-6s %-8s %-28s %-18s %-10s\n" "USER" "TYPE" "NAME" "FLAGS" "PARENT" +printf "%-6s %-8s %-28s %-18s %-10s\n" "----" "----" "----" "-----" "------" + +for L in "${USER_LINES[@]}"; do + UID="${L%%|*}" + REST="${L#*|}" + NAME="${REST%%|*}" + FLAGS="$(get_flags_for_user "$UID")" + PARENT="$(get_parent_for_user "$UID")" + [[ -z "$FLAGS" ]] && FLAGS="-" + [[ -z "$PARENT" ]] && PARENT="-" + TYPE="$(guess_type "$NAME" "$FLAGS" "$UID")" + + printf "u%-5s %-8s %-28s %-18s %-10s\n" "$UID" "$TYPE" "$NAME" "$FLAGS" "$PARENT" +done diff --git a/zsh/platform/android/pull-apks-by-user.zsh b/zsh/platform/android/pull-apks-by-user.zsh new file mode 100755 index 0000000..67b7b72 --- /dev/null +++ b/zsh/platform/android/pull-apks-by-user.zsh @@ -0,0 +1,205 @@ +#!/usr/bin/env zsh + +set -euo pipefail + +# pull-apks-by-user.sh +# Usage: +# ./pull-apks-by-user.sh [output-dir] +# +# Examples: +# ./pull-apks-by-user.sh filemacros +# ./pull-apks-by-user.sh com.virtualize ./tab-s9-apks +# +# Notes: +# - Attempts to pull APK dirs per Android user/profile. +# - Some profiles (e.g., Samsung Secure Folder / Knox) may deny shell access; those get skipped. + +SEARCH_TERM="${1:-}" +OUT_ROOT="${2:-./pulled-apks}" + +if [[ -z "$SEARCH_TERM" ]]; then + echo "Usage: $0 [output-dir]" >&2 + exit 2 +fi + +command -v adb >/dev/null 2>&1 || { echo "adb not found in PATH" >&2; exit 1; } + +mkdir -p "$OUT_ROOT" + +safe_name() { + local s="$1" + echo "$s" | tr -c 'A-Za-z0-9._-' '_' +} + +# Best-effort user type guesser using the label from cmd user list +guess_user_type() { + local label="$1" + local id="$2" + + # Normalize + local l + l="$(echo "$label" | tr '[:upper:]' '[:lower:]')" + + if [[ "$id" == "0" ]]; then + echo "owner" + return + fi + + if echo "$l" | grep -qE 'work|managed|profile'; then + echo "work" + return + fi + + if echo "$l" | grep -qE 'secure folder|securefolder|knox'; then + echo "secure" + return + fi + + # Common Samsung patterns sometimes show "UserInfo{150:...}" without clear label + echo "user" +} + +echo "==> Output dir: $OUT_ROOT" +echo "==> Search term: $SEARCH_TERM" +echo + +# Get list of users: lines like "UserInfo{0:Owner:13} running" +USER_LIST_RAW="$(adb shell cmd user list 2>/dev/null || true)" + +if [[ -z "$USER_LIST_RAW" ]]; then + echo "!! Failed to read user list (adb shell cmd user list)." >&2 + exit 1 +fi + +# Parse user ids + labels +# Extract: id and label inside UserInfo{: