From a1127c8f2c2d809d904a0d1d4c58e6bd370ae530 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Thu, 29 Oct 2026 01:18:53 +0800 Subject: [PATCH 1/6] fix: remove build-only dependencies from runtime --- package.json | 4 +- pnpm-lock.yaml | 228 +------------------------------------------ test/helpers/init.js | 13 --- 3 files changed, 4 insertions(+), 241 deletions(-) diff --git a/package.json b/package.json index 5444642..9582301 100644 --- a/package.json +++ b/package.json @@ -12,14 +12,11 @@ "@oclif/core": "2.8.2", "@oclif/plugin-help": "5.2.9", "@oclif/plugin-version": "1.3.3", - "cli-ux": "^5.4.4", "fs-extra": "^8.1.0", "lodash.defaults": "^4.2.0", - "oclif": "3.8.1", "tslib": "^2.5.0" }, "devDependencies": { - "@oclif/test": "^2.3.17", "@types/chai": "^4.2.7", "@types/fs-extra": "^8.0.1", "@types/lodash.defaults": "^4.2.6", @@ -31,6 +28,7 @@ "eslint-config-oclif": "^6.0.130", "mocha": "^10.2.0", "nyc": "^13.3.0", + "oclif": "3.8.1", "shx": "^0.3.4", "ts-node": "^10.9.1", "typescript": "^5.6.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4243e40..3bedd8b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,25 +23,16 @@ importers: '@oclif/plugin-version': specifier: 1.3.3 version: 1.3.3(@types/node@24.10.8)(typescript@5.9.3) - cli-ux: - specifier: ^5.4.4 - version: 5.6.7(@oclif/config@1.18.16) fs-extra: specifier: ^8.1.0 version: 8.1.0 lodash.defaults: specifier: ^4.2.0 version: 4.2.0 - oclif: - specifier: 3.8.1 - version: 3.8.1(@types/node@24.10.8)(encoding@0.1.13)(mem-fs@2.3.0)(typescript@5.9.3) tslib: specifier: ^2.5.0 version: 2.8.1 devDependencies: - '@oclif/test': - specifier: ^2.3.17 - version: 2.5.6(@types/node@24.10.8)(typescript@5.9.3) '@types/chai': specifier: ^4.2.7 version: 4.3.20 @@ -75,6 +66,9 @@ importers: nyc: specifier: ^13.3.0 version: 13.3.0 + oclif: + specifier: 3.8.1 + version: 3.8.1(@types/node@24.10.8)(encoding@0.1.13)(mem-fs@2.3.0)(typescript@5.9.3) shx: specifier: ^0.3.4 version: 0.3.4 @@ -375,18 +369,6 @@ packages: resolution: {integrity: sha512-NCcr1uQo1k5U+SYlnIrbAh3cxy+OQT1VtqiAbxdymSlptbzBb62AjH2xXgjNCoP073hoa1CfCAcwoZ8k96C4nA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - '@oclif/command@1.8.36': - resolution: {integrity: sha512-/zACSgaYGtAQRzc7HjzrlIs14FuEYAZrMOEwicRoUnZVyRunG4+t5iSEeQu0Xy2bgbCD0U1SP/EdeNZSTXRwjQ==} - engines: {node: '>=12.0.0'} - deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. - peerDependencies: - '@oclif/config': ^1 - - '@oclif/config@1.18.16': - resolution: {integrity: sha512-VskIxVcN22qJzxRUq+raalq6Q3HUde7sokB7/xk5TqRZGEKRVbFeqdQBxDWwQeudiJEgcNiMvIFbMQ43dY37FA==} - engines: {node: '>=8.0.0'} - deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. - '@oclif/core@2.16.0': resolution: {integrity: sha512-dL6atBH0zCZl1A1IXCKJgLPrM/wR7K+Wi401E/IvqsK8m2iCHW+0TEOGrans/cuN3oTW+uxIyJFHJ8Im0k4qBw==} engines: {node: '>=14.0.0'} @@ -395,24 +377,6 @@ packages: resolution: {integrity: sha512-g50NrCdEcFlBfuwZb9RxLmxPNQ9wIaBPOiwbxlGYRkHMnsC6LNHcvVtyDnmndU8qoXrmCOZ6ocSZenOMlG+G1w==} engines: {node: '>=14.0.0'} - '@oclif/errors@1.3.6': - resolution: {integrity: sha512-fYaU4aDceETd89KXP+3cLyg9EHZsLD3RxF2IU9yxahhBpspWjkWi3Dy3bTgcwZ3V47BgxQaGapzJWDM33XIVDQ==} - engines: {node: '>=8.0.0'} - deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. - - '@oclif/help@1.0.15': - resolution: {integrity: sha512-Yt8UHoetk/XqohYX76DfdrUYLsPKMc5pgkzsZVHDyBSkLiGRzujVaGZdjr32ckVZU9q3a47IjhWxhip7Dz5W/g==} - engines: {node: '>=8.0.0'} - deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. - - '@oclif/linewrap@1.0.0': - resolution: {integrity: sha512-Ups2dShK52xXa8w6iBWLgcjPJWjais6KPJQq3gQ/88AY6BXoTX+MIGFPrWQO1KLMiQfoTpcLnUwloN4brrVUHw==} - - '@oclif/parser@3.8.17': - resolution: {integrity: sha512-l04iSd0xoh/16TGVpXb81Gg3z7tlQGrEup16BrVLsZBK6SEYpYHRJZnM32BwZrHI97ZSFfuSwVlzoo6HdsaK8A==} - engines: {node: '>=8.0.0'} - deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. - '@oclif/plugin-help@5.2.9': resolution: {integrity: sha512-0J3oowPURZJ4Dn1p1WpQ46E4+CoV20KTn1cvsNiDl6Hmbw+qoljKQnArJJzNFeZQxWo4R7/S42PrzKJTVYh68Q==} engines: {node: '>=12.0.0'} @@ -429,15 +393,6 @@ packages: resolution: {integrity: sha512-y7eSzT6R5bmTIJbiMMXgOlbBpcWXGlVhNeQJBLBCCy1+90Wbjyqf6uvY0i2WcO4sh/THTJ20qCW80j3XUlgDTA==} engines: {node: '>=12.0.0'} - '@oclif/screen@1.0.4': - resolution: {integrity: sha512-60CHpq+eqnTxLZQ4PGHYNwUX572hgpMHGPtTWMjdTMsAvlm69lZV/4ly6O3sAYkomo4NggGcomrDpBe34rxUqw==} - engines: {node: '>=8.0.0'} - deprecated: Deprecated in favor of @oclif/core - - '@oclif/test@2.5.6': - resolution: {integrity: sha512-AcusFApdU6/akXaofhBDrY4IM9uYzlOD9bYCCM0NwUXOv1m6320hSp2DT/wkj9H1gsvKbJXZHqgtXsNGZTWLFg==} - engines: {node: '>=12.0.0'} - '@octokit/auth-token@2.5.0': resolution: {integrity: sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g==} @@ -608,12 +563,6 @@ packages: '@types/responselike@1.0.3': resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} - '@types/sinon@21.0.0': - resolution: {integrity: sha512-+oHKZ0lTI+WVLxx1IbJDNmReQaIsQJjN2e7UUrJHEeByG7bFeKJYsv1E75JxTQ9QKJDp21bAa/0W2Xo4srsDnw==} - - '@types/sinonjs__fake-timers@15.0.1': - resolution: {integrity: sha512-Ko2tjWJq8oozHzHV+reuvS5KYIRAokHnGbDwGh/J64LntgpbuylF74ipEL24HCyRjf9FOlBiBHWBR1RlVKsI1w==} - '@types/vinyl@2.0.12': resolution: {integrity: sha512-Sr2fYMBUVGYq8kj3UthXFAu5UN6ZW+rYr4NACjZQJvHvj+c8lYv0CahmZ2P/r7iUkN44gGUBwqxZkrKXYPb7cw==} @@ -1129,11 +1078,6 @@ packages: resolution: {integrity: sha512-IqLQi4lO0nIB4tcdTpN4LCB9FI3uqrJZK7RC515EnhZ6qBaglkIgICb1wjeAqpdoOabm1+SuQtkXIPdYC93jhQ==} engines: {node: '>= 0.2.0'} - cli-ux@5.6.7: - resolution: {integrity: sha512-dsKAurMNyFDnO6X1TiiRNiVbL90XReLKcvIq4H777NMqXGBxBws23ag8ubCJE97vVZEgWG2eSUhsyLf63Jv8+g==} - engines: {node: '>=8.0.0'} - deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. - cli-width@3.0.0: resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} engines: {node: '>= 10'} @@ -1667,15 +1611,6 @@ packages: exponential-backoff@3.1.3: resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} - extract-stack@2.0.0: - resolution: {integrity: sha512-AEo4zm+TenK7zQorGK1f9mJ8L14hnTDi2ZQPR+Mub1NX8zimka1mXpV5LpH8x9HoUmFSHZCfLHqWvp0Y4FxxzQ==} - engines: {node: '>=8'} - - fancy-test@2.0.42: - resolution: {integrity: sha512-TX8YTALYAmExny+f+G24MFxWry3Pk09+9uykwRjfwjibRxJ9ZjJzrnHYVBZK46XQdyli7d+rQc5U/KK7V6uLsw==} - engines: {node: '>=12.0.0'} - deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. - fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -2336,9 +2271,6 @@ packages: json-stringify-nice@1.1.4: resolution: {integrity: sha512-5Z5RFW63yxReJ7vANgW6eZFGWaQvnPE3WNmZoOJrSkGju2etKA2L5rrOa1sm877TVTFt57A80BH1bArcmlLfPw==} - json-stringify-safe@5.0.1: - resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} - json5@1.0.2: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true @@ -2571,9 +2503,6 @@ packages: engines: {node: '>= 14.0.0'} hasBin: true - mock-stdin@1.0.0: - resolution: {integrity: sha512-tukRdb9Beu27t6dN+XztSRHq9J0B/CoAOySGzHfn8UTfmqipA5yNT/sDUEyYdAV3Hpka6Wx6kOMxuObdOex60Q==} - ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -2603,10 +2532,6 @@ packages: resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} engines: {node: '>= 0.6'} - nock@13.5.6: - resolution: {integrity: sha512-o2zOYiCpzRqSzPj0Zt/dQ/DqZeYoaQ7TUonc/xUPjCGl9WeHpNbxgVvOquXYAaJzI0M9BXV3HTzG0p8IUAbBTQ==} - engines: {node: '>= 10.13'} - node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -3004,10 +2929,6 @@ packages: resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} engines: {node: '>=10'} - propagate@2.0.1: - resolution: {integrity: sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==} - engines: {node: '>= 8'} - proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} @@ -3343,10 +3264,6 @@ packages: stable-hash@0.0.5: resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} - stdout-stderr@0.1.13: - resolution: {integrity: sha512-Xnt9/HHHYfjZ7NeQLvuQDyL1LnbsbddgMFKCuaQKwGCdJm8LnstZIXop+uOY36UR1UXXoHXfMbC1KlVdVd2JLA==} - engines: {node: '>=8.0.0'} - stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} @@ -4224,28 +4141,6 @@ snapshots: - bluebird - supports-color - '@oclif/command@1.8.36(@oclif/config@1.18.16)(supports-color@8.1.1)': - dependencies: - '@oclif/config': 1.18.16(supports-color@8.1.1) - '@oclif/errors': 1.3.6 - '@oclif/help': 1.0.15(supports-color@8.1.1) - '@oclif/parser': 3.8.17 - debug: 4.4.3(supports-color@8.1.1) - semver: 7.7.3 - transitivePeerDependencies: - - supports-color - - '@oclif/config@1.18.16(supports-color@8.1.1)': - dependencies: - '@oclif/errors': 1.3.6 - '@oclif/parser': 3.8.17 - debug: 4.4.3(supports-color@8.1.1) - globby: 11.1.0 - is-wsl: 2.2.0 - tslib: 2.8.1 - transitivePeerDependencies: - - supports-color - '@oclif/core@2.16.0(@types/node@24.10.8)(typescript@5.9.3)': dependencies: '@types/cli-progress': 3.11.6 @@ -4319,37 +4214,6 @@ snapshots: - '@types/node' - typescript - '@oclif/errors@1.3.6': - dependencies: - clean-stack: 3.0.1 - fs-extra: 8.1.0 - indent-string: 4.0.0 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - - '@oclif/help@1.0.15(supports-color@8.1.1)': - dependencies: - '@oclif/config': 1.18.16(supports-color@8.1.1) - '@oclif/errors': 1.3.6 - chalk: 4.1.2 - indent-string: 4.0.0 - lodash: 4.17.21 - string-width: 4.2.3 - strip-ansi: 6.0.1 - widest-line: 3.1.0 - wrap-ansi: 6.2.0 - transitivePeerDependencies: - - supports-color - - '@oclif/linewrap@1.0.0': {} - - '@oclif/parser@3.8.17': - dependencies: - '@oclif/errors': 1.3.6 - '@oclif/linewrap': 1.0.0 - chalk: 4.1.2 - tslib: 2.8.1 - '@oclif/plugin-help@5.2.9(@types/node@24.10.8)(typescript@5.9.3)': dependencies: '@oclif/core': 2.8.2(@types/node@24.10.8)(typescript@5.9.3) @@ -4394,19 +4258,6 @@ snapshots: - supports-color - typescript - '@oclif/screen@1.0.4': {} - - '@oclif/test@2.5.6(@types/node@24.10.8)(typescript@5.9.3)': - dependencies: - '@oclif/core': 2.16.0(@types/node@24.10.8)(typescript@5.9.3) - fancy-test: 2.0.42 - transitivePeerDependencies: - - '@swc/core' - - '@swc/wasm' - - '@types/node' - - supports-color - - typescript - '@octokit/auth-token@2.5.0': dependencies: '@octokit/types': 6.41.0 @@ -4615,12 +4466,6 @@ snapshots: dependencies: '@types/node': 24.10.8 - '@types/sinon@21.0.0': - dependencies: - '@types/sinonjs__fake-timers': 15.0.1 - - '@types/sinonjs__fake-timers@15.0.1': {} - '@types/vinyl@2.0.12': dependencies: '@types/expect': 1.20.4 @@ -5212,37 +5057,6 @@ snapshots: dependencies: colors: 1.0.3 - cli-ux@5.6.7(@oclif/config@1.18.16): - dependencies: - '@oclif/command': 1.8.36(@oclif/config@1.18.16)(supports-color@8.1.1) - '@oclif/errors': 1.3.6 - '@oclif/linewrap': 1.0.0 - '@oclif/screen': 1.0.4 - ansi-escapes: 4.3.2 - ansi-styles: 4.3.0 - cardinal: 2.1.1 - chalk: 4.1.2 - clean-stack: 3.0.1 - cli-progress: 3.12.0 - extract-stack: 2.0.0 - fs-extra: 8.1.0 - hyperlinker: 1.0.0 - indent-string: 4.0.0 - is-wsl: 2.2.0 - js-yaml: 3.14.2 - lodash: 4.17.21 - natural-orderby: 2.0.3 - object-treeify: 1.1.33 - password-prompt: 1.1.3 - semver: 7.7.3 - string-width: 4.2.3 - strip-ansi: 6.0.1 - supports-color: 8.1.1 - supports-hyperlinks: 2.3.0 - tslib: 2.8.1 - transitivePeerDependencies: - - '@oclif/config' - cli-width@3.0.0: {} cliui@7.0.4: @@ -5913,21 +5727,6 @@ snapshots: exponential-backoff@3.1.3: {} - extract-stack@2.0.0: {} - - fancy-test@2.0.42: - dependencies: - '@types/chai': 4.3.20 - '@types/lodash': 4.17.23 - '@types/node': 24.10.8 - '@types/sinon': 21.0.0 - lodash: 4.17.21 - mock-stdin: 1.0.0 - nock: 13.5.6 - stdout-stderr: 0.1.13 - transitivePeerDependencies: - - supports-color - fast-deep-equal@3.1.3: {} fast-glob@3.3.3: @@ -6609,8 +6408,6 @@ snapshots: json-stringify-nice@1.1.4: {} - json-stringify-safe@5.0.1: {} - json5@1.0.2: dependencies: minimist: 1.2.8 @@ -6915,8 +6712,6 @@ snapshots: yargs-parser: 20.2.9 yargs-unparser: 2.0.0 - mock-stdin@1.0.0: {} - ms@2.1.3: {} multimatch@5.0.0: @@ -6939,14 +6734,6 @@ snapshots: negotiator@0.6.4: {} - nock@13.5.6: - dependencies: - debug: 4.4.3(supports-color@8.1.1) - json-stringify-safe: 5.0.1 - propagate: 2.0.1 - transitivePeerDependencies: - - supports-color - node-fetch@2.7.0(encoding@0.1.13): dependencies: whatwg-url: 5.0.0 @@ -7427,8 +7214,6 @@ snapshots: err-code: 2.0.3 retry: 0.12.0 - propagate@2.0.1: {} - proxy-from-env@1.1.0: {} pump@3.0.3: @@ -7798,13 +7583,6 @@ snapshots: stable-hash@0.0.5: {} - stdout-stderr@0.1.13: - dependencies: - debug: 4.4.3(supports-color@8.1.1) - strip-ansi: 6.0.1 - transitivePeerDependencies: - - supports-color - stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0 diff --git a/test/helpers/init.js b/test/helpers/init.js index 6e09315..58bddba 100644 --- a/test/helpers/init.js +++ b/test/helpers/init.js @@ -40,16 +40,3 @@ if (fs.existsSync(envPath)) { process.env.TS_NODE_PROJECT = path.resolve('test/tsconfig.json') process.env.NODE_ENV = 'development' - -// Initialize oclif config for @oclif/test -try { - const {Config} = require('@oclif/config') - const config = Config.load() - globalThis.oclif = globalThis.oclif || {} - globalThis.oclif.config = config - globalThis.oclif.columns = 80 -} catch { - // If oclif config can't be loaded, set minimal globals - globalThis.oclif = globalThis.oclif || {} - globalThis.oclif.columns = 80 -} From 7e3ad19a02b0028280bbb37a91f8ee913de97562 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Thu, 29 Oct 2026 01:21:09 +0800 Subject: [PATCH 2/6] refactor: decouple table output from oclif --- .mocharc.json | 1 + package.json | 6 +- pnpm-lock.yaml | 44 +++++---- src/commands/folders/create.ts | 7 +- src/commands/folders/index.ts | 7 +- src/commands/history.ts | 7 +- src/commands/notes/create.ts | 7 +- src/commands/notes/index.ts | 7 +- src/commands/team-folders/create.ts | 7 +- src/commands/team-folders/index.ts | 7 +- src/commands/team-notes/create.ts | 7 +- src/commands/team-notes/index.ts | 7 +- src/commands/teams.ts | 7 +- src/commands/whoami.ts | 7 +- src/table.ts | 143 ++++++++++++++++++++++++++++ test/table.test.ts | 120 +++++++++++++++++++++++ tsconfig.json | 2 +- 17 files changed, 341 insertions(+), 52 deletions(-) create mode 100644 src/table.ts create mode 100644 test/table.test.ts diff --git a/.mocharc.json b/.mocharc.json index 4a09d14..c33f5de 100644 --- a/.mocharc.json +++ b/.mocharc.json @@ -1,4 +1,5 @@ { + "node-option": ["no-experimental-strip-types"], "require": [ "test/helpers/init.js", "ts-node/register" diff --git a/package.json b/package.json index 9582301..7dfc08c 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,13 @@ "@oclif/core": "2.8.2", "@oclif/plugin-help": "5.2.9", "@oclif/plugin-version": "1.3.3", + "cli-table3": "^0.6.5", "fs-extra": "^8.1.0", "lodash.defaults": "^4.2.0", - "tslib": "^2.5.0" + "natural-orderby": "^5.0.0", + "string-width": "^4.2.3", + "tslib": "^2.5.0", + "yaml": "^2.9.1" }, "devDependencies": { "@types/chai": "^4.2.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3bedd8b..31f02c4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,15 +23,27 @@ importers: '@oclif/plugin-version': specifier: 1.3.3 version: 1.3.3(@types/node@24.10.8)(typescript@5.9.3) + cli-table3: + specifier: ^0.6.5 + version: 0.6.5 fs-extra: specifier: ^8.1.0 version: 8.1.0 lodash.defaults: specifier: ^4.2.0 version: 4.2.0 + natural-orderby: + specifier: ^5.0.0 + version: 5.0.0 + string-width: + specifier: ^4.2.3 + version: 4.2.3 tslib: specifier: ^2.5.0 version: 2.8.1 + yaml: + specifier: ^2.9.1 + version: 2.9.1 devDependencies: '@types/chai': specifier: ^4.2.7 @@ -335,12 +347,10 @@ packages: '@npmcli/move-file@1.1.2': resolution: {integrity: sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==} engines: {node: '>=10'} - deprecated: This functionality has been moved to @npmcli/fs '@npmcli/move-file@2.0.1': resolution: {integrity: sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - deprecated: This functionality has been moved to @npmcli/fs '@npmcli/name-from-folder@1.0.1': resolution: {integrity: sha512-qq3oEfcLFwNfEYOQ8HLimRGKlD8WSeGEdtUa7hmzpR8Sa7haL1KVQrvgO6wqMjhWFFVjgtrh1gIxDz+P8sjUaA==} @@ -813,12 +823,10 @@ packages: are-we-there-yet@2.0.0: resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==} engines: {node: '>=10'} - deprecated: This package is no longer supported. are-we-there-yet@3.0.1: resolution: {integrity: sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - deprecated: This package is no longer supported. arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} @@ -1074,6 +1082,10 @@ packages: resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} engines: {node: '>=6'} + cli-table3@0.6.5: + resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} + engines: {node: 10.* || >= 12.*} + cli-table@0.3.11: resolution: {integrity: sha512-IqLQi4lO0nIB4tcdTpN4LCB9FI3uqrJZK7RC515EnhZ6qBaglkIgICb1wjeAqpdoOabm1+SuQtkXIPdYC93jhQ==} engines: {node: '>= 0.2.0'} @@ -1230,7 +1242,6 @@ packages: debuglog@1.0.1: resolution: {integrity: sha512-syBZ+rnAK3EgMsH2aYEOLUW7mZSY9Gb+0wUMCFsZvcmiz+HigA0LOcq/HoQqVuGG+EKykunc7QG2bzrponfaSw==} - deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. decamelize@4.0.0: resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==} @@ -1745,12 +1756,10 @@ packages: gauge@3.0.2: resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} engines: {node: '>=10'} - deprecated: This package is no longer supported. gauge@4.0.4: resolution: {integrity: sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - deprecated: This package is no longer supported. generator-function@2.0.1: resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} @@ -1815,12 +1824,10 @@ packages: glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported glob@8.1.0: resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} engines: {node: '>=12'} - deprecated: Glob versions prior to v9 are no longer supported globals@13.24.0: resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} @@ -1994,7 +2001,6 @@ packages: inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -2324,7 +2330,6 @@ packages: lodash.template@4.5.0: resolution: {integrity: sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==} - deprecated: This package is deprecated. Use https://socket.dev/npm/package/eta instead. lodash.templatesettings@4.2.0: resolution: {integrity: sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==} @@ -2647,12 +2652,10 @@ packages: npmlog@5.0.1: resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} - deprecated: This package is no longer supported. npmlog@6.0.2: resolution: {integrity: sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - deprecated: This package is no longer supported. number-is-nan@1.0.1: resolution: {integrity: sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==} @@ -2945,7 +2948,6 @@ packages: querystring@0.2.0: resolution: {integrity: sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==} engines: {node: '>=0.4.x'} - deprecated: The querystring API is considered Legacy. new code should use the URLSearchParams API instead. queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -2975,7 +2977,6 @@ packages: read-package-json@6.0.4: resolution: {integrity: sha512-AEtWXYfopBj2z5N5PbkAOeNHRPUg5q+Nen7QLxV8M2zJq1ym6/lCz3fYNTCXe19puu2d06jfHhrP7v/S2PtMMw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - deprecated: This package is no longer supported. Please use @npmcli/package-json instead. read-pkg-up@7.0.1: resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} @@ -2998,7 +2999,6 @@ packages: readdir-scoped-modules@1.1.0: resolution: {integrity: sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw==} - deprecated: This functionality has been moved to @npmcli/fs readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} @@ -3074,7 +3074,6 @@ packages: rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true run-async@2.4.1: @@ -3685,6 +3684,11 @@ packages: yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yaml@2.9.1: + resolution: {integrity: sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==} + engines: {node: '>= 14.6'} + hasBin: true + yargs-parser@20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} @@ -5053,6 +5057,10 @@ snapshots: cli-spinners@2.9.2: {} + cli-table3@0.6.5: + dependencies: + string-width: 4.2.3 + cli-table@0.3.11: dependencies: colors: 1.0.3 @@ -8089,6 +8097,8 @@ snapshots: yallist@4.0.0: {} + yaml@2.9.1: {} + yargs-parser@20.2.9: {} yargs-parser@21.1.1: {} diff --git a/src/commands/folders/create.ts b/src/commands/folders/create.ts index 2968648..434fee2 100644 --- a/src/commands/folders/create.ts +++ b/src/commands/folders/create.ts @@ -1,7 +1,7 @@ import type {CreateUserFolderBody} from '@hackmd/api' -import {Flags, ux} from '@oclif/core' +import {Flags} from '@oclif/core' import HackMDCommand from '../../command' import { @@ -11,6 +11,7 @@ import { folderName, parentFolderId, } from '../../flags' +import {table, tableFlags} from '../../table' export default class Create extends HackMDCommand { static description = 'Create a folder' @@ -27,7 +28,7 @@ a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d docs fc7a3d48-4a07-4cbf-bf4f-e65dd896e01c # icon: folderIcon, name: folderName, parentFolderId, - ...ux.table.flags(), + ...tableFlags(), } async run() { @@ -50,7 +51,7 @@ a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d docs fc7a3d48-4a07-4cbf-bf4f-e65dd896e01c # const APIClient = await this.getAPIClient() const folder = await APIClient.createFolder(payload) - ux.table( + table( [folder], Object.fromEntries([ [ diff --git a/src/commands/folders/index.ts b/src/commands/folders/index.ts index 870ebe1..6796972 100644 --- a/src/commands/folders/index.ts +++ b/src/commands/folders/index.ts @@ -1,7 +1,8 @@ -import {Flags, ux} from '@oclif/core' +import {Flags} from '@oclif/core' import HackMDCommand from '../../command' import {folderId} from '../../flags' +import {table, tableFlags} from '../../table' export default class IndexCommand extends HackMDCommand { static description = 'HackMD folders commands' @@ -14,7 +15,7 @@ ID Color Description Icon Name static flags = { folderId, help: Flags.help({char: 'h'}), - ...ux.table.flags(), + ...tableFlags(), } async run() { @@ -24,7 +25,7 @@ ID Color Description Icon Name const APIClient = await this.getAPIClient() const folders = flags.folderId ? [await APIClient.getFolder(flags.folderId)] : await APIClient.getFolderList() - ux.table( + table( folders, Object.fromEntries([ [ diff --git a/src/commands/history.ts b/src/commands/history.ts index 2c38a78..ac8da7c 100644 --- a/src/commands/history.ts +++ b/src/commands/history.ts @@ -1,6 +1,7 @@ -import {Flags, ux} from '@oclif/core' +import {Flags} from '@oclif/core' import HackMDCommand from '../command' +import {table, tableFlags} from '../table' export default class History extends HackMDCommand { static description = 'List user browse history' @@ -13,7 +14,7 @@ BnC6gN0_TfStV2KKmPPXeg Welcome to your team's workspace null C ] static flags = { help: Flags.help({char: 'h'}), - ...ux.table.flags(), + ...tableFlags(), } async run() { @@ -23,7 +24,7 @@ BnC6gN0_TfStV2KKmPPXeg Welcome to your team's workspace null C const APIClient = await this.getAPIClient() const history = await APIClient.getHistory() - ux.table(history, { + table(history, { id: { header: 'ID', }, diff --git a/src/commands/notes/create.ts b/src/commands/notes/create.ts index 132e427..6ae104b 100644 --- a/src/commands/notes/create.ts +++ b/src/commands/notes/create.ts @@ -4,7 +4,7 @@ import type { NotePermissionRole, } from '@hackmd/api' -import {Flags, ux} from '@oclif/core' +import {Flags} from '@oclif/core' import * as fs from 'node:fs' import HackMDCommand from '../../command' @@ -18,6 +18,7 @@ import { parentFolderId, } from '../../flags' import {openEditor} from '../../open-editor' +import {table, tableFlags} from '../../table' import {safeStdinRead, temporaryMD} from '../../utils' export default class CreateCommand extends HackMDCommand { @@ -51,7 +52,7 @@ raUuSTetT5uQbqQfLnz9lA A new note gvfz2UB5THiKABQJQnLs6Q n tags: noteTags, title: noteTitle, writePermission: notePermission(), - ...ux.table.flags(), + ...tableFlags(), } async run() { @@ -86,7 +87,7 @@ raUuSTetT5uQbqQfLnz9lA A new note gvfz2UB5THiKABQJQnLs6Q n const APIClient = await this.getAPIClient() const note = await APIClient.createNote(options as CreateNoteOptions) - ux.table( + table( [note], { id: { diff --git a/src/commands/notes/index.ts b/src/commands/notes/index.ts index d42cc1f..be2187e 100644 --- a/src/commands/notes/index.ts +++ b/src/commands/notes/index.ts @@ -1,7 +1,8 @@ -import {Flags, ux} from '@oclif/core' +import {Flags} from '@oclif/core' import HackMDCommand from '../../command' import {noteId} from '../../flags' +import {table, tableFlags} from '../../table' export default class IndexCommand extends HackMDCommand { static description = 'HackMD notes commands' @@ -14,7 +15,7 @@ raUuSTetT5uQbqQfLnz9lA CLI test note gvfz2UB5THiKABQJQnLs6Q n static flags = { help: Flags.help({char: 'h'}), noteId, - ...ux.table.flags(), + ...tableFlags(), } async run() { @@ -24,7 +25,7 @@ raUuSTetT5uQbqQfLnz9lA CLI test note gvfz2UB5THiKABQJQnLs6Q n const APIClient = await this.getAPIClient() const notes = flags.noteId ? [await APIClient.getNote(flags.noteId)] : await APIClient.getNoteList() - ux.table(notes, { + table(notes, { id: { header: 'ID', }, diff --git a/src/commands/team-folders/create.ts b/src/commands/team-folders/create.ts index bf7cb24..de8df9f 100644 --- a/src/commands/team-folders/create.ts +++ b/src/commands/team-folders/create.ts @@ -1,6 +1,6 @@ import type {CreateTeamFolderBody} from '@hackmd/api' -import {Flags, ux} from '@oclif/core' +import {Flags} from '@oclif/core' import HackMDCommand from '../../command' import { @@ -11,6 +11,7 @@ import { parentFolderId, teamPath, } from '../../flags' +import {table, tableFlags} from '../../table' export default class Create extends HackMDCommand { static description = 'Create a team folder' @@ -28,7 +29,7 @@ a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d team-docs fc7a3d48-4a07-4cbf-bf4f-e65dd896e name: folderName, parentFolderId, teamPath, - ...ux.table.flags(), + ...tableFlags(), } async run() { @@ -55,7 +56,7 @@ a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d team-docs fc7a3d48-4a07-4cbf-bf4f-e65dd896e const APIClient = await this.getAPIClient() const folder = await APIClient.createTeamFolder(teamPath, payload) - ux.table( + table( [folder], Object.fromEntries([ [ diff --git a/src/commands/team-folders/index.ts b/src/commands/team-folders/index.ts index 10f225c..6c85af7 100644 --- a/src/commands/team-folders/index.ts +++ b/src/commands/team-folders/index.ts @@ -1,7 +1,8 @@ -import {Flags, ux} from '@oclif/core' +import {Flags} from '@oclif/core' import HackMDCommand from '../../command' import {folderId, teamPath} from '../../flags' +import {table, tableFlags} from '../../table' export default class IndexCommand extends HackMDCommand { static description = 'HackMD team folders commands' @@ -15,7 +16,7 @@ ID Color Description Icon Name Pa folderId, help: Flags.help({char: 'h'}), teamPath, - ...ux.table.flags(), + ...tableFlags(), } async run() { @@ -29,7 +30,7 @@ ID Color Description Icon Name Pa const APIClient = await this.getAPIClient() const folders = flags.folderId ? [await APIClient.getTeamFolder(flags.teamPath, flags.folderId)] : await APIClient.getTeamFolderList(flags.teamPath) - ux.table( + table( folders, Object.fromEntries([ [ diff --git a/src/commands/team-notes/create.ts b/src/commands/team-notes/create.ts index db4a0af..539c9aa 100644 --- a/src/commands/team-notes/create.ts +++ b/src/commands/team-notes/create.ts @@ -1,6 +1,6 @@ import type {CommentPermissionType, CreateNoteOptions, NotePermissionRole} from '@hackmd/api' -import {Flags, ux} from '@oclif/core' +import {Flags} from '@oclif/core' import fs from 'node:fs' import HackMDCommand from '../../command' @@ -8,6 +8,7 @@ import { commentPermission, editor, noteContent, notePermission, noteTags, noteTitle, parentFolderId, teamPath, } from '../../flags' import {openEditor} from '../../open-editor' +import {table, tableFlags} from '../../table' import {safeStdinRead, temporaryMD} from '../../utils' export default class Create extends HackMDCommand { @@ -42,7 +43,7 @@ raUuSTetT5uQbqQfLnz9lA A new note gvfz2UB5THiKABQJQnLs6Q n teamPath, title: noteTitle, writePermission: notePermission(), - ...ux.table.flags(), + ...tableFlags(), } async run() { @@ -82,7 +83,7 @@ raUuSTetT5uQbqQfLnz9lA A new note gvfz2UB5THiKABQJQnLs6Q n const APIClient = await this.getAPIClient() const note = await APIClient.createTeamNote(teamPath, options as CreateNoteOptions) - ux.table([note], { + table([note], { id: { header: 'ID', }, diff --git a/src/commands/team-notes/index.ts b/src/commands/team-notes/index.ts index fb8e7c9..9b2de2d 100644 --- a/src/commands/team-notes/index.ts +++ b/src/commands/team-notes/index.ts @@ -1,7 +1,8 @@ -import {Flags, ux} from '@oclif/core' +import {Flags} from '@oclif/core' import HackMDCommand from '../../command' import {teamPath} from '../../flags' +import {table, tableFlags} from '../../table' export default class IndexCommand extends HackMDCommand { static description = 'HackMD team-notes commands' @@ -15,7 +16,7 @@ BnC6gN0_TfStV2KKmPPXeg Welcome to your team's workspace null CLI-test`, static flags = { help: Flags.help({char: 'h'}), teamPath, - ...ux.table.flags(), + ...tableFlags(), } async run() { @@ -29,7 +30,7 @@ BnC6gN0_TfStV2KKmPPXeg Welcome to your team's workspace null CLI-test`, const APIClient = await this.getAPIClient() const notes = await APIClient.getTeamNotes(flags.teamPath) - ux.table(notes, { + table(notes, { id: { header: 'ID', }, diff --git a/src/commands/teams.ts b/src/commands/teams.ts index cac30f6..bd7d5e2 100644 --- a/src/commands/teams.ts +++ b/src/commands/teams.ts @@ -1,6 +1,7 @@ -import {Flags, ux} from '@oclif/core' +import {Flags} from '@oclif/core' import HackMDCommand from '../command' +import {table, tableFlags} from '../table' export default class Teams extends HackMDCommand { static description = 'List teams' @@ -12,7 +13,7 @@ f76308a6-d77a-41f6-86d0-8ada426a6fb4 CLI test team CLI-test 82f7f3d9-4079-4c78-8 ] static flags = { help: Flags.help({char: 'h'}), - ...ux.table.flags(), + ...tableFlags(), } async run() { @@ -22,7 +23,7 @@ f76308a6-d77a-41f6-86d0-8ada426a6fb4 CLI test team CLI-test 82f7f3d9-4079-4c78-8 const APIClient = await this.getAPIClient() const notes = await APIClient.getTeams() - ux.table(notes, { + table(notes, { id: { header: 'ID', }, diff --git a/src/commands/whoami.ts b/src/commands/whoami.ts index c998dcb..8a1f014 100644 --- a/src/commands/whoami.ts +++ b/src/commands/whoami.ts @@ -1,6 +1,7 @@ -import {Flags, ux} from '@oclif/core' +import {Flags} from '@oclif/core' import HackMDCommand from '../command' +import {table, tableFlags} from '../table' export default class Whoami extends HackMDCommand { static description = 'Show current user information' @@ -12,7 +13,7 @@ ID Name Email User path ] static flags = { help: Flags.help({char: 'h'}), - ...ux.table.flags(), + ...tableFlags(), } async run() { @@ -22,7 +23,7 @@ ID Name Email User path const APIClient = await this.getAPIClient() const user = await APIClient.getMe() - ux.table([user], { + table([user], { email: {}, id: { header: 'ID', diff --git a/src/table.ts b/src/table.ts new file mode 100644 index 0000000..ce5f3fe --- /dev/null +++ b/src/table.ts @@ -0,0 +1,143 @@ +import {Flags} from '@oclif/core' +import Table from 'cli-table3' +import {orderBy} from 'natural-orderby' +import {inspect} from 'node:util' +import stringWidth from 'string-width' +import {stringify} from 'yaml' + +interface Column { + extended?: boolean; + get?: (row: T) => unknown; + header?: string; + minWidth?: number; +} + +interface Options { + columns?: string; + csv?: boolean; + extended?: boolean; + filter?: string; + 'no-header'?: boolean; + 'no-truncate'?: boolean; + output?: string; + printLine?: (line: string) => void; + sort?: string; +} + +export function table(data: T[], definitions: Record>, options: Options = {}): void { + const printLine = options.printLine ?? console.log + let columns = Object.entries(definitions).map(([key, column]) => ({ + ...column, + header: column.header ?? key.replaceAll('_', ' ').replace(/^./, char => char.toUpperCase()), + key, + })) + let rows = data.map(row => Object.fromEntries(columns.map(column => { + const value = column.get ? column.get(row) : (row as Record)[column.key] ?? '' + return [column.key, typeof value === 'string' ? value : inspect(value, {breakLength: Number.POSITIVE_INFINITY})] + }))) + + const findColumn = (header: string) => columns.find(column => column.header.toLowerCase() === header.toLowerCase()) + if (options.filter) { + const separator = options.filter.indexOf('=') + const header = options.filter.slice(0, separator) + const negate = header.startsWith('-') + const column = findColumn(negate ? header.slice(1) : header) + if (separator === -1 || !column || !options.filter.slice(separator + 1)) { + throw new Error('Filter flag has an invalid value') + } + + const pattern = new RegExp(options.filter.slice(separator + 1)) + rows = rows.filter(row => pattern.test(row[column.key]) !== negate) + } + + if (options.sort) { + const sorters = options.sort.split(',').map(header => ({ + column: findColumn(header.replace(/^-/, '')), + order: header.startsWith('-') ? 'desc' as const : 'asc' as const, + })).filter(sorter => sorter.column) + rows = orderBy(rows, sorters.map(sorter => sorter.column!.key), sorters.map(sorter => sorter.order)) + } + + columns = options.columns + ? [...new Set(options.columns.split(','))].flatMap(header => findColumn(header) ?? []) + : columns.filter(column => options.extended || !column.extended) + + const output = options.csv ? 'csv' : options.output + if (output === 'csv') { + const escape = (value: string) => /[",\r\n]/.test(value) ? `"${value.replaceAll('"', '""')}"` : value + if (!options['no-header']) printLine(columns.map(column => escape(column.header)).join(',')) + for (const row of rows) printLine(columns.map(column => escape(row[column.key])).join(',')) + return + } + + if (output === 'json' || output === 'yaml') { + const selected = rows.map(row => Object.fromEntries(columns.map(column => [column.key, row[column.key]]))) + printLine(output === 'json' ? JSON.stringify(selected, undefined, 2) : stringify(selected).trimEnd()) + return + } + + renderTable(rows, columns, options) +} + +function renderTable( + rows: Record[], + columns: Array<{header: string; key: string; minWidth?: number}>, + options: Options, +): void { + const printLine = options.printLine ?? console.log + if (columns.length === 0) return + const widths = columns.map(column => { + let width = Math.max(column.minWidth ?? 0, stringWidth(column.header)) + for (const row of rows) { + for (const line of row[column.key].split('\n')) width = Math.max(width, stringWidth(line)) + } + + return width + 1 + }) + if (process.stdout.isTTY && !options['no-truncate']) { + const available = process.stdout.columns - columns.length - 1 + const minimum = columns.map(column => Math.max(column.minWidth ?? 0, stringWidth(column.header) + 1)) + let total = 0 + for (const width of widths) total += width + while (total > available) { + let widest = 0 + for (let index = 1; index < widths.length; index++) { + if (widths[index] - minimum[index] > widths[widest] - minimum[widest]) widest = index + } + + if (widths[widest] <= minimum[widest]) break + widths[widest]-- + total-- + } + } + + const rendered = new Table({ + chars: { + bottom: '', 'bottom-left': '', 'bottom-mid': '', 'bottom-right': '', + left: '', 'left-mid': '', mid: '─', 'mid-mid': ' ', middle: ' ', right: '', 'right-mid': '', + top: '', 'top-left': '', 'top-mid': '', 'top-right': '', + }, + colWidths: widths, + head: options['no-header'] ? [] : columns.map(column => column.header), + style: { + border: [], compact: true, head: [], 'padding-left': 0, 'padding-right': 1, + }, + }) + for (const row of rows) rendered.push(columns.map(column => row[column.key])) + for (const line of rendered.toString().split('\n')) printLine(line) +} + +export const tableFlags = () => ({ + columns: Flags.string({description: 'only show provided columns (comma-separated)', exclusive: ['extended']}), + csv: Flags.boolean({description: 'output is csv format [alias: --output=csv]', exclusive: ['no-truncate']}), + extended: Flags.boolean({char: 'x', description: 'show extra columns', exclusive: ['columns']}), + filter: Flags.string({description: 'filter property by partial string matching, ex: name=foo'}), + 'no-header': Flags.boolean({description: 'hide table header from output', exclusive: ['csv']}), + 'no-truncate': Flags.boolean({description: 'do not truncate output to fit screen', exclusive: ['csv']}), + output: Flags.string({ + description: 'output in a more machine friendly format', + exclusive: ['no-truncate', 'csv'], + options: ['csv', 'json', 'yaml'], + }), + sort: Flags.string({description: 'property to sort by (prepend \'-\' for descending)'}), +}) diff --git a/test/table.test.ts b/test/table.test.ts new file mode 100644 index 0000000..f24810a --- /dev/null +++ b/test/table.test.ts @@ -0,0 +1,120 @@ +import {Parser} from '@oclif/core' +import {expect} from 'chai' +import stringWidth from 'string-width' +import {parse} from 'yaml' + +import {table, tableFlags} from '../src/table' + +const rows = [ + { + id: '10', note: 'hello, "world"\n第二行', tags: ['one', 'two'], title: 'Note 10', + }, + { + id: '2', note: '', tags: [], title: 'Note 2', + }, + { + id: '1', note: null, tags: ['one'], title: '中文😀', + }, +] +const columns = { + id: {header: 'ID'}, + note: {extended: true}, + tags: {get: (row: typeof rows[number]) => row.tags.join(', ')}, + title: {header: 'Title'}, +} + +function render(options: Parameters[2] = {}): string { + const lines: string[] = [] + table(rows, columns, {...options, printLine: line => lines.push(line)}) + return lines.join('\n') +} + +describe('Table compatibility', () => { + it('keeps selected columns, their order, and natural multi-column sorting', () => { + const output = render({columns: 'title,ID,title', output: 'json', sort: 'id'}) + expect(JSON.parse(output)).to.deep.equal([ + {id: '1', title: '中文😀'}, + {id: '2', title: 'Note 2'}, + {id: '10', title: 'Note 10'}, + ]) + expect(Object.keys(JSON.parse(output)[0])).to.deep.equal(['title', 'id']) + expect(JSON.parse(render({output: 'json', sort: '-id'})).map((row: {id: string}) => row.id)) + .to.deep.equal(['10', '2', '1']) + const lines: string[] = [] + table([{group: 'B', id: '1'}, {group: 'A', id: '2'}, {group: 'A', id: '10'}], { + group: {}, id: {}, + }, {output: 'json', printLine: line => lines.push(line), sort: 'group,-id'}) + expect(JSON.parse(lines[0])).to.deep.equal([ + {group: 'A', id: '10'}, {group: 'A', id: '2'}, {group: 'B', id: '1'}, + ]) + }) + + it('supports positive and negative regex filters and reports invalid filters', () => { + expect(JSON.parse(render({filter: 'Title=^Note', output: 'json'}))).to.have.length(2) + expect(JSON.parse(render({filter: '-Title=^Note', output: 'json'}))).to.have.length(1) + expect(() => render({filter: 'missing=value'})).to.throw('Filter flag has an invalid value') + expect(() => render({filter: 'title=['})).to.throw() + }) + + it('keeps string values and custom getters in JSON and YAML', () => { + const options = {extended: true} + const json = JSON.parse(render({...options, output: 'json'})) + expect(parse(render({...options, output: 'yaml'}))).to.deep.equal(json) + expect(json[0].tags).to.equal('one, two') + expect(json[2].note).to.equal('') + }) + + it('escapes CSV quotes, commas, and newlines and supports --csv', () => { + const output = render({columns: 'id,note', output: 'csv'}) + expect(output).to.equal('ID,Note\n10,"hello, ""world""\n第二行"\n2,\n1,') + expect(render({columns: 'id,note', csv: true})).to.equal(output) + expect(render({columns: 'id', 'no-header': true, output: 'csv'})).to.equal('10\n2\n1') + }) + + it('renders Unicode, multiline cells, and headerless tables', () => { + expect(render()).to.include('中文😀').and.include('ID').and.not.include('hello,') + expect(render({extended: true})).to.include('第二行') + expect(render({'no-header': true})).to.not.include('Title') + const lines: string[] = [] + table([], columns, {output: 'json', printLine: line => lines.push(line)}) + expect(lines).to.deep.equal(['[]']) + expect(() => table([], {}, {printLine() {}})).to.not.throw() + }) + + it('truncates only TTY output and honors --no-truncate', () => { + const tty = Object.getOwnPropertyDescriptor(process.stdout, 'isTTY') + const width = Object.getOwnPropertyDescriptor(process.stdout, 'columns') + try { + Object.defineProperty(process.stdout, 'isTTY', {configurable: true, value: true}) + Object.defineProperty(process.stdout, 'columns', {configurable: true, value: 24}) + const lines: string[] = [] + const data = [{title: '中文😀'.repeat(20)}] + table(data, {title: {}}, {printLine: line => lines.push(line)}) + expect(lines.every(line => stringWidth(line) <= 24)).to.equal(true) + expect(lines.join('\n')).to.include('…') + lines.length = 0 + table(data, {title: {}}, {'no-truncate': true, printLine: line => lines.push(line)}) + expect(lines.join('\n')).to.include(data[0].title) + } finally { + if (tty) Object.defineProperty(process.stdout, 'isTTY', tty) + else Reflect.deleteProperty(process.stdout, 'isTTY') + if (width) Object.defineProperty(process.stdout, 'columns', width) + else Reflect.deleteProperty(process.stdout, 'columns') + } + }) + + it('preserves table flags and exclusivity without sharing mutable definitions', async () => { + const flags = tableFlags() + expect(tableFlags().columns).to.not.equal(flags.columns) + const result = await Parser.parse(['-x', '--sort=-ID', '--filter=Title=Note'], {flags}) + expect(result.flags).to.include({extended: true, filter: 'Title=Note', sort: '-ID'}) + await Promise.all([['--columns=id', '-x'], ['--csv', '--output=json'], ['--csv', '--no-header']].map(async argv => { + try { + await Parser.parse(argv, {flags: tableFlags()}) + expect.fail(`Expected conflicting flags to fail: ${argv.join(' ')}`) + } catch (error) { + expect((error as Error).message).to.match(/cannot|exclusive/i) + } + })) + }) +}) diff --git a/tsconfig.json b/tsconfig.json index ab53f7f..c4ac9da 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,7 @@ "outDir": "lib", "rootDir": "src", "strict": true, - "target": "es2017", + "target": "es2022", "esModuleInterop": true, "skipLibCheck": true }, From 744920ce92b2d0d785752e697fbe1882418450b3 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Thu, 29 Oct 2026 01:23:26 +0800 Subject: [PATCH 3/6] refactor: upgrade oclif and preserve CLI behavior --- README.md | 492 +-- bin/dev | 8 +- bin/run | 4 +- package.json | 15 +- pnpm-lock.yaml | 4638 +++++++-------------------- src/autocomplete.ts | 81 + src/command.ts | 20 +- src/commands/autocomplete/index.ts | 42 + src/commands/autocomplete/script.ts | 22 + test/autocomplete.test.ts | 30 + test/prompt-cancel.test.ts | 32 + 11 files changed, 1660 insertions(+), 3724 deletions(-) create mode 100644 src/autocomplete.ts create mode 100644 src/commands/autocomplete/index.ts create mode 100644 src/commands/autocomplete/script.ts create mode 100644 test/autocomplete.test.ts create mode 100644 test/prompt-cancel.test.ts diff --git a/README.md b/README.md index 04aa67e..195dfd7 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ $ npm install -g @hackmd/hackmd-cli $ hackmd-cli COMMAND running command... $ hackmd-cli (--version|-v) -@hackmd/hackmd-cli/2.5.1 darwin-arm64 node-v26.0.0 +@hackmd/hackmd-cli/2.5.1 darwin-arm64 node-v24.22.0 $ hackmd-cli --help [COMMAND] USAGE $ hackmd-cli COMMAND @@ -39,6 +39,12 @@ USAGE ``` +### Shell completion + +Run `hackmd-cli autocomplete bash`, `hackmd-cli autocomplete zsh`, or `hackmd-cli autocomplete fish` and follow the printed setup instructions. + +Fish completion is generated in the CLI cache. Its setup instructions copy the generated file into your user completion directory; no system directory or `pkg-config` is required. Run the setup again after upgrading the CLI. + ## Configuration ### Set access token @@ -51,7 +57,7 @@ Set the access token by `login` command. By doing so, the access token will be s ```sh-session $ hackmd-cli login -Enter your access token: My_ACCESS_TOKEN +Enter your access token: [hidden] ``` Or you don't want to keep the credentials in one file, you can set it through environment variable: @@ -107,7 +113,7 @@ All available configurations are listed in the table below. * [`hackmd-cli folders delete`](#hackmd-cli-folders-delete) * [`hackmd-cli folders order`](#hackmd-cli-folders-order) * [`hackmd-cli folders update`](#hackmd-cli-folders-update) -* [`hackmd-cli help [COMMANDS]`](#hackmd-cli-help-commands) +* [`hackmd-cli help [COMMAND]`](#hackmd-cli-help-command) * [`hackmd-cli history`](#hackmd-cli-history) * [`hackmd-cli login`](#hackmd-cli-login) * [`hackmd-cli logout`](#hackmd-cli-logout) @@ -130,20 +136,20 @@ All available configurations are listed in the table below. ## `hackmd-cli autocomplete [SHELL]` -display autocomplete installation instructions +Display autocomplete installation instructions ``` USAGE $ hackmd-cli autocomplete [SHELL] [-r] ARGUMENTS - SHELL shell type + [SHELL] (bash|fish|zsh|powershell) Shell type FLAGS -r, --refresh-cache Refresh cache (ignores displaying instructions) DESCRIPTION - display autocomplete installation instructions + Display autocomplete installation instructions EXAMPLES $ hackmd-cli autocomplete @@ -157,7 +163,7 @@ EXAMPLES $ hackmd-cli autocomplete --refresh-cache ``` -_See code: [@hackmd/oclif-plugin-autocomplete](https://github.com/oclif/plugin-autocomplete/blob/v2.1.9-fish/src/commands/autocomplete/index.ts)_ +_See code: [src/commands/autocomplete/index.ts](https://github.com/hackmdio/hackmd-cli/blob/v2.5.1/src/commands/autocomplete/index.ts)_ ## `hackmd-cli export` @@ -168,8 +174,8 @@ USAGE $ hackmd-cli export [-h] [--noteId ] FLAGS - -h, --help Show CLI help. - --noteId= HackMD note id + -h, --help Show CLI help. + --noteId= HackMD note id DESCRIPTION Export note content @@ -187,21 +193,21 @@ HackMD folders commands ``` USAGE - $ hackmd-cli folders [--folderId ] [-h] [--columns | -x] [--sort ] [--filter ] - [--output csv|json|yaml | | [--csv | --no-truncate]] [--no-header | ] + $ hackmd-cli folders [--folderId ] [-h] [--columns | -x] [--filter ] [--no-header | + [--csv | --no-truncate]] [--output csv|json|yaml | | ] [--sort ] FLAGS - -h, --help Show CLI help. - -x, --extended show extra columns - --columns= only show provided columns (comma-separated) - --csv output is csv format [alias: --output=csv] - --filter= filter property by partial string matching, ex: name=foo - --folderId= HackMD folder id - --no-header hide table header from output - --no-truncate do not truncate output to fit screen - --output=