diff --git a/package-lock.json b/package-lock.json index 638ccbf..78b8b0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,23 @@ { - "name": "vexdo", - "version": "0.1.0", + "name": "@vexdo/cli", + "version": "0.1.4", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "vexdo", - "version": "0.1.0", + "name": "@vexdo/cli", + "version": "0.1.4", "dependencies": { "@anthropic-ai/sdk": "^0.39.0", "@types/js-yaml": "^4.0.9", + "@types/react": "^19.2.14", "commander": "^12.1.0", + "ink": "^6.8.0", + "ink-text-input": "^6.0.0", "js-yaml": "^4.1.1", "ora": "^8.1.1", "picocolors": "^1.1.1", + "react": "^19.2.4", "yaml": "^2.6.1" }, "bin": { @@ -31,6 +35,31 @@ "vitest": "^3.0.8" } }, + "node_modules/@alcalzone/ansi-tokenize": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.2.5.tgz", + "integrity": "sha512-3NX/MpTdroi0aKz134A6RC2Gb2iXVECN4QaAXnvCIxxIm3C3AVB1mkUe8NaaiyvOpDfsrqWhYtj+Q6a62RrTsw==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@alcalzone/ansi-tokenize/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/@anthropic-ai/sdk": { "version": "0.39.0", "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.39.0.tgz", @@ -1153,6 +1182,15 @@ "form-data": "^4.0.4" } }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.57.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.0.tgz", @@ -1572,6 +1610,21 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", @@ -1629,6 +1682,18 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, + "node_modules/auto-bind": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-5.0.1.tgz", + "integrity": "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -1761,6 +1826,18 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cli-cursor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", @@ -1788,6 +1865,50 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-truncate": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", + "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", + "license": "MIT", + "dependencies": { + "slice-ansi": "^8.0.0", + "string-width": "^8.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.0.tgz", + "integrity": "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/code-excerpt": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-4.0.0.tgz", + "integrity": "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==", + "license": "MIT", + "dependencies": { + "convert-to-spaces": "^2.0.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1853,6 +1974,15 @@ "node": "^14.18.0 || >=16.10.0" } }, + "node_modules/convert-to-spaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz", + "integrity": "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1868,6 +1998,12 @@ "node": ">= 8" } }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1945,6 +2081,18 @@ "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", "license": "MIT" }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -1997,6 +2145,16 @@ "node": ">= 0.4" } }, + "node_modules/es-toolkit": { + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.45.1.tgz", + "integrity": "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/esbuild": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", @@ -2736,6 +2894,18 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -2755,6 +2925,203 @@ "dev": true, "license": "ISC" }, + "node_modules/ink": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/ink/-/ink-6.8.0.tgz", + "integrity": "sha512-sbl1RdLOgkO9isK42WCZlJCFN9hb++sX9dsklOvfd1YQ3bQ2AiFu12Q6tFlr0HvEUvzraJntQCCpfEoUe9DSzA==", + "license": "MIT", + "dependencies": { + "@alcalzone/ansi-tokenize": "^0.2.4", + "ansi-escapes": "^7.3.0", + "ansi-styles": "^6.2.1", + "auto-bind": "^5.0.1", + "chalk": "^5.6.0", + "cli-boxes": "^3.0.0", + "cli-cursor": "^4.0.0", + "cli-truncate": "^5.1.1", + "code-excerpt": "^4.0.0", + "es-toolkit": "^1.39.10", + "indent-string": "^5.0.0", + "is-in-ci": "^2.0.0", + "patch-console": "^2.0.0", + "react-reconciler": "^0.33.0", + "scheduler": "^0.27.0", + "signal-exit": "^3.0.7", + "slice-ansi": "^8.0.0", + "stack-utils": "^2.0.6", + "string-width": "^8.1.1", + "terminal-size": "^4.0.1", + "type-fest": "^5.4.1", + "widest-line": "^6.0.0", + "wrap-ansi": "^9.0.0", + "ws": "^8.18.0", + "yoga-layout": "~3.2.1" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@types/react": ">=19.0.0", + "react": ">=19.0.0", + "react-devtools-core": ">=6.1.2" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react-devtools-core": { + "optional": true + } + } + }, + "node_modules/ink-text-input": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ink-text-input/-/ink-text-input-6.0.0.tgz", + "integrity": "sha512-Fw64n7Yha5deb1rHY137zHTAbSTNelUKuB5Kkk2HACXEtwIHBCf9OH2tP/LQ9fRYTl1F0dZgbW0zPnZk6FA9Lw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "type-fest": "^4.18.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "ink": ">=5", + "react": ">=18" + } + }, + "node_modules/ink-text-input/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ink-text-input/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ink/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ink/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ink/node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ink/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ink/node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ink/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/ink/node_modules/string-width": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.0.tgz", + "integrity": "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ink/node_modules/type-fest": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.4.4.tgz", + "integrity": "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw==", + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2765,6 +3132,21 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -2778,6 +3160,21 @@ "node": ">=0.10.0" } }, + "node_modules/is-in-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-in-ci/-/is-in-ci-2.0.0.tgz", + "integrity": "sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w==", + "license": "MIT", + "bin": { + "is-in-ci": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-interactive": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", @@ -3033,6 +3430,15 @@ "node": ">= 0.6" } }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/mimic-function": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", @@ -3291,6 +3697,15 @@ "node": ">=6" } }, + "node_modules/patch-console": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/patch-console/-/patch-console-2.0.0.tgz", + "integrity": "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3508,6 +3923,30 @@ ], "license": "MIT" }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-reconciler": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.33.0.tgz", + "integrity": "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^19.2.0" + } + }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -3645,6 +4084,12 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, "node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", @@ -3700,6 +4145,34 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/slice-ansi": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", + "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/source-map": { "version": "0.7.6", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", @@ -3720,6 +4193,27 @@ "node": ">=0.10.0" } }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -3850,6 +4344,30 @@ "node": ">=8" } }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/terminal-size": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/terminal-size/-/terminal-size-4.0.1.tgz", + "integrity": "sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -4332,6 +4850,37 @@ "node": ">=8" } }, + "node_modules/widest-line": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-6.0.0.tgz", + "integrity": "sha512-U89AsyEeAsyoF0zVJBkG9zBgekjgjK7yk9sje3F4IQpXBJ10TF6ByLlIfjMhcmHMJgHZI4KHt4rdNfktzxIAMA==", + "license": "MIT", + "dependencies": { + "string-width": "^8.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/widest-line/node_modules/string-width": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.0.tgz", + "integrity": "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -4342,6 +4891,35 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -4349,6 +4927,27 @@ "dev": true, "license": "ISC" }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yaml": { "version": "2.8.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", @@ -4376,6 +4975,12 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/yoga-layout": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", + "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==", + "license": "MIT" } } } diff --git a/package.json b/package.json index e1e2840..ce929a5 100644 --- a/package.json +++ b/package.json @@ -23,10 +23,14 @@ "dependencies": { "@anthropic-ai/sdk": "^0.39.0", "@types/js-yaml": "^4.0.9", + "@types/react": "^19.2.14", "commander": "^12.1.0", + "ink": "^6.8.0", + "ink-text-input": "^6.0.0", "js-yaml": "^4.1.1", "ora": "^8.1.1", "picocolors": "^1.1.1", + "react": "^19.2.4", "yaml": "^2.6.1" }, "devDependencies": { diff --git a/src/commands/board.tsx b/src/commands/board.tsx new file mode 100644 index 0000000..0fa74b2 --- /dev/null +++ b/src/commands/board.tsx @@ -0,0 +1,28 @@ +import type { Command } from 'commander'; +import React from 'react'; +import { render } from 'ink'; + +import { Board } from '../components/Board.js'; +import { findProjectRoot } from '../lib/config.js'; +import * as logger from '../lib/logger.js'; + +function fatalAndExit(message: string): never { + logger.fatal(message); + process.exit(1); +} + +export async function runBoard(): Promise { + const projectRoot = findProjectRoot(); + if (!projectRoot) { + fatalAndExit('Not inside a vexdo project.'); + } + + const instance = render(); + await instance.waitUntilExit(); +} + +export function registerBoardCommand(program: Command): void { + program.command('board').description('Open interactive task board').action(() => { + void runBoard(); + }); +} diff --git a/src/components/Board.tsx b/src/components/Board.tsx new file mode 100644 index 0000000..1eb73a5 --- /dev/null +++ b/src/components/Board.tsx @@ -0,0 +1,319 @@ +import { spawnSync } from 'node:child_process'; + +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { Box, Text, useApp, useInput } from 'ink'; + +import { loadBoardState, type BoardState, type TaskSummary } from '../lib/board.js'; + +interface BoardProps { + projectRoot: string; +} + +type ColumnKey = 'backlog' | 'in_progress' | 'review' | 'done'; + +interface ColumnDefinition { + key: ColumnKey; + title: string; + tasks: TaskSummary[]; +} + +function clamp(value: number, min: number, max: number): number { + return Math.max(min, Math.min(value, max)); +} + +function runExternal(command: string, args: string[]): { ok: boolean; message: string } { + const wasRaw = process.stdin.isTTY && process.stdin.isRaw; + if (process.stdin.isTTY && wasRaw) { + process.stdin.setRawMode(false); + } + + const result = spawnSync(command, args, { stdio: 'inherit' }); + + if (process.stdin.isTTY && wasRaw) { + process.stdin.setRawMode(true); + } + + if (result.error) { + return { + ok: false, + message: result.error.message, + }; + } + + return { + ok: result.status === 0, + message: result.status === 0 ? 'Command completed.' : `Command exited with status ${String(result.status)}.`, + }; +} + +function openInEditor(filePath: string): { ok: boolean; message: string } { + const editor = process.env.EDITOR; + if (!editor) { + return { ok: false, message: 'EDITOR is not set.' }; + } + + return runExternal(editor, [filePath]); +} + +function runPrimaryAction(column: ColumnKey, task: TaskSummary): { ok: boolean; message: string } { + if (task.blocked) { + return runExternal('vexdo', ['logs', task.id]); + } + + if (column === 'backlog') { + return runExternal('vexdo', ['start', task.path]); + } + + if (column === 'in_progress') { + return runExternal('vexdo', ['status']); + } + + if (column === 'review') { + return runExternal('vexdo', ['submit']); + } + + return openInEditor(task.path); +} + +function TaskCard({ task, selected }: { task: TaskSummary; selected: boolean }): React.JSX.Element { + const prefix = task.blocked ? '⚠ ' : ''; + return {`${selected ? '> ' : ' '}${prefix}${task.id}`}; +} + +function Column({ + title, + count, + tasks, + selectedRow, + active, +}: { + title: string; + count: number; + tasks: TaskSummary[]; + selectedRow: number; + active: boolean; +}): React.JSX.Element { + return ( + + {title} + {`(${String(count)})`} + + {tasks.length === 0 ? : null} + {tasks.map((task, index) => ( + + ))} + + + ); +} + +function StatusBar({ + task, + column, + message, + confirmAbort, +}: { + task: TaskSummary | undefined; + column: ColumnKey; + message: string | null; + confirmAbort: boolean; +}): React.JSX.Element { + if (!task) { + return ( + + No task selected. + [←/→/↑/↓] navigate [r] refresh [q] quit + + ); + } + + const primaryLabel = task.blocked + ? '[↵] logs' + : column === 'backlog' + ? '[↵] start' + : column === 'in_progress' + ? '[↵] status' + : column === 'review' + ? '[↵] submit' + : '[↵] edit'; + + return ( + + {`${task.id} · ${task.title}`} + + {`${primaryLabel} [e] edit [l] logs [a] abort [r] refresh [q] quit${confirmAbort ? ' Confirm abort: press [a] again' : ''}`} + + {message ? {message} : null} + + ); +} + +export function Board({ projectRoot }: BoardProps): React.JSX.Element { + const { exit } = useApp(); + const [state, setState] = useState(null); + const [loading, setLoading] = useState(true); + const [cursor, setCursor] = useState({ column: 0, row: 0 }); + const [message, setMessage] = useState(null); + const [confirmAbort, setConfirmAbort] = useState(false); + + const refresh = useCallback(async () => { + setLoading(true); + try { + const next = await loadBoardState(projectRoot); + setState(next); + setMessage(null); + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + setMessage(`Failed to load board: ${errorMessage}`); + } finally { + setLoading(false); + } + }, [projectRoot]); + + useEffect(() => { + void refresh(); + }, [refresh]); + + const columns = useMemo(() => { + if (!state) { + return [ + { key: 'backlog', title: 'BACKLOG', tasks: [] }, + { key: 'in_progress', title: 'IN PROGRESS', tasks: [] }, + { key: 'review', title: 'REVIEW', tasks: [] }, + { key: 'done', title: 'DONE', tasks: [] }, + ]; + } + + return [ + { key: 'backlog', title: 'BACKLOG', tasks: [...state.blocked, ...state.backlog] }, + { key: 'in_progress', title: 'IN PROGRESS', tasks: state.in_progress }, + { key: 'review', title: 'REVIEW', tasks: state.review }, + { key: 'done', title: 'DONE', tasks: state.done }, + ]; + }, [state]); + + const activeColumn = columns[cursor.column] ?? columns[0]; + const selectedTask = activeColumn.tasks.at(cursor.row); + + useEffect(() => { + const nextColumn = clamp(cursor.column, 0, columns.length - 1); + const maxRow = Math.max(0, (columns[nextColumn]?.tasks.length ?? 1) - 1); + const nextRow = clamp(cursor.row, 0, maxRow); + + if (nextColumn !== cursor.column || nextRow !== cursor.row) { + setCursor({ column: nextColumn, row: nextRow }); + } + }, [columns, cursor.column, cursor.row]); + + useInput((input, key) => { + if (input === 'q' || (key.ctrl && input === 'c')) { + exit(); + return; + } + + if (key.leftArrow) { + setConfirmAbort(false); + setCursor((prev) => { + const nextColumn = clamp(prev.column - 1, 0, columns.length - 1); + const maxRow = Math.max(0, columns[nextColumn].tasks.length - 1); + return { column: nextColumn, row: clamp(prev.row, 0, maxRow) }; + }); + return; + } + + if (key.rightArrow) { + setConfirmAbort(false); + setCursor((prev) => { + const nextColumn = clamp(prev.column + 1, 0, columns.length - 1); + const maxRow = Math.max(0, columns[nextColumn].tasks.length - 1); + return { column: nextColumn, row: clamp(prev.row, 0, maxRow) }; + }); + return; + } + + if (key.upArrow) { + setConfirmAbort(false); + setCursor((prev) => ({ ...prev, row: clamp(prev.row - 1, 0, Math.max(0, activeColumn.tasks.length - 1)) })); + return; + } + + if (key.downArrow) { + setConfirmAbort(false); + setCursor((prev) => ({ ...prev, row: clamp(prev.row + 1, 0, Math.max(0, activeColumn.tasks.length - 1)) })); + return; + } + + if (input === 'r') { + setConfirmAbort(false); + void refresh(); + return; + } + + if (key.return && selectedTask) { + const result = runPrimaryAction(activeColumn.key, selectedTask); + setMessage(result.message); + setConfirmAbort(false); + void refresh(); + return; + } + + if (input === 'e' && selectedTask) { + const result = openInEditor(selectedTask.path); + setMessage(result.message); + setConfirmAbort(false); + return; + } + + if (input === 'l' && selectedTask) { + const result = runExternal('vexdo', ['logs', selectedTask.id]); + setMessage(result.message); + setConfirmAbort(false); + return; + } + + if (input === 'a' && selectedTask) { + if (!confirmAbort) { + setConfirmAbort(true); + setMessage(`Confirm abort task '${selectedTask.id}' by pressing 'a' again.`); + return; + } + + const result = runExternal('vexdo', ['abort']); + setMessage(result.message); + setConfirmAbort(false); + void refresh(); + } + }); + + const terminalRows = typeof process.stdout.rows === 'number' ? process.stdout.rows : 24; + + return ( + + + vexdo board + [q] quit + + + {loading ? ( + + Loading tasks… + + ) : ( + + {columns.map((column, index) => ( + + ))} + + )} + + + + ); +} diff --git a/src/index.ts b/src/index.ts index 53103df..7fb15ca 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ import path from 'node:path'; import { Command } from 'commander'; import { registerAbortCommand } from './commands/abort.js'; +import { registerBoardCommand } from './commands/board.js'; import { registerFixCommand } from './commands/fix.js'; import { registerInitCommand } from './commands/init.js'; import { registerLogsCommand } from './commands/logs.js'; @@ -38,5 +39,6 @@ registerSubmitCommand(program); registerStatusCommand(program); registerAbortCommand(program); registerLogsCommand(program); +registerBoardCommand(program); program.parse(process.argv); diff --git a/src/lib/board.ts b/src/lib/board.ts new file mode 100644 index 0000000..2ec3471 --- /dev/null +++ b/src/lib/board.ts @@ -0,0 +1,98 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +import { parse } from 'yaml'; + +export interface TaskSummary { + id: string; + title: string; + path: string; + blocked?: boolean; +} + +export interface BoardState { + backlog: TaskSummary[]; + in_progress: TaskSummary[]; + review: TaskSummary[]; + done: TaskSummary[]; + blocked: TaskSummary[]; +} + +interface ParsedTask { + id?: string; + title?: string; +} + +function parseTaskYaml(raw: string): ParsedTask { + try { + const parsed: unknown = parse(raw); + if (typeof parsed !== 'object' || parsed === null) { + return {}; + } + const obj = parsed as Record; + return { + id: typeof obj.id === 'string' ? obj.id : undefined, + title: typeof obj.title === 'string' ? obj.title : undefined, + }; + } catch { + return {}; + } +} + +async function listTaskFiles(directory: string): Promise { + if (!fs.existsSync(directory)) { + return []; + } + + const entries = await fs.promises.readdir(directory, { withFileTypes: true }); + return entries + .filter((entry) => entry.isFile() && (entry.name.endsWith('.yml') || entry.name.endsWith('.yaml'))) + .map((entry) => path.join(directory, entry.name)); +} + +async function readTaskSummary(filePath: string): Promise { + const raw = await fs.promises.readFile(filePath, 'utf8'); + const parsed = parseTaskYaml(raw); + const fallbackId = path.basename(filePath).replace(/\.ya?ml$/i, ''); + + return { + id: parsed.id ?? fallbackId, + title: parsed.title ?? fallbackId, + path: filePath, + }; +} + +async function loadColumn(projectRoot: string, columnDir: string): Promise { + const directory = path.join(projectRoot, 'tasks', columnDir); + const files = await listTaskFiles(directory); + const tasks = await Promise.all(files.map((filePath) => readTaskSummary(filePath))); + return tasks.sort((a, b) => a.id.localeCompare(b.id)); +} + +export async function loadBoardState(projectRoot: string): Promise { + const [backlog, inProgress, review, blocked, doneFiles] = await Promise.all([ + loadColumn(projectRoot, 'backlog'), + loadColumn(projectRoot, 'in_progress'), + loadColumn(projectRoot, 'review'), + loadColumn(projectRoot, 'blocked'), + listTaskFiles(path.join(projectRoot, 'tasks', 'done')), + ]); + + const doneWithStats = await Promise.all( + doneFiles.map(async (filePath) => ({ + filePath, + stat: await fs.promises.stat(filePath), + })), + ); + + doneWithStats.sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs); + const done = await Promise.all(doneWithStats.slice(0, 20).map((item) => readTaskSummary(item.filePath))); + + return { + backlog, + in_progress: inProgress, + review, + done, + blocked: blocked.map((task) => ({ ...task, blocked: true })), + }; +} diff --git a/test/unit/board.test.ts b/test/unit/board.test.ts new file mode 100644 index 0000000..4db16c7 --- /dev/null +++ b/test/unit/board.test.ts @@ -0,0 +1,66 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +import { afterEach, describe, expect, it } from 'vitest'; + +import { loadBoardState } from '../../src/lib/board.js'; + +const tempDirs: string[] = []; + +function makeTempDir(): string { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'vexdo-board-')); + tempDirs.push(dir); + return dir; +} + +function writeTask(root: string, column: string, fileName: string, id: string, title: string): void { + const dir = path.join(root, 'tasks', column); + fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(path.join(dir, fileName), `id: ${id}\ntitle: ${title}\n`, 'utf8'); +} + +afterEach(() => { + for (const dir of tempDirs.splice(0)) { + fs.rmSync(dir, { recursive: true, force: true }); + } +}); + +describe('loadBoardState', () => { + it('loads all columns and marks blocked tasks', async () => { + const root = makeTempDir(); + writeTask(root, 'backlog', 'task-a.yml', 'task-a', 'Task A'); + writeTask(root, 'in_progress', 'task-b.yml', 'task-b', 'Task B'); + writeTask(root, 'review', 'task-c.yml', 'task-c', 'Task C'); + writeTask(root, 'done', 'task-d.yml', 'task-d', 'Task D'); + writeTask(root, 'blocked', 'task-e.yml', 'task-e', 'Task E'); + + const board = await loadBoardState(root); + + expect(board.backlog.map((item) => item.id)).toEqual(['task-a']); + expect(board.in_progress.map((item) => item.id)).toEqual(['task-b']); + expect(board.review.map((item) => item.id)).toEqual(['task-c']); + expect(board.done.map((item) => item.id)).toEqual(['task-d']); + expect(board.blocked[0]).toMatchObject({ id: 'task-e', blocked: true }); + }); + + it('limits done column to 20 most recent files', async () => { + const root = makeTempDir(); + const doneDir = path.join(root, 'tasks', 'done'); + fs.mkdirSync(doneDir, { recursive: true }); + + for (let index = 0; index < 25; index += 1) { + const id = `task-${String(index).padStart(2, '0')}`; + const filePath = path.join(doneDir, `${id}.yml`); + fs.writeFileSync(filePath, `id: ${id}\ntitle: ${id}\n`, 'utf8'); + const ts = Date.now() - (25 - index) * 1_000; + fs.utimesSync(filePath, ts / 1000, ts / 1000); + } + + const board = await loadBoardState(root); + + expect(board.done).toHaveLength(20); + expect(board.done[0]?.id).toBe('task-24'); + expect(board.done[19]?.id).toBe('task-05'); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index f5a9ca4..3b06ac3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,17 @@ "skipLibCheck": true, "rootDir": ".", "outDir": "dist", - "types": ["node", "vitest/globals"] + "types": [ + "node", + "vitest/globals" + ], + "jsx": "react-jsx" }, - "include": ["src", "test", "tests", "tsup.config.ts", "vitest.config.ts"] + "include": [ + "src", + "test", + "tests", + "tsup.config.ts", + "vitest.config.ts" + ] }