diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 5e28c9e0c..ba70645a9 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -19,6 +19,7 @@ "check-disk-space": "3.4.0", "cli-columns": "^4.0.0", "cli-table3": "^0.6.3", + "commander": "^11.1.0", "configstore": "5.0.1", "copy-dir": "0.4.0", "debug": "4.3.4", @@ -135,6 +136,7 @@ "nock": "13.5.1", "prettier": "npm:wp-prettier@2.8.5", "rimraf": "5.0.5", + "ts-node": "^10.9.1", "typescript": "^5.2.2" }, "engines": { @@ -410,6 +412,15 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/cli/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/@babel/cli/node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -2320,6 +2331,28 @@ "node": ">=0.1.90" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@es-joy/jsdoccomment": { "version": "0.40.1", "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.40.1.tgz", @@ -3662,6 +3695,30 @@ "node": ">=14.16" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, "node_modules/@types/args": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/@types/args/-/args-5.0.3.tgz", @@ -4219,6 +4276,15 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.1.tgz", + "integrity": "sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -4363,6 +4429,12 @@ "node": ">=14" } }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -5619,12 +5691,11 @@ } }, "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", "engines": { - "node": ">= 6" + "node": ">=16" } }, "node_modules/comment-parser": { @@ -5738,6 +5809,12 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -5991,6 +6068,15 @@ "node": ">=8" } }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -10305,6 +10391,12 @@ "semver": "bin/semver.js" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -12575,6 +12667,49 @@ "node": ">=8" } }, + "node_modules/ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, "node_modules/tsconfig-paths": { "version": "3.14.2", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", @@ -13022,6 +13157,12 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, "node_modules/v8-to-istanbul": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", @@ -13617,6 +13758,15 @@ "node": ">=8" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -13816,6 +13966,12 @@ "slash": "^2.0.0" }, "dependencies": { + "commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true + }, "convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -15157,6 +15313,27 @@ "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", "optional": true }, + "@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "dependencies": { + "@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + } + } + }, "@es-joy/jsdoccomment": { "version": "0.40.1", "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.40.1.tgz", @@ -16179,6 +16356,30 @@ "defer-to-connect": "^2.0.1" } }, + "@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, "@types/args": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/@types/args/-/args-5.0.3.tgz", @@ -16635,6 +16836,12 @@ "dev": true, "requires": {} }, + "acorn-walk": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.1.tgz", + "integrity": "sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw==", + "dev": true + }, "agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -16741,6 +16948,12 @@ "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==", "dev": true }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -17645,10 +17858,9 @@ } }, "commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==" }, "comment-parser": { "version": "1.4.0", @@ -17743,6 +17955,12 @@ "prompts": "^2.0.1" } }, + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -17914,6 +18132,12 @@ "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", "dev": true }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, "diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -21062,6 +21286,12 @@ } } }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, "makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -22702,6 +22932,27 @@ "tslib": "^1.9.3" } }, + "ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dev": true, + "requires": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + } + }, "tsconfig-paths": { "version": "3.14.2", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", @@ -23008,6 +23259,12 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" }, + "v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, "v8-to-istanbul": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", @@ -23465,6 +23722,12 @@ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true + }, "yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index a633e66d7..e04cb9daf 100644 --- a/package.json +++ b/package.json @@ -136,6 +136,7 @@ "nock": "13.5.1", "prettier": "npm:wp-prettier@2.8.5", "rimraf": "5.0.5", + "ts-node": "^10.9.1", "typescript": "^5.2.2" }, "dependencies": { @@ -148,6 +149,7 @@ "check-disk-space": "3.4.0", "cli-columns": "^4.0.0", "cli-table3": "^0.6.3", + "commander": "^11.1.0", "configstore": "5.0.1", "copy-dir": "0.4.0", "debug": "4.3.4", diff --git a/src/commands/app.ts b/src/commands/app.ts new file mode 100644 index 000000000..8e22b74d7 --- /dev/null +++ b/src/commands/app.ts @@ -0,0 +1,83 @@ +import chalk from 'chalk'; + +import app from '../lib/api/app'; +import { BaseVIPCommand } from '../lib/base-command'; +import { getEnvIdentifier } from '../lib/cli/command'; +import { trackEvent } from '../lib/tracker'; + +import type { CommandUsage } from '../lib/types/commands'; + +export class AppCommand extends BaseVIPCommand { + protected readonly name: string = 'app'; + + protected readonly usage: CommandUsage = { + description: 'List and modify your VIP applications', + examples: [ + { + description: 'Example 1', + usage: 'vip app app', + }, + { + description: 'Example 2', + usage: 'vip example ', + }, + ], + }; + + // protected readonly commandOptions: CommandOption[] = []; + + protected childCommands: BaseVIPCommand[] = []; + + protected async execute( ...arg: unknown[] ): void { + let res; + try { + res = await app( + arg[ 0 ], + 'id,repo,name,environments{id,appId,name,type,branch,currentCommit,primaryDomain{name},launched}' + ); + } catch ( err ) { + await trackEvent( 'app_command_fetch_error', { + error: `App ${ arg[ 0 ] } does not exist`, + } ); + + console.log( `App ${ chalk.blueBright( arg[ 0 ] ) } does not exist` ); + return; + } + + if ( ! res || ! res.environments ) { + await trackEvent( 'app_command_fetch_error', { + error: `App ${ arg[ 0 ] } does not exist`, + } ); + + console.log( `App ${ chalk.blueBright( arg[ 0 ] ) } does not exist` ); + return; + } + + await trackEvent( 'app_command_success' ); + + // Clone the read-only response object so we can modify it + const clonedResponse = Object.assign( {}, res ); + + const header = [ + { key: 'id', value: res.id }, + { key: 'name', value: res.name }, + { key: 'repo', value: res.repo }, + ]; + + clonedResponse.environments = clonedResponse.environments.map( env => { + const clonedEnv = Object.assign( {}, env ); + + clonedEnv.name = getEnvIdentifier( env ); + + // Use the short version of git commit hash + clonedEnv.currentCommit = clonedEnv.currentCommit.substring( 0, 7 ); + + // Flatten object + clonedEnv.primaryDomain = clonedEnv.primaryDomain.name; + delete clonedEnv.__typename; + return clonedEnv; + } ); + + return { header, data: clonedResponse.environments }; + } +} diff --git a/src/commands/example-command.ts b/src/commands/example-command.ts new file mode 100644 index 000000000..f79f28794 --- /dev/null +++ b/src/commands/example-command.ts @@ -0,0 +1,80 @@ +import { BaseVIPCommand } from '../lib/base-command'; + +import type { CommandOption, CommandUsage } from '../lib/types/commands'; + +export class ExampleCommand extends BaseVIPCommand { + protected readonly name: string = 'example'; + + protected readonly usage: CommandUsage = { + description: 'Example command', + examples: [ + { + description: 'Example 1', + usage: 'vip example arg1 arg2', + }, + { + description: 'Example 2', + usage: 'vip example --named=arg1 --also=arg2', + }, + ], + }; + + protected readonly commandOptions: CommandOption[] = [ + { + name: '--slug , -s ', + description: 'An env slug', + type: 'string', + required: true, + }, + ]; + + protected childCommands: BaseVIPCommand[] = [ new ExampleChildCommand() ]; + + protected execute( opts: unknown[] ): void { + console.log( 'parent', this.getName() ); + } +} + +export class ExampleChildCommand extends BaseVIPCommand { + protected readonly name: string = 'child'; + protected readonly usage: CommandUsage = { + description: 'Example child command', + examples: [ + { + description: 'Example 1', + usage: 'vip example child arg1 arg2', + }, + { + description: 'Example 2', + usage: 'vip example child --named=arg1 --also=arg2', + }, + ], + }; + + protected childCommands: BaseVIPCommand[] = [ new ExampleGrandChildCommand() ]; + + protected execute( opts: unknown[], ...args: unknown[] ): void { + console.log( this.getName() ); + } +} + +export class ExampleGrandChildCommand extends BaseVIPCommand { + protected readonly name: string = 'grandchild'; + protected readonly usage: CommandUsage = { + description: 'Example grandchild command', + examples: [ + { + description: 'Example 1', + usage: 'vip example child arg1 arg2', + }, + { + description: 'Example 2', + usage: 'vip example child --named=arg1 --also=arg2', + }, + ], + }; + + protected execute( opts: unknown[], ...args: unknown[] ): void { + console.log( this.getName() ); + } +} diff --git a/src/lib/base-command.ts b/src/lib/base-command.ts new file mode 100644 index 000000000..82f85a85b --- /dev/null +++ b/src/lib/base-command.ts @@ -0,0 +1,310 @@ +import debugLib from 'debug'; +import { prompt } from 'enquirer'; + +import { formatData } from './cli/format'; +import Token from './token'; +import { trackEvent, aliasUser } from './tracker'; + +import type { CommandOption, CommandArgument, CommandUsage } from './types/commands'; +// Needs to go inside the command +const debug = debugLib( '@automattic/vip:bin:vip' ); +// Config +const tokenURL = 'https://dashboard.wpvip.com/me/cli/token'; + +export abstract class BaseVIPCommand { + protected name: string = 'vip'; + protected isDebugConfirmed: boolean = false; + protected needsAuth: boolean = true; + + protected readonly commandOptions: CommandOption[] = [ + { + name: 'app', + description: 'Application id or slug', + type: 'string', + required: false, + }, + { + name: 'env', + description: 'Application environment', + type: 'string', + required: false, + }, + ]; + + protected readonly commandArguments: CommandArgument[] = [ + { + name: 'app', + description: 'Application id or slug', + type: 'string', + required: false, + }, + ]; + + protected readonly usage: CommandUsage = { + description: 'Base command', + examples: [ + { + description: 'Example 1', + usage: 'vip example arg1 arg2', + }, + { + description: 'Example 2', + usage: 'vip example --named=arg1 --also=arg2', + }, + ], + }; + + protected childCommands: BaseVIPCommand[] = []; + + public constructor() {} + + protected getTrackingParams( _args: Record< string, unknown > ): Record< string, unknown > { + return {}; + } + + protected shouldTrackFailure( _error: Error ): boolean { + return true; + } + + /** + * Authentication routine. + * This will prompt the user to authenticate with their VIP account. + * + * @protected + * @returns {Promise< void >} + * @memberof BaseVIPCommand + */ + protected async authenticate(): Promise< void > { + /** + * @param {any[]} argv + * @param {any[]} params + * @returns {boolean} + */ + function doesArgvHaveAtLeastOneParam( argv, params ) { + return argv.some( arg => params.includes( arg ) ); + } + + let token = await Token.get(); + + const isHelpCommand = doesArgvHaveAtLeastOneParam( process.argv, [ 'help', '-h', '--help' ] ); + const isVersionCommand = doesArgvHaveAtLeastOneParam( process.argv, [ '-v', '--version' ] ); + const isLogoutCommand = doesArgvHaveAtLeastOneParam( process.argv, [ 'logout' ] ); + const isLoginCommand = doesArgvHaveAtLeastOneParam( process.argv, [ 'login' ] ); + const isDevEnvCommandWithoutEnv = + doesArgvHaveAtLeastOneParam( process.argv, [ 'dev-env' ] ) && + ! containsAppEnvArgument( process.argv ); + + debug( 'Argv:', process.argv ); + + if ( + ! isLoginCommand && + ( isLogoutCommand || + isHelpCommand || + isVersionCommand || + isDevEnvCommandWithoutEnv || + token?.valid() ) + ) { + return; + } + + console.log(); + console.log( ' _ __ ________ ________ ____' ); + console.log( ' | | / // _/ __ / ____/ / / _/' ); + console.log( ' | | / / / // /_/ /______/ / / / / / ' ); + console.log( ' | |/ /_/ // ____//_____/ /___/ /____/ / ' ); + console.log( ' |___//___/_/ ____/_____/___/ ' ); + console.log(); + + console.log( + ' VIP-CLI is your tool for interacting with and managing your VIP applications.' + ); + console.log(); + + console.log( + ' Authenticate your installation of VIP-CLI with your Personal Access Token. This URL will be opened in your web browser automatically so that you can retrieve your token: ' + + tokenURL + ); + console.log(); + + await trackEvent( 'login_command_execute' ); + + const answer = await prompt( { + type: 'confirm', + name: 'continue', + message: 'Ready to authenticate?', + } ); + + if ( ! answer.continue ) { + await trackEvent( 'login_command_browser_cancelled' ); + + return; + } + + const { default: open } = await import( 'open' ); + + await open( tokenURL, { wait: false } ); + + await trackEvent( 'login_command_browser_opened' ); + + const { token: tokenInput } = await prompt( { + type: 'password', + name: 'token', + message: 'Access Token:', + } ); + + try { + token = new Token( tokenInput ); + } catch ( err ) { + console.log( 'The token provided is malformed. Please check the token and try again.' ); + + await trackEvent( 'login_command_token_submit_error', { error: err.message } ); + + return; + } + + if ( token.expired() ) { + console.log( 'The token provided is expired. Please log in again to refresh the token.' ); + + await trackEvent( 'login_command_token_submit_error', { error: 'expired' } ); + + return; + } + + if ( ! token.valid() ) { + console.log( 'The provided token is not valid. Please log in again to refresh the token.' ); + + await trackEvent( 'login_command_token_submit_error', { error: 'invalid' } ); + + return; + } + + try { + await Token.set( token.raw ); + } catch ( err ) { + await trackEvent( 'login_command_token_submit_error', { + error: err.message, + } ); + + throw err; + } + + // De-anonymize user for tracking + await aliasUser( token.id ); + + await trackEvent( 'login_command_token_submit_success' ); + + if ( isLoginCommand ) { + console.log( 'You are now logged in - see `vip -h` for a list of available commands.' ); + + process.exit(); + } + } + + // args length can vary based the number of arguments and options the command defines, the command itsrlf is always the last argument + // Can some of this logic be moved out to a hook? + + /** + * This is a wrapper method that performs common routines before and after executing the command + * + * @param {...unknown[]} args + * @returns {Promise< void >} + * @memberof BaseVIPCommand + */ + public async run( ...args: unknown[] ): Promise< void > { + if ( this.needsAuth ) { + try { + await this.authenticate(); + } catch ( error ) { + console.log( error ); + } + } + + let res; + // Invoke the command and send tracking information + const trackingParams = this.getTrackingParams( { args } ); + // console.log( args ); + // let [ _args, opts, command ] = args; + const command = args[ args.length - 1 ]; + const _opts = command.opts(); + // console.log( command.opts() ); + + if ( _opts?.inspect && ! this.isDebugConfirmed ) { + await prompt( { + type: 'confirm', + name: 'confirm', + message: "Attach the debugger, once you see 'Debugger attached' above hit 'y' to continue", + } ); + this.isDebugConfirmed = true; + } + + try { + await trackEvent( `${ this.name }_execute`, trackingParams ); + res = await this.execute( ...args ); + + if ( _opts.format && res ) { + if ( res.header ) { + if ( _opts.format !== 'json' ) { + console.log( formatData( res.header, 'keyValue' ) ); + } + res = res.data; + } + + res = res.map( row => { + const out = { ...row }; + if ( out.__typename ) { + // Apollo injects __typename + delete out.__typename; + } + + return out; + } ); + + await trackEvent( 'command_output', { + format: _opts.format, + } ); + + const formattedOut = formatData( res, _opts.format ); + + console.log( formattedOut ); + + return {}; + } + await trackEvent( `${ this.name }_success`, trackingParams ); + } catch ( error ) { + const err = + error instanceof Error ? error : new Error( error?.toString() ?? 'Unknown error' ); + + if ( this.shouldTrackFailure( err ) ) { + await trackEvent( `${ this.name }_error`, { + ...trackingParams, + failure: err.message, + stack: err.stack, + } ); + } + + throw error; + } + } + + protected abstract execute( ..._args: unknown[] ): void; + + public getName(): string { + return this.name; + } + + public getUsage(): CommandUsage { + return this.usage; + } + + public getChildCommands(): BaseVIPCommand[] { + return this.childCommands; + } + + public getOptions(): CommandOption[] { + return this.commandOptions; + } + + public getArguments(): CommandArgument[] { + return this.commandArguments; + } +} diff --git a/src/lib/command-registry.ts b/src/lib/command-registry.ts new file mode 100644 index 000000000..1d0371006 --- /dev/null +++ b/src/lib/command-registry.ts @@ -0,0 +1,42 @@ +/** + * The registry that stores/invokes all the commands. + * + * The main entry point will call it. + * + * @class CommandRegistry + */ +import { BaseVIPCommand } from './base-command'; + +export class CommandRegistry { + private static instance: CommandRegistry; + private readonly commands: Map< string, BaseVIPCommand >; + + private constructor() { + this.commands = new Map< string, BaseVIPCommand >(); + } + + public static getInstance(): CommandRegistry { + if ( ! CommandRegistry.instance ) { + CommandRegistry.instance = new CommandRegistry(); + } + return CommandRegistry.instance; + } + + public registerCommand( command: BaseVIPCommand ): void { + this.commands.set( command.getName(), command ); + } + + public async invokeCommand( commandName: string, ...args: unknown[] ): Promise< void > { + const command = this.commands.get( commandName ); + if ( command ) { + await command.run( ...args ); + } else { + console.log( this.commands ); + throw new Error( `Command '${ commandName }' not found.` ); + } + } + + public getCommands(): Map< string, BaseVIPCommand > { + return this.commands; + } +} diff --git a/src/lib/command.ts b/src/lib/command.ts new file mode 100644 index 000000000..02370d0f2 --- /dev/null +++ b/src/lib/command.ts @@ -0,0 +1,123 @@ +import { Command } from 'commander'; + +import { BaseVIPCommand } from './base-command'; +import { parseEnvAliasFromArgv } from './cli/envAlias'; +import { CommandRegistry } from './command-registry'; +import { description, version } from '../../package.json'; +import { AppCommand } from '../commands/app'; +import { ExampleCommand } from '../commands/example-command'; + +/** + * Base Command from which every subcommand should inherit. + * + * @class BaseCommand + */ + +const makeVIPCommand = ( command: BaseVIPCommand ): Command => { + const usage = command.getUsage(); + const options = command.getOptions(); + const name = command.getName(); + const commandArgs = command.getArguments(); + const cmd = new Command( name ).description( usage.description ); + + for ( const argument of commandArgs ) { + let argumentName = argument.name; + if ( argument.required ) { + argumentName = `<${ argumentName }>`; + } else { + argumentName = `[${ argumentName }]`; + } + + cmd.argument( argumentName, argument.description ); + } + + for ( const option of options ) { + cmd.option( option.name, option.description ); + } + + cmd + .option( '-d, --debug [component]', 'Show debug' ) + .option( '--inspect', 'Attach a debugger' ) + .option( '--format [json|table|csv|ids]', 'Output format' ); + + cmd.action( async ( ...args: unknown[] ) => { + await registry.invokeCommand( name, ...args ); + } ); + + cmd.configureHelp( { showGlobalOptions: true } ); + return cmd; +}; + +const processCommand = ( parent: Command, command: BaseVIPCommand ): void => { + const cmd = makeVIPCommand( command ); + + command.getChildCommands().forEach( childCommand => { + registry.registerCommand( childCommand ); + processCommand( cmd, childCommand ); + } ); + parent.addCommand( cmd ); +}; + +/** + * @param {string[]} args + * @param {Command} command + * @returns {string[]} + */ +function sortArguments( args, command ) { + const subcommands = command.commands.map( cmd => cmd.name() ); + if ( subcommands.length ) { + const saved = []; + while ( args.length ) { + const arg = /** @type {string} */ args.shift(); + if ( arg === '--' ) { + return [ ...saved, arg, ...args ]; + } + + if ( subcommands.includes( arg ) ) { + return [ + arg, + ...sortArguments( + [ ...saved, ...args ], + /** @type {Command} */ command.commands.find( cmd => cmd.name() === arg ) + ), + ]; + } + + saved.push( arg ); + } + + return [ ...saved ]; + } + + return args; +} + +const program = new Command(); + +program + .name( 'vip' ) + .description( description ) + .version( version ) + .configureHelp( { showGlobalOptions: true } ); + +const registry = CommandRegistry.getInstance(); +registry.registerCommand( new ExampleCommand() ); +registry.registerCommand( new AppCommand() ); + +[ ...registry.getCommands().values() ].map( command => processCommand( program, command ) ); + +const { argv, ...appAlias } = parseEnvAliasFromArgv( process.argv ); + +// let appAliasString = Object.values( appAlias ).filter( e => e ).join( '.' ); +// if ( appAliasString ) { +// argv.push( `@${ appAliasString }` ); +// } +// console.log(appAlias); +console.log( argv, sortArguments( process.argv.slice( 2 ), program ), { appAlias }, [ + ...argv.slice( 0, 2 ), + ...sortArguments( process.argv.slice( 2 ), program ), +] ); +// program.parse( sortArguments(process.argv, program ), { appAlias } ); +program.parse( [ ...argv.slice( 0, 2 ), ...sortArguments( process.argv.slice( 2 ), program ) ], { + appAlias, +} ); diff --git a/src/lib/types/commands.ts b/src/lib/types/commands.ts new file mode 100644 index 000000000..4cdb34a67 --- /dev/null +++ b/src/lib/types/commands.ts @@ -0,0 +1,24 @@ +export interface CommandExample { + description: string; + usage: string; +} + +export interface CommandOption { + name: string; + alias?: string; + description: string; + type: string; + required?: boolean; +} + +export interface CommandArgument { + name: string; + description: string; + type: string; + required: boolean; +} + +export interface CommandUsage { + description: string; + examples: CommandExample[]; +}