From 96463df8da1f50af8e8ce4d99c9d1029db34478b Mon Sep 17 00:00:00 2001 From: Haile <118998054+blackmammoth@users.noreply.github.com> Date: Wed, 15 Apr 2026 14:26:12 +0300 Subject: [PATCH] Feature/backend ts support andunification of auth settings on frontend (#654) * fix: remove project dependency from settings controller and onboarding * fix(settings): remove onClose prop from useSettingsController args * chore: tailwind classes order * refactor: move provider auth status management to custom hook * refactor: rename SessionProvider to LLMProvider * feat(frontend): support for @ alias based imports) * fix: replace init.sql with schema.js * fix: refactor database initialization to use schema.js for SQL statements * feat(server): add a real backend TypeScript build and enforce module boundaries The backend had started to grow beyond what the frontend-only tooling setup could support safely. We were still running server code directly from /server, linting mainly the client, and relying on path assumptions such as "../.." that only worked in the source layout. That created three problems: - backend alias imports were hard to resolve consistently in the editor, ESLint, and the runtime - server code had no enforced module boundary rules, so cross-module deep imports could bypass intended public entry points - building the backend into a separate output directory would break repo-level lookups for package.json, .env, dist, and public assets because those paths were derived from source-only relative assumptions This change makes the backend tooling explicit and runtime-safe. A dedicated backend TypeScript config now lives in server/tsconfig.json, with tsconfig.server.json reduced to a compatibility shim. This gives the language service and backend tooling a canonical project rooted in /server while still preserving top-level compatibility for any existing references. The backend alias mapping now resolves relative to /server, which avoids colliding with the frontend's "@/..." -> "src/*" mapping. The package scripts were updated so development runs through tsx with the backend tsconfig, build now produces a compiled backend in dist-server, and typecheck/lint cover both client and server. A new build-server.mjs script runs TypeScript and tsc-alias and cleans dist-server first, which prevents stale compiled files from shadowing current source files after refactors. To make the compiled backend behave the same as the source backend, runtime path resolution was centralized in server/utils/runtime-paths.js. Instead of assuming fixed relative paths from each module, server entry points now resolve the actual app root and server root at runtime. That keeps package.json, .env, dist, public, and default database paths stable whether code is executed from /server or from /dist-server/server. ESLint was expanded from a frontend-only setup into a backend-aware one. The backend now uses import resolution tied to the backend tsconfig so aliased imports resolve correctly in linting, import ordering matches the frontend style, and unused/duplicate imports are surfaced consistently. Most importantly, eslint-plugin-boundaries now enforces server module boundaries. Files under server/modules can no longer import another module's internals directly. Cross-module imports must go through that module's barrel file (index.ts/index.js). boundaries/no-unknown was also enabled so alias-resolution gaps cannot silently bypass the rule. Together, these changes make the backend buildable, keep runtime path resolution stable after compilation, align server tooling with the client where appropriate, and enforce a stricter modular architecture for server code. * fix: update package.json to include dist-server in files and remove tsconfig.server.json * refactor: remove build-server.mjs and inline its logic into package.json scripts * fix: update paths in package.json and bin.js to use dist-server directory * feat(eslint): add backend shared types and enforce compile-time contract for imports * fix(eslint): update shared types pattern --------- Co-authored-by: Haileyesus --- .gitignore | 3 +- eslint.config.js | 132 ++- package-lock.json | 846 +++++++++++++++++- package.json | 29 +- redirect-package/bin.js | 2 +- server/cli.js | 22 +- server/database/db.js | 83 +- server/database/init.sql | 99 -- server/database/schema.js | 102 +++ server/index.js | 24 +- server/load-env.js | 17 +- server/routes/commands.js | 10 +- server/tsconfig.json | 33 + server/utils/runtime-paths.js | 37 + .../chat/hooks/useChatComposerState.ts | 4 +- .../chat/hooks/useChatProviderState.ts | 6 +- .../chat/hooks/useChatRealtimeHandlers.ts | 4 +- .../chat/hooks/useChatSessionState.ts | 18 +- src/components/chat/types/types.ts | 4 +- src/components/chat/view/ChatInterface.tsx | 6 +- .../view/subcomponents/ChatMessagesPane.tsx | 6 +- .../ProviderSelectionEmptyState.tsx | 14 +- .../llm-logo-provider/SessionProviderLogo.tsx | 4 +- src/components/onboarding/view/Onboarding.tsx | 87 +- .../subcomponents/AgentConnectionCard.tsx | 5 +- .../subcomponents/AgentConnectionsStep.tsx | 7 +- src/components/onboarding/view/types.ts | 12 - src/components/onboarding/view/utils.ts | 19 - .../hooks/useProviderAuthStatus.ts | 109 +++ src/components/provider-auth/types.ts | 28 +- .../provider-auth/view/ProviderLoginModal.tsx | 35 +- .../settings/constants/constants.ts | 14 - .../settings/hooks/useSettingsController.ts | 113 +-- src/components/settings/types/types.ts | 12 +- src/components/settings/view/Settings.tsx | 31 +- .../agents-settings/AgentsSettingsTab.tsx | 39 +- .../view/tabs/agents-settings/types.ts | 11 +- .../sidebar/hooks/useSidebarController.ts | 4 +- src/components/sidebar/types/types.ts | 6 +- src/components/sidebar/view/Sidebar.tsx | 6 +- .../view/subcomponents/SidebarProjectItem.tsx | 6 +- .../view/subcomponents/SidebarProjectList.tsx | 6 +- .../subcomponents/SidebarProjectSessions.tsx | 6 +- .../view/subcomponents/SidebarSessionItem.tsx | 6 +- src/constants/config.ts | 14 +- src/stores/useSessionStore.ts | 12 +- src/types/app.ts | 4 +- tsconfig.json | 10 +- vite.config.js | 6 + 49 files changed, 1531 insertions(+), 582 deletions(-) delete mode 100644 server/database/init.sql create mode 100644 server/database/schema.js create mode 100644 server/tsconfig.json create mode 100644 server/utils/runtime-paths.js delete mode 100644 src/components/onboarding/view/types.ts create mode 100644 src/components/provider-auth/hooks/useProviderAuthStatus.ts diff --git a/.gitignore b/.gitignore index 35394555..e9d1b52a 100755 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ lerna-debug.log* # Build outputs dist/ +dist-server/ dist-ssr/ build/ out/ @@ -138,4 +139,4 @@ tasks/ !src/i18n/locales/de/tasks.json # Git worktrees -.worktrees/ \ No newline at end of file +.worktrees/ diff --git a/eslint.config.js b/eslint.config.js index 5645f2e8..3ef25d92 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -3,7 +3,9 @@ import tseslint from "typescript-eslint"; import react from "eslint-plugin-react"; import reactHooks from "eslint-plugin-react-hooks"; import reactRefresh from "eslint-plugin-react-refresh"; -import importX from "eslint-plugin-import-x"; +import { createNodeResolver, importX } from "eslint-plugin-import-x"; +import { createTypeScriptImportResolver } from "eslint-import-resolver-typescript"; +import boundaries from "eslint-plugin-boundaries"; import tailwindcss from "eslint-plugin-tailwindcss"; import unusedImports from "eslint-plugin-unused-imports"; import globals from "globals"; @@ -82,7 +84,7 @@ export default tseslint.config( "sibling", "index", ], - "newlines-between": "never", + "newlines-between": "always", }, ], @@ -98,5 +100,131 @@ export default tseslint.config( "no-control-regex": "off", "no-useless-escape": "off", }, + }, + { + files: ["server/**/*.{js,ts}"], // apply this block only to backend source files + ignores: ["server/**/*.d.ts"], // skip generated declaration files in backend linting + plugins: { + boundaries, // enforce backend architecture boundaries (module-to-module contracts) + "import-x": importX, // keep import hygiene rules (duplicates, unresolved paths, etc.) + "unused-imports": unusedImports, // remove dead imports/variables from backend files + }, + languageOptions: { + parser: tseslint.parser, // parse both JS and TS syntax in backend files + parserOptions: { + ecmaVersion: "latest", // support modern ECMAScript syntax in backend code + sourceType: "module", // treat backend files as ESM modules + }, + globals: { + ...globals.node, // expose Node.js globals such as process, Buffer, and __dirname equivalents + }, + }, + settings: { + "boundaries/include": ["server/**/*.{js,ts}"], // only analyze dependency boundaries inside backend files + "import/resolver": { + // boundaries resolves imports through eslint-module-utils, which reads the classic + // import/resolver setting instead of import-x/resolver-next. + typescript: { + project: ["server/tsconfig.json"], // resolve backend aliases using the canonical backend tsconfig + alwaysTryTypes: true, // keep normal TS package/type resolution working alongside aliases + }, + node: { + extensions: [".mjs", ".cjs", ".js", ".json", ".node", ".ts", ".tsx"], // preserve Node-style fallback resolution for plain files + }, + }, + "import-x/resolver-next": [ + // ESLint's import plugin does not read tsconfig path aliases on its own. + // This resolver teaches import-x how to understand the backend-only "@/*" + // mapping defined in server/tsconfig.json, which fixes false no-unresolved errors in editors. + createTypeScriptImportResolver({ + project: ["server/tsconfig.json"], // point the resolver at the canonical backend tsconfig instead of the frontend one + alwaysTryTypes: true, // keep standard TypeScript package resolution working while backend aliases are enabled + }), + // Keep Node-style resolution available for normal package imports and plain relative JS files. + // The TypeScript resolver handles aliases, while the Node resolver preserves the expected fallback behavior. + createNodeResolver({ + extensions: [".mjs", ".cjs", ".js", ".json", ".node", ".ts", ".tsx"], + }), + ], + "boundaries/elements": [ + { + type: "backend-shared-types", // shared backend type contract that modules may consume without creating runtime coupling + pattern: ["server/shared/types.{js,ts}"], // support the current shared types path + mode: "file", // treat the types file itself as the boundary element instead of the whole folder + }, + { + type: "backend-module", // logical element name used by boundaries rules below + pattern: "server/modules/*", // each direct folder in server/modules is treated as one module boundary + mode: "folder", // classify dependencies at folder-module level (not per individual file) + capture: ["moduleName"], // capture the module folder name for messages/debugging/template use + }, + ], + }, + rules: { + // --- Unused imports/vars (backend) --- + "unused-imports/no-unused-imports": "warn", // warn when imports are not used so they can be cleaned up + "unused-imports/no-unused-vars": "off", // keep backend signal focused on dead imports instead of local unused variables + + // --- Import hygiene (backend) --- + "import-x/no-duplicates": "warn", // prevent duplicate import lines from the same module + "import-x/order": [ + "warn", // keep backend import grouping/order consistent with the frontend config + { + groups: [ + "builtin", // Node built-ins such as fs, path, and url come first + "external", // third-party packages come after built-ins + "internal", // aliased internal imports such as @/... come next + "parent", // ../ imports come after aliased internal imports + "sibling", // ./foo imports come after parent imports + "index", // bare ./ imports stay last + ], + "newlines-between": "always", // require a blank line between import groups in backend files too + }, + ], + "import-x/no-unresolved": "error", // fail when an import path cannot be resolved + "import-x/no-useless-path-segments": "warn", // prefer cleaner paths (remove redundant ./ and ../ segments) + "import-x/no-absolute-path": "error", // disallow absolute filesystem imports in backend files + + // --- General safety/style (backend) --- + eqeqeq: ["warn", "always", { null: "ignore" }], // avoid accidental coercion while still allowing x == null checks + + // --- Architecture boundaries (backend modules) --- + "boundaries/dependencies": [ + "error", // treat architecture violations as lint errors + { + default: "allow", // allow normal imports unless a rule below explicitly disallows them + checkInternals: false, // do not apply these cross-module rules to imports inside the same module + rules: [ + { + from: { type: "backend-module" }, // modules may depend on the shared types contract only as erased type-only imports + to: { type: "backend-shared-types" }, + disallow: { + dependency: { kind: ["value", "typeof"] }, + }, // block runtime imports so shared types stay a compile-time contract instead of a hidden shared module + message: + "Backend modules may only use `import type` when importing from server/shared/types.ts (or server/types.ts).", + }, + { + to: { type: "backend-module" }, // when importing anything that belongs to another backend module + disallow: { to: { internalPath: "**" } }, // block all direct/deep imports into module internals by default + message: + "Cross-module imports must go through that module's barrel file (server/modules//index.ts or index.js).", // explicit error message for architecture violations + }, + { + to: { type: "backend-module" }, // same target scope as the disallow rule above + allow: { + to: { + internalPath: [ + "index", // allow extensionless barrel imports resolved as module root index + "index.{js,mjs,cjs,ts,tsx}", // allow explicit index.* barrel file imports + ], + }, + }, // re-allow only public module entry points (barrel files) + }, + ], + }, + ], + "boundaries/no-unknown": "error", // fail fast if boundaries cannot classify a dependency, which prevents silent rule bypasses + }, } ); diff --git a/package-lock.json b/package-lock.json index 21bee5b6..0905f759 100644 --- a/package-lock.json +++ b/package-lock.json @@ -69,7 +69,7 @@ "ws": "^8.14.2" }, "bin": { - "cloudcli": "server/cli.js" + "cloudcli": "dist-server/server/cli.js" }, "devDependencies": { "@commitlint/cli": "^20.4.3", @@ -84,6 +84,8 @@ "autoprefixer": "^10.4.16", "concurrently": "^8.2.2", "eslint": "^9.39.3", + "eslint-import-resolver-typescript": "^4.4.4", + "eslint-plugin-boundaries": "^6.0.2", "eslint-plugin-import-x": "^4.16.1", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.0.1", @@ -98,6 +100,8 @@ "release-it": "^19.0.5", "sharp": "^0.34.2", "tailwindcss": "^3.4.0", + "tsc-alias": "^1.8.16", + "tsx": "^4.21.0", "typescript": "^5.9.3", "typescript-eslint": "^8.56.1", "vite": "^7.0.4" @@ -443,6 +447,23 @@ "node": ">=6.9.0" } }, + "node_modules/@boundaries/elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@boundaries/elements/-/elements-2.0.1.tgz", + "integrity": "sha512-sAWO3D8PFP6pBXdxxW93SQi/KQqqhE2AAHo3AgWfdtJXwO6bfK6/wUN81XnOZk0qRC6vHzUEKhjwVD9dtDWvxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-import-resolver-node": "0.3.9", + "eslint-module-utils": "2.12.1", + "handlebars": "4.7.9", + "is-core-module": "2.16.1", + "micromatch": "4.0.8" + }, + "engines": { + "node": ">=18.18" + } + }, "node_modules/@codemirror/autocomplete": { "version": "6.18.6", "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz", @@ -4815,6 +4836,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/array.prototype.findlast": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", @@ -6673,6 +6704,19 @@ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "license": "Apache-2.0" }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", @@ -7217,6 +7261,112 @@ } } }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-4.4.4.tgz", + "integrity": "sha512-1iM2zeBvrYmUNTj2vSC/90JTHDth+dfOfiNKkxApWRsTJYNrc8rOdxxIf5vazX+BiAXTeOT0UvWpGI/7qIWQOw==", + "dev": true, + "license": "ISC", + "dependencies": { + "debug": "^4.4.1", + "eslint-import-context": "^0.1.8", + "get-tsconfig": "^4.10.1", + "is-bun-module": "^2.0.0", + "stable-hash-x": "^0.2.0", + "tinyglobby": "^0.2.14", + "unrs-resolver": "^1.7.11" + }, + "engines": { + "node": "^16.17.0 || >=18.6.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-boundaries": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-boundaries/-/eslint-plugin-boundaries-6.0.2.tgz", + "integrity": "sha512-wSHgiYeMEbziP91lH0UQ9oslgF2djG1x+LV9z/qO19ggMKZaCB8pKIGePHAY91eLF4EAgpsxQk8MRSFGRPfPzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@boundaries/elements": "2.0.1", + "chalk": "4.1.2", + "eslint-import-resolver-node": "0.3.9", + "eslint-module-utils": "2.12.1", + "handlebars": "4.7.9", + "micromatch": "4.0.8" + }, + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, "node_modules/eslint-plugin-import-x": { "version": "4.16.1", "resolved": "https://registry.npmjs.org/eslint-plugin-import-x/-/eslint-plugin-import-x-4.16.1.tgz", @@ -8519,6 +8669,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -8554,9 +8725,9 @@ } }, "node_modules/handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", "dev": true, "license": "MIT", "dependencies": { @@ -9446,6 +9617,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-bun-module/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -12026,6 +12220,20 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/mylas": { + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/mylas/-/mylas-2.1.14.tgz", + "integrity": "sha512-BzQguy9W9NJgoVn2mRWzbFrFWWztGCcng2QI9+41frfk+Athwgx3qhqhvStz7ExeUUu7Kzw427sNzHpEZNINog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/raouldeheer" + } + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -12989,6 +13197,16 @@ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -13051,6 +13269,19 @@ "pathe": "^2.0.3" } }, + "node_modules/plimit-lit": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/plimit-lit/-/plimit-lit-1.6.1.tgz", + "integrity": "sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "queue-lit": "^1.5.1" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -13412,6 +13643,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/queue-lit": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/queue-lit/-/queue-lit-1.5.2.tgz", + "integrity": "sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -15173,6 +15414,16 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/slice-ansi": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", @@ -16479,12 +16730,599 @@ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", "license": "Apache-2.0" }, + "node_modules/tsc-alias": { + "version": "1.8.16", + "resolved": "https://registry.npmjs.org/tsc-alias/-/tsc-alias-1.8.16.tgz", + "integrity": "sha512-QjCyu55NFyRSBAl6+MTFwplpFcnm2Pq01rR/uxfqJoLMm6X3O14KEGtaSDZpJYaE1bJBGDjD0eSuiIWPe2T58g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.3", + "commander": "^9.0.0", + "get-tsconfig": "^4.10.0", + "globby": "^11.0.4", + "mylas": "^2.1.9", + "normalize-path": "^3.0.0", + "plimit-lit": "^1.2.6" + }, + "bin": { + "tsc-alias": "dist/bin/index.js" + }, + "engines": { + "node": ">=16.20.2" + } + }, + "node_modules/tsc-alias/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/tsc-alias/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/tsc-alias/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tsc-alias/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", diff --git a/package.json b/package.json index 2a80d5a3..b4d2653f 100644 --- a/package.json +++ b/package.json @@ -3,14 +3,15 @@ "version": "1.29.2", "description": "A web-based UI for Claude Code CLI", "type": "module", - "main": "server/index.js", + "main": "dist-server/server/index.js", "bin": { - "cloudcli": "server/cli.js" + "cloudcli": "dist-server/server/cli.js" }, "files": [ "server/", "shared/", "dist/", + "dist-server/", "scripts/", "README.md" ], @@ -23,14 +24,19 @@ "url": "https://github.com/siteboon/claudecodeui/issues" }, "scripts": { - "dev": "concurrently --kill-others \"npm run server\" \"npm run client\"", - "server": "node server/index.js", + "dev": "concurrently --kill-others \"npm run server:dev\" \"npm run client\"", + "server": "node dist-server/server/index.js", + "server:dev": "tsx --tsconfig server/tsconfig.json server/index.js", + "server:dev-watch": "tsx watch --tsconfig server/tsconfig.json server/index.js", "client": "vite", - "build": "vite build", + "build": "npm run build:client && npm run build:server", + "build:client": "vite build", + "prebuild:server": "node -e \"require('node:fs').rmSync('dist-server', { recursive: true, force: true })\"", + "build:server": "tsc -p server/tsconfig.json && tsc-alias -p server/tsconfig.json", "preview": "vite preview", - "typecheck": "tsc --noEmit -p tsconfig.json", - "lint": "eslint src/", - "lint:fix": "eslint src/ --fix", + "typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p server/tsconfig.json", + "lint": "eslint src/ server/", + "lint:fix": "eslint src/ server/ --fix", "start": "npm run build && npm run server", "release": "./release.sh", "prepublishOnly": "npm run build", @@ -130,6 +136,8 @@ "autoprefixer": "^10.4.16", "concurrently": "^8.2.2", "eslint": "^9.39.3", + "eslint-import-resolver-typescript": "^4.4.4", + "eslint-plugin-boundaries": "^6.0.2", "eslint-plugin-import-x": "^4.16.1", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.0.1", @@ -144,11 +152,14 @@ "release-it": "^19.0.5", "sharp": "^0.34.2", "tailwindcss": "^3.4.0", + "tsc-alias": "^1.8.16", + "tsx": "^4.21.0", "typescript": "^5.9.3", "typescript-eslint": "^8.56.1", "vite": "^7.0.4" }, "lint-staged": { - "src/**/*.{ts,tsx,js,jsx}": "eslint" + "src/**/*.{ts,tsx,js,jsx}": "eslint", + "server/**/*.{js,ts}": "eslint" } } diff --git a/redirect-package/bin.js b/redirect-package/bin.js index 2f31c0c6..fc33eef1 100644 --- a/redirect-package/bin.js +++ b/redirect-package/bin.js @@ -1,2 +1,2 @@ #!/usr/bin/env node -import('@cloudcli-ai/cloudcli/server/cli.js'); +import('@cloudcli-ai/cloudcli/dist-server/server/cli.js'); diff --git a/server/cli.js b/server/cli.js index 3c0d1d4f..7551afc4 100755 --- a/server/cli.js +++ b/server/cli.js @@ -16,11 +16,12 @@ import fs from 'fs'; import path from 'path'; import os from 'os'; -import { fileURLToPath } from 'url'; -import { dirname } from 'path'; +import { findAppRoot, getModuleDir } from './utils/runtime-paths.js'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); +const __dirname = getModuleDir(import.meta.url); +// The CLI is compiled into dist-server/server, but it still needs to read the top-level +// package.json and .env file. Resolving the app root once keeps those lookups stable. +const APP_ROOT = findAppRoot(__dirname); // ANSI color codes for terminal output const colors = { @@ -50,13 +51,16 @@ const c = { }; // Load package.json for version info -const packageJsonPath = path.join(__dirname, '../package.json'); +const packageJsonPath = path.join(APP_ROOT, 'package.json'); const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); +// Match the runtime fallback in load-env.js so "cloudcli status" reports the same default +// database location that the backend will actually use when no DATABASE_PATH is configured. +const DEFAULT_DATABASE_PATH = path.join(os.homedir(), '.cloudcli', 'auth.db'); // Load environment variables from .env file if it exists function loadEnvFile() { try { - const envPath = path.join(__dirname, '../.env'); + const envPath = path.join(APP_ROOT, '.env'); const envFile = fs.readFileSync(envPath, 'utf8'); envFile.split('\n').forEach(line => { const trimmedLine = line.trim(); @@ -75,12 +79,12 @@ function loadEnvFile() { // Get the database path (same logic as db.js) function getDatabasePath() { loadEnvFile(); - return process.env.DATABASE_PATH || path.join(__dirname, 'database', 'auth.db'); + return process.env.DATABASE_PATH || DEFAULT_DATABASE_PATH; } // Get the installation directory function getInstallDir() { - return path.join(__dirname, '..'); + return APP_ROOT; } // Show status command @@ -124,7 +128,7 @@ function showStatus() { console.log(` Status: ${projectsExists ? c.ok('[OK] Exists') : c.warn('[WARN] Not found')}`); // Config file location - const envFilePath = path.join(__dirname, '../.env'); + const envFilePath = path.join(APP_ROOT, '.env'); const envExists = fs.existsSync(envFilePath); console.log(`\n${c.info('[INFO]')} Configuration File:`); console.log(` ${c.dim(envFilePath)}`); diff --git a/server/database/db.js b/server/database/db.js index 9ab0ad78..f43c894a 100644 --- a/server/database/db.js +++ b/server/database/db.js @@ -2,11 +2,21 @@ import Database from 'better-sqlite3'; import path from 'path'; import fs from 'fs'; import crypto from 'crypto'; -import { fileURLToPath } from 'url'; -import { dirname } from 'path'; +import { findAppRoot, getModuleDir } from '../utils/runtime-paths.js'; +import { + APP_CONFIG_TABLE_SQL, + USER_NOTIFICATION_PREFERENCES_TABLE_SQL, + VAPID_KEYS_TABLE_SQL, + PUSH_SUBSCRIPTIONS_TABLE_SQL, + SESSION_NAMES_TABLE_SQL, + SESSION_NAMES_LOOKUP_INDEX_SQL, + DATABASE_SCHEMA_SQL +} from './schema.js'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); +const __dirname = getModuleDir(import.meta.url); +// The compiled backend lives under dist-server/server/database, but the install root we log +// should still point at the project/app root. Resolving it here avoids build-layout drift. +const APP_ROOT = findAppRoot(__dirname); // ANSI color codes for terminal output const colors = { @@ -24,7 +34,6 @@ const c = { // Use DATABASE_PATH environment variable if set, otherwise use default location const DB_PATH = process.env.DATABASE_PATH || path.join(__dirname, 'auth.db'); -const INIT_SQL_PATH = path.join(__dirname, 'init.sql'); // Ensure database directory exists if custom path is provided if (process.env.DATABASE_PATH) { @@ -62,14 +71,10 @@ const db = new Database(DB_PATH); // app_config must exist before any other module imports (auth.js reads the JWT secret at load time). // runMigrations() also creates this table, but it runs too late for existing installations // where auth.js is imported before initializeDatabase() is called. -db.exec(`CREATE TABLE IF NOT EXISTS app_config ( - key TEXT PRIMARY KEY, - value TEXT NOT NULL, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP -)`); +db.exec(APP_CONFIG_TABLE_SQL); // Show app installation path prominently -const appInstallPath = path.join(__dirname, '../..'); +const appInstallPath = APP_ROOT; console.log(''); console.log(c.dim('═'.repeat(60))); console.log(`${c.info('[INFO]')} App Installation: ${c.bright(appInstallPath)}`); @@ -100,53 +105,12 @@ const runMigrations = () => { db.exec('ALTER TABLE users ADD COLUMN has_completed_onboarding BOOLEAN DEFAULT 0'); } - db.exec(` - CREATE TABLE IF NOT EXISTS user_notification_preferences ( - user_id INTEGER PRIMARY KEY, - preferences_json TEXT NOT NULL, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE - ) - `); - - db.exec(` - CREATE TABLE IF NOT EXISTS vapid_keys ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - public_key TEXT NOT NULL, - private_key TEXT NOT NULL, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP - ) - `); - - db.exec(` - CREATE TABLE IF NOT EXISTS push_subscriptions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL, - endpoint TEXT NOT NULL UNIQUE, - keys_p256dh TEXT NOT NULL, - keys_auth TEXT NOT NULL, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE - ) - `); - // Create app_config table if it doesn't exist (for existing installations) - db.exec(`CREATE TABLE IF NOT EXISTS app_config ( - key TEXT PRIMARY KEY, - value TEXT NOT NULL, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP - )`); - - // Create session_names table if it doesn't exist (for existing installations) - db.exec(`CREATE TABLE IF NOT EXISTS session_names ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - session_id TEXT NOT NULL, - provider TEXT NOT NULL DEFAULT 'claude', - custom_name TEXT NOT NULL, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, - UNIQUE(session_id, provider) - )`); - db.exec('CREATE INDEX IF NOT EXISTS idx_session_names_lookup ON session_names(session_id, provider)'); + db.exec(USER_NOTIFICATION_PREFERENCES_TABLE_SQL); + db.exec(VAPID_KEYS_TABLE_SQL); + db.exec(PUSH_SUBSCRIPTIONS_TABLE_SQL); + db.exec(APP_CONFIG_TABLE_SQL); + db.exec(SESSION_NAMES_TABLE_SQL); + db.exec(SESSION_NAMES_LOOKUP_INDEX_SQL); console.log('Database migrations completed successfully'); } catch (error) { @@ -158,8 +122,7 @@ const runMigrations = () => { // Initialize database with schema const initializeDatabase = async () => { try { - const initSQL = fs.readFileSync(INIT_SQL_PATH, 'utf8'); - db.exec(initSQL); + db.exec(DATABASE_SCHEMA_SQL); console.log('Database initialized successfully'); runMigrations(); } catch (error) { diff --git a/server/database/init.sql b/server/database/init.sql deleted file mode 100644 index 98351516..00000000 --- a/server/database/init.sql +++ /dev/null @@ -1,99 +0,0 @@ --- Initialize authentication database -PRAGMA foreign_keys = ON; - --- Users table (single user system) -CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT UNIQUE NOT NULL, - password_hash TEXT NOT NULL, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - last_login DATETIME, - is_active BOOLEAN DEFAULT 1, - git_name TEXT, - git_email TEXT, - has_completed_onboarding BOOLEAN DEFAULT 0 -); - --- Indexes for performance -CREATE INDEX IF NOT EXISTS idx_users_username ON users(username); -CREATE INDEX IF NOT EXISTS idx_users_active ON users(is_active); - --- API Keys table for external API access -CREATE TABLE IF NOT EXISTS api_keys ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL, - key_name TEXT NOT NULL, - api_key TEXT UNIQUE NOT NULL, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - last_used DATETIME, - is_active BOOLEAN DEFAULT 1, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE -); - -CREATE INDEX IF NOT EXISTS idx_api_keys_key ON api_keys(api_key); -CREATE INDEX IF NOT EXISTS idx_api_keys_user_id ON api_keys(user_id); -CREATE INDEX IF NOT EXISTS idx_api_keys_active ON api_keys(is_active); - --- User credentials table for storing various tokens/credentials (GitHub, GitLab, etc.) -CREATE TABLE IF NOT EXISTS user_credentials ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL, - credential_name TEXT NOT NULL, - credential_type TEXT NOT NULL, -- 'github_token', 'gitlab_token', 'bitbucket_token', etc. - credential_value TEXT NOT NULL, - description TEXT, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - is_active BOOLEAN DEFAULT 1, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE -); - -CREATE INDEX IF NOT EXISTS idx_user_credentials_user_id ON user_credentials(user_id); -CREATE INDEX IF NOT EXISTS idx_user_credentials_type ON user_credentials(credential_type); -CREATE INDEX IF NOT EXISTS idx_user_credentials_active ON user_credentials(is_active); - --- User notification preferences (backend-owned, provider-agnostic) -CREATE TABLE IF NOT EXISTS user_notification_preferences ( - user_id INTEGER PRIMARY KEY, - preferences_json TEXT NOT NULL, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE -); - --- VAPID key pair for Web Push notifications -CREATE TABLE IF NOT EXISTS vapid_keys ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - public_key TEXT NOT NULL, - private_key TEXT NOT NULL, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP -); - --- Browser push subscriptions -CREATE TABLE IF NOT EXISTS push_subscriptions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL, - endpoint TEXT NOT NULL UNIQUE, - keys_p256dh TEXT NOT NULL, - keys_auth TEXT NOT NULL, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE -); - --- Session custom names (provider-agnostic display name overrides) -CREATE TABLE IF NOT EXISTS session_names ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - session_id TEXT NOT NULL, - provider TEXT NOT NULL DEFAULT 'claude', - custom_name TEXT NOT NULL, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, - UNIQUE(session_id, provider) -); - -CREATE INDEX IF NOT EXISTS idx_session_names_lookup ON session_names(session_id, provider); - --- App configuration table (auto-generated secrets, settings, etc.) -CREATE TABLE IF NOT EXISTS app_config ( - key TEXT PRIMARY KEY, - value TEXT NOT NULL, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP -); diff --git a/server/database/schema.js b/server/database/schema.js new file mode 100644 index 00000000..21c1b8eb --- /dev/null +++ b/server/database/schema.js @@ -0,0 +1,102 @@ +export const APP_CONFIG_TABLE_SQL = `CREATE TABLE IF NOT EXISTS app_config ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +);`; + +export const USER_NOTIFICATION_PREFERENCES_TABLE_SQL = `CREATE TABLE IF NOT EXISTS user_notification_preferences ( + user_id INTEGER PRIMARY KEY, + preferences_json TEXT NOT NULL, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +);`; + +export const VAPID_KEYS_TABLE_SQL = `CREATE TABLE IF NOT EXISTS vapid_keys ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + public_key TEXT NOT NULL, + private_key TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +);`; + +export const PUSH_SUBSCRIPTIONS_TABLE_SQL = `CREATE TABLE IF NOT EXISTS push_subscriptions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + endpoint TEXT NOT NULL UNIQUE, + keys_p256dh TEXT NOT NULL, + keys_auth TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +);`; + +export const SESSION_NAMES_TABLE_SQL = `CREATE TABLE IF NOT EXISTS session_names ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + provider TEXT NOT NULL DEFAULT 'claude', + custom_name TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + UNIQUE(session_id, provider) +);`; + +export const SESSION_NAMES_LOOKUP_INDEX_SQL = `CREATE INDEX IF NOT EXISTS idx_session_names_lookup ON session_names(session_id, provider);`; + +export const DATABASE_SCHEMA_SQL = `PRAGMA foreign_keys = ON; + +CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + last_login DATETIME, + is_active BOOLEAN DEFAULT 1, + git_name TEXT, + git_email TEXT, + has_completed_onboarding BOOLEAN DEFAULT 0 +); + +CREATE INDEX IF NOT EXISTS idx_users_username ON users(username); +CREATE INDEX IF NOT EXISTS idx_users_active ON users(is_active); + +CREATE TABLE IF NOT EXISTS api_keys ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + key_name TEXT NOT NULL, + api_key TEXT UNIQUE NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + last_used DATETIME, + is_active BOOLEAN DEFAULT 1, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +); + +CREATE INDEX IF NOT EXISTS idx_api_keys_key ON api_keys(api_key); +CREATE INDEX IF NOT EXISTS idx_api_keys_user_id ON api_keys(user_id); +CREATE INDEX IF NOT EXISTS idx_api_keys_active ON api_keys(is_active); + +CREATE TABLE IF NOT EXISTS user_credentials ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + credential_name TEXT NOT NULL, + credential_type TEXT NOT NULL, + credential_value TEXT NOT NULL, + description TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + is_active BOOLEAN DEFAULT 1, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +); + +CREATE INDEX IF NOT EXISTS idx_user_credentials_user_id ON user_credentials(user_id); +CREATE INDEX IF NOT EXISTS idx_user_credentials_type ON user_credentials(credential_type); +CREATE INDEX IF NOT EXISTS idx_user_credentials_active ON user_credentials(is_active); + +${USER_NOTIFICATION_PREFERENCES_TABLE_SQL} + +${VAPID_KEYS_TABLE_SQL} + +${PUSH_SUBSCRIPTIONS_TABLE_SQL} + +${SESSION_NAMES_TABLE_SQL} + +${SESSION_NAMES_LOOKUP_INDEX_SQL} + +${APP_CONFIG_TABLE_SQL} +`; diff --git a/server/index.js b/server/index.js index da6ca675..3e5e657d 100755 --- a/server/index.js +++ b/server/index.js @@ -3,13 +3,13 @@ import './load-env.js'; import fs from 'fs'; import path from 'path'; -import { fileURLToPath } from 'url'; -import { dirname } from 'path'; +import { findAppRoot, getModuleDir } from './utils/runtime-paths.js'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -const installMode = fs.existsSync(path.join(__dirname, '..', '.git')) ? 'git' : 'npm'; +const __dirname = getModuleDir(import.meta.url); +// The server source runs from /server, while the compiled output runs from /dist-server/server. +// Resolving the app root once keeps every repo-level lookup below aligned across both layouts. +const APP_ROOT = findAppRoot(__dirname); +const installMode = fs.existsSync(path.join(APP_ROOT, '.git')) ? 'git' : 'npm'; // ANSI color codes for terminal output const colors = { @@ -405,11 +405,11 @@ app.use('/api/sessions', authenticateToken, messagesRoutes); app.use('/api/agent', agentRoutes); // Serve public files (like api-docs.html) -app.use(express.static(path.join(__dirname, '../public'))); +app.use(express.static(path.join(APP_ROOT, 'public'))); // Static files served after API routes // Add cache control: HTML files should not be cached, but assets can be cached -app.use(express.static(path.join(__dirname, '../dist'), { +app.use(express.static(path.join(APP_ROOT, 'dist'), { setHeaders: (res, filePath) => { if (filePath.endsWith('.html')) { // Prevent HTML caching to avoid service worker issues after builds @@ -431,7 +431,7 @@ app.use(express.static(path.join(__dirname, '../dist'), { app.post('/api/system/update', authenticateToken, async (req, res) => { try { // Get the project root directory (parent of server directory) - const projectRoot = path.join(__dirname, '..'); + const projectRoot = APP_ROOT; console.log('Starting system update from directory:', projectRoot); @@ -2273,7 +2273,7 @@ app.get('*', (req, res) => { // Only serve index.html for HTML routes, not for static assets // Static assets should already be handled by express.static middleware above - const indexPath = path.join(__dirname, '../dist/index.html'); + const indexPath = path.join(APP_ROOT, 'dist', 'index.html'); // Check if dist/index.html exists (production build available) if (fs.existsSync(indexPath)) { @@ -2388,7 +2388,7 @@ async function startServer() { configureWebPush(); // Check if running in production mode (dist folder exists) - const distIndexPath = path.join(__dirname, '../dist/index.html'); + const distIndexPath = path.join(APP_ROOT, 'dist', 'index.html'); const isProduction = fs.existsSync(distIndexPath); // Log Claude implementation mode @@ -2402,7 +2402,7 @@ async function startServer() { console.log(`${c.info('[INFO]')} To run in development mode with hot-module replacement, go to http://${DISPLAY_HOST}:${VITE_PORT}`); server.listen(SERVER_PORT, HOST, async () => { - const appInstallPath = path.join(__dirname, '..'); + const appInstallPath = APP_ROOT; console.log(''); console.log(c.dim('═'.repeat(63))); diff --git a/server/load-env.js b/server/load-env.js index ad9ccbb7..2c889534 100644 --- a/server/load-env.js +++ b/server/load-env.js @@ -2,14 +2,15 @@ import fs from 'fs'; import os from 'os'; import path from 'path'; -import { fileURLToPath } from 'url'; -import { dirname } from 'path'; +import { findAppRoot, getModuleDir } from './utils/runtime-paths.js'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); +const __dirname = getModuleDir(import.meta.url); +// Resolve the repo/app root via the nearest /server folder so this file keeps finding the +// same top-level .env file from both /server/load-env.js and /dist-server/server/load-env.js. +const APP_ROOT = findAppRoot(__dirname); try { - const envPath = path.join(__dirname, '../.env'); + const envPath = path.join(APP_ROOT, '.env'); const envFile = fs.readFileSync(envPath, 'utf8'); envFile.split('\n').forEach(line => { const trimmedLine = line.trim(); @@ -24,6 +25,10 @@ try { console.log('No .env file found or error reading it:', e.message); } +// Keep the default database in a stable user-level location so rebuilding dist-server +// never changes where the backend stores auth.db when DATABASE_PATH is not set explicitly. +const DEFAULT_DATABASE_PATH = path.join(os.homedir(), '.cloudcli', 'auth.db'); + if (!process.env.DATABASE_PATH) { - process.env.DATABASE_PATH = path.join(os.homedir(), '.cloudcli', 'auth.db'); + process.env.DATABASE_PATH = DEFAULT_DATABASE_PATH; } diff --git a/server/routes/commands.js b/server/routes/commands.js index 388a8f76..4ce3c4c0 100644 --- a/server/routes/commands.js +++ b/server/routes/commands.js @@ -1,13 +1,15 @@ import express from 'express'; import { promises as fs } from 'fs'; import path from 'path'; -import { fileURLToPath } from 'url'; import os from 'os'; import { CLAUDE_MODELS, CURSOR_MODELS, CODEX_MODELS } from '../../shared/modelConstants.js'; import { parseFrontmatter } from '../utils/frontmatter.js'; +import { findAppRoot, getModuleDir } from '../utils/runtime-paths.js'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); +const __dirname = getModuleDir(import.meta.url); +// This route reads the top-level package.json for the status command, so it needs the real +// app root even after compilation moves the route file under dist-server/server/routes. +const APP_ROOT = findAppRoot(__dirname); const router = express.Router(); @@ -291,7 +293,7 @@ Custom commands can be created in: '/status': async (args, context) => { // Read version from package.json - const packageJsonPath = path.join(path.dirname(__dirname), '..', 'package.json'); + const packageJsonPath = path.join(APP_ROOT, 'package.json'); let version = 'unknown'; let packageName = 'claude-code-ui'; diff --git a/server/tsconfig.json b/server/tsconfig.json new file mode 100644 index 00000000..59a363ed --- /dev/null +++ b/server/tsconfig.json @@ -0,0 +1,33 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "baseUrl": ".", + "paths": { + // In the backend config, "@" maps to the /server directory itself. + "@/*": ["*"] + }, + // The backend is still mostly JavaScript today, so allowJs lets us add a real + // TypeScript build without forcing a large rename before the tooling is usable. + "allowJs": true, + // Keep the migration incremental: existing JS keeps building, while any new TS files + // still go through the normal TypeScript pipeline and strict checks. + "checkJs": false, + "strict": true, + "noEmitOnError": true, + // The backend build emits both /server and /shared into dist-server, so rootDir must + // stay one level above this file even though the config itself now lives in /server. + "rootDir": "..", + "outDir": "../dist-server", + "sourceMap": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "types": ["node"] + }, + "include": ["./**/*.js", "./**/*.ts", "../shared/**/*.js", "../shared/**/*.ts"], + "exclude": ["../dist", "../dist-server", "../node_modules", "../src"] +} diff --git a/server/utils/runtime-paths.js b/server/utils/runtime-paths.js new file mode 100644 index 00000000..92137187 --- /dev/null +++ b/server/utils/runtime-paths.js @@ -0,0 +1,37 @@ +import path from 'path'; +import { fileURLToPath } from 'url'; + +export function getModuleDir(importMetaUrl) { + return path.dirname(fileURLToPath(importMetaUrl)); +} + +export function findServerRoot(startDir) { + // Source files live under /server, while compiled files live under /dist-server/server. + // Walking up to the nearest "server" folder gives every backend module one stable anchor + // that works in both layouts instead of relying on fragile "../.." assumptions. + let currentDir = startDir; + + while (path.basename(currentDir) !== 'server') { + const parentDir = path.dirname(currentDir); + + if (parentDir === currentDir) { + throw new Error(`Could not resolve the backend server root from "${startDir}".`); + } + + currentDir = parentDir; + } + + return currentDir; +} + +export function findAppRoot(startDir) { + const serverRoot = findServerRoot(startDir); + const parentOfServerRoot = path.dirname(serverRoot); + + // Source files live at /server, while compiled files live at /dist-server/server. + // When the nearest server folder sits inside dist-server we need to hop one extra level up + // so repo-level files still resolve from the real app root instead of the build directory. + return path.basename(parentOfServerRoot) === 'dist-server' + ? path.dirname(parentOfServerRoot) + : parentOfServerRoot; +} diff --git a/src/components/chat/hooks/useChatComposerState.ts b/src/components/chat/hooks/useChatComposerState.ts index 858faff9..15f4b63f 100644 --- a/src/components/chat/hooks/useChatComposerState.ts +++ b/src/components/chat/hooks/useChatComposerState.ts @@ -19,7 +19,7 @@ import type { PendingPermissionRequest, PermissionMode, } from '../types/types'; -import type { Project, ProjectSession, SessionProvider } from '../../../types/app'; +import type { Project, ProjectSession, LLMProvider } from '../../../types/app'; import { escapeRegExp } from '../utils/chatFormatting'; import { useFileMentions } from './useFileMentions'; import { type SlashCommand, useSlashCommands } from './useSlashCommands'; @@ -33,7 +33,7 @@ interface UseChatComposerStateArgs { selectedProject: Project | null; selectedSession: ProjectSession | null; currentSessionId: string | null; - provider: SessionProvider; + provider: LLMProvider; permissionMode: PermissionMode | string; cyclePermissionMode: () => void; cursorModel: string; diff --git a/src/components/chat/hooks/useChatProviderState.ts b/src/components/chat/hooks/useChatProviderState.ts index 9d48ce3d..6d39d22a 100644 --- a/src/components/chat/hooks/useChatProviderState.ts +++ b/src/components/chat/hooks/useChatProviderState.ts @@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { authenticatedFetch } from '../../../utils/api'; import { CLAUDE_MODELS, CODEX_MODELS, CURSOR_MODELS, GEMINI_MODELS } from '../../../../shared/modelConstants'; import type { PendingPermissionRequest, PermissionMode } from '../types/types'; -import type { ProjectSession, SessionProvider } from '../../../types/app'; +import type { ProjectSession, LLMProvider } from '../../../types/app'; interface UseChatProviderStateArgs { selectedSession: ProjectSession | null; @@ -11,8 +11,8 @@ interface UseChatProviderStateArgs { export function useChatProviderState({ selectedSession }: UseChatProviderStateArgs) { const [permissionMode, setPermissionMode] = useState('default'); const [pendingPermissionRequests, setPendingPermissionRequests] = useState([]); - const [provider, setProvider] = useState(() => { - return (localStorage.getItem('selected-provider') as SessionProvider) || 'claude'; + const [provider, setProvider] = useState(() => { + return (localStorage.getItem('selected-provider') as LLMProvider) || 'claude'; }); const [cursorModel, setCursorModel] = useState(() => { return localStorage.getItem('cursor-model') || CURSOR_MODELS.DEFAULT; diff --git a/src/components/chat/hooks/useChatRealtimeHandlers.ts b/src/components/chat/hooks/useChatRealtimeHandlers.ts index 6d734730..73d1a5e7 100644 --- a/src/components/chat/hooks/useChatRealtimeHandlers.ts +++ b/src/components/chat/hooks/useChatRealtimeHandlers.ts @@ -1,7 +1,7 @@ import { useEffect, useRef } from 'react'; import type { Dispatch, MutableRefObject, SetStateAction } from 'react'; import type { PendingPermissionRequest } from '../types/types'; -import type { Project, ProjectSession, SessionProvider } from '../../../types/app'; +import type { Project, ProjectSession, LLMProvider } from '../../../types/app'; import type { SessionStore, NormalizedMessage } from '../../../stores/useSessionStore'; type PendingViewSession = { @@ -48,7 +48,7 @@ type LatestChatMessage = { interface UseChatRealtimeHandlersArgs { latestMessage: LatestChatMessage | null; - provider: SessionProvider; + provider: LLMProvider; selectedProject: Project | null; selectedSession: ProjectSession | null; currentSessionId: string | null; diff --git a/src/components/chat/hooks/useChatSessionState.ts b/src/components/chat/hooks/useChatSessionState.ts index e952ee1a..b551060a 100644 --- a/src/components/chat/hooks/useChatSessionState.ts +++ b/src/components/chat/hooks/useChatSessionState.ts @@ -2,7 +2,7 @@ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } fr import type { MutableRefObject } from 'react'; import { authenticatedFetch } from '../../../utils/api'; import type { ChatMessage, Provider } from '../types/types'; -import type { Project, ProjectSession, SessionProvider } from '../../../types/app'; +import type { Project, ProjectSession, LLMProvider } from '../../../types/app'; import { createCachedDiffCalculator, type DiffCalculator } from '../utils/messageTransforms'; import { normalizedToChatMessages } from './useChatMessages'; import type { SessionStore, NormalizedMessage } from '../../../stores/useSessionStore'; @@ -40,7 +40,7 @@ interface ScrollRestoreState { function chatMessageToNormalized( msg: ChatMessage, sessionId: string, - provider: SessionProvider, + provider: LLMProvider, ): NormalizedMessage | null { const id = `local_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; const ts = msg.timestamp instanceof Date @@ -151,7 +151,7 @@ export function useChatSessionState({ // When a real session ID arrives and we have a pending user message, flush it to the store const prevActiveSessionRef = useRef(null); if (activeSessionId && activeSessionId !== prevActiveSessionRef.current && pendingUserMessage) { - const prov = (localStorage.getItem('selected-provider') as SessionProvider) || 'claude'; + const prov = (localStorage.getItem('selected-provider') as LLMProvider) || 'claude'; const normalized = chatMessageToNormalized(pendingUserMessage, activeSessionId, prov); if (normalized) { sessionStore.appendRealtime(activeSessionId, normalized); @@ -189,7 +189,7 @@ export function useChatSessionState({ setPendingUserMessage(msg); return; } - const prov = (localStorage.getItem('selected-provider') as SessionProvider) || 'claude'; + const prov = (localStorage.getItem('selected-provider') as LLMProvider) || 'claude'; const normalized = chatMessageToNormalized(msg, activeSessionId, prov); if (normalized) { sessionStore.appendRealtime(activeSessionId, normalized); @@ -240,7 +240,7 @@ export function useChatSessionState({ try { const slot = await sessionStore.fetchMore(selectedSession.id, { - provider: sessionProvider as SessionProvider, + provider: sessionProvider as LLMProvider, projectName: selectedProject.name, projectPath: selectedProject.fullPath || selectedProject.path || '', limit: MESSAGES_PER_PAGE, @@ -374,7 +374,7 @@ export function useChatSessionState({ // Fetch from server → store updates → chatMessages re-derives automatically setIsLoadingSessionMessages(true); sessionStore.fetchFromServer(selectedSession.id, { - provider: (selectedSession.__provider || provider) as SessionProvider, + provider: (selectedSession.__provider || provider) as LLMProvider, projectName: selectedProject.name, projectPath: selectedProject.fullPath || selectedProject.path || '', limit: MESSAGES_PER_PAGE, @@ -410,7 +410,7 @@ export function useChatSessionState({ // Skip store refresh during active streaming if (!isLoading) { await sessionStore.refreshFromServer(selectedSession.id, { - provider: (selectedSession.__provider || provider) as SessionProvider, + provider: (selectedSession.__provider || provider) as LLMProvider, projectName: selectedProject.name, projectPath: selectedProject.fullPath || selectedProject.path || '', }); @@ -468,7 +468,7 @@ export function useChatSessionState({ try { // Load all messages into the store for search navigation const slot = await sessionStore.fetchFromServer(selectedSession.id, { - provider: sessionProvider as SessionProvider, + provider: sessionProvider as LLMProvider, projectName: selectedProject.name, projectPath: selectedProject.fullPath || selectedProject.path || '', limit: null, @@ -655,7 +655,7 @@ export function useChatSessionState({ try { const slot = await sessionStore.fetchFromServer(requestSessionId, { - provider: sessionProvider as SessionProvider, + provider: sessionProvider as LLMProvider, projectName: selectedProject.name, projectPath: selectedProject.fullPath || selectedProject.path || '', limit: null, diff --git a/src/components/chat/types/types.ts b/src/components/chat/types/types.ts index 66d50741..900028c0 100644 --- a/src/components/chat/types/types.ts +++ b/src/components/chat/types/types.ts @@ -1,6 +1,6 @@ -import type { Project, ProjectSession, SessionProvider } from '../../../types/app'; +import type { Project, ProjectSession, LLMProvider } from '../../../types/app'; -export type Provider = SessionProvider; +export type Provider = LLMProvider; export type PermissionMode = 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan'; diff --git a/src/components/chat/view/ChatInterface.tsx b/src/components/chat/view/ChatInterface.tsx index 19483f64..44709502 100644 --- a/src/components/chat/view/ChatInterface.tsx +++ b/src/components/chat/view/ChatInterface.tsx @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'; import { useTasksSettings } from '../../../contexts/TasksSettingsContext'; import { QuickSettingsPanel } from '../../quick-settings-panel'; import type { ChatInterfaceProps, Provider } from '../types/types'; -import type { SessionProvider } from '../../../types/app'; +import type { LLMProvider } from '../../../types/app'; import { useChatProviderState } from '../hooks/useChatProviderState'; import { useChatSessionState } from '../hooks/useChatSessionState'; import { useChatRealtimeHandlers } from '../hooks/useChatRealtimeHandlers'; @@ -206,9 +206,9 @@ function ChatInterface({ // so missed streaming events are shown. Also reset isLoading. const handleWebSocketReconnect = useCallback(async () => { if (!selectedProject || !selectedSession) return; - const providerVal = (localStorage.getItem('selected-provider') as SessionProvider) || 'claude'; + const providerVal = (localStorage.getItem('selected-provider') as LLMProvider) || 'claude'; await sessionStore.refreshFromServer(selectedSession.id, { - provider: (selectedSession.__provider || providerVal) as SessionProvider, + provider: (selectedSession.__provider || providerVal) as LLMProvider, projectName: selectedProject.name, projectPath: selectedProject.fullPath || selectedProject.path || '', }); diff --git a/src/components/chat/view/subcomponents/ChatMessagesPane.tsx b/src/components/chat/view/subcomponents/ChatMessagesPane.tsx index 63ae4841..9d47a7c2 100644 --- a/src/components/chat/view/subcomponents/ChatMessagesPane.tsx +++ b/src/components/chat/view/subcomponents/ChatMessagesPane.tsx @@ -2,7 +2,7 @@ import { useTranslation } from 'react-i18next'; import { useCallback, useRef } from 'react'; import type { Dispatch, RefObject, SetStateAction } from 'react'; import type { ChatMessage } from '../../types/types'; -import type { Project, ProjectSession, SessionProvider } from '../../../../types/app'; +import type { Project, ProjectSession, LLMProvider } from '../../../../types/app'; import { getIntrinsicMessageKey } from '../../utils/messageKeys'; import MessageComponent from './MessageComponent'; import ProviderSelectionEmptyState from './ProviderSelectionEmptyState'; @@ -15,8 +15,8 @@ interface ChatMessagesPaneProps { chatMessages: ChatMessage[]; selectedSession: ProjectSession | null; currentSessionId: string | null; - provider: SessionProvider; - setProvider: (provider: SessionProvider) => void; + provider: LLMProvider; + setProvider: (provider: LLMProvider) => void; textareaRef: RefObject; claudeModel: string; setClaudeModel: (model: string) => void; diff --git a/src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx b/src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx index 792b12c7..9eaf690e 100644 --- a/src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx +++ b/src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx @@ -8,14 +8,14 @@ import { CODEX_MODELS, GEMINI_MODELS, } from "../../../../../shared/modelConstants"; -import type { ProjectSession, SessionProvider } from "../../../../types/app"; +import type { ProjectSession, LLMProvider } from "../../../../types/app"; import { NextTaskBanner } from "../../../task-master"; type ProviderSelectionEmptyStateProps = { selectedSession: ProjectSession | null; currentSessionId: string | null; - provider: SessionProvider; - setProvider: (next: SessionProvider) => void; + provider: LLMProvider; + setProvider: (next: LLMProvider) => void; textareaRef: React.RefObject; claudeModel: string; setClaudeModel: (model: string) => void; @@ -32,7 +32,7 @@ type ProviderSelectionEmptyStateProps = { }; type ProviderDef = { - id: SessionProvider; + id: LLMProvider; name: string; infoKey: string; accent: string; @@ -75,7 +75,7 @@ const PROVIDERS: ProviderDef[] = [ }, ]; -function getModelConfig(p: SessionProvider) { +function getModelConfig(p: LLMProvider) { if (p === "claude") return CLAUDE_MODELS; if (p === "codex") return CODEX_MODELS; if (p === "gemini") return GEMINI_MODELS; @@ -83,7 +83,7 @@ function getModelConfig(p: SessionProvider) { } function getModelValue( - p: SessionProvider, + p: LLMProvider, c: string, cu: string, co: string, @@ -119,7 +119,7 @@ export default function ProviderSelectionEmptyState({ defaultValue: "Start the next task", }); - const selectProvider = (next: SessionProvider) => { + const selectProvider = (next: LLMProvider) => { setProvider(next); localStorage.setItem("selected-provider", next); setTimeout(() => textareaRef.current?.focus(), 100); diff --git a/src/components/llm-logo-provider/SessionProviderLogo.tsx b/src/components/llm-logo-provider/SessionProviderLogo.tsx index 1eaef523..53c02291 100644 --- a/src/components/llm-logo-provider/SessionProviderLogo.tsx +++ b/src/components/llm-logo-provider/SessionProviderLogo.tsx @@ -1,11 +1,11 @@ -import type { SessionProvider } from '../../types/app'; +import type { LLMProvider } from '../../types/app'; import ClaudeLogo from './ClaudeLogo'; import CodexLogo from './CodexLogo'; import CursorLogo from './CursorLogo'; import GeminiLogo from './GeminiLogo'; type SessionProviderLogoProps = { - provider?: SessionProvider | string | null; + provider?: LLMProvider | string | null; className?: string; }; diff --git a/src/components/onboarding/view/Onboarding.tsx b/src/components/onboarding/view/Onboarding.tsx index 58627c4c..6d9a69ca 100644 --- a/src/components/onboarding/view/Onboarding.tsx +++ b/src/components/onboarding/view/Onboarding.tsx @@ -1,17 +1,15 @@ import { Check, ChevronLeft, ChevronRight, Loader2 } from 'lucide-react'; import { useCallback, useEffect, useRef, useState } from 'react'; +import type { LLMProvider } from '../../../types/app'; import { authenticatedFetch } from '../../../utils/api'; +import { useProviderAuthStatus } from '../../provider-auth/hooks/useProviderAuthStatus'; import ProviderLoginModal from '../../provider-auth/view/ProviderLoginModal'; import AgentConnectionsStep from './subcomponents/AgentConnectionsStep'; import GitConfigurationStep from './subcomponents/GitConfigurationStep'; import OnboardingStepProgress from './subcomponents/OnboardingStepProgress'; -import type { CliProvider, ProviderStatusMap } from './types'; import { - cliProviders, - createInitialProviderStatuses, gitEmailPattern, readErrorMessageFromResponse, - selectedProject, } from './utils'; type OnboardingProps = { @@ -24,59 +22,14 @@ export default function Onboarding({ onComplete }: OnboardingProps) { const [gitEmail, setGitEmail] = useState(''); const [isSubmitting, setIsSubmitting] = useState(false); const [errorMessage, setErrorMessage] = useState(''); - const [activeLoginProvider, setActiveLoginProvider] = useState(null); - const [providerStatuses, setProviderStatuses] = useState(createInitialProviderStatuses); + const [activeLoginProvider, setActiveLoginProvider] = useState(null); + const { + providerAuthStatus, + checkProviderAuthStatus, + refreshProviderAuthStatuses, + } = useProviderAuthStatus(); - const previousActiveLoginProviderRef = useRef(undefined); - - const checkProviderAuthStatus = useCallback(async (provider: CliProvider) => { - try { - const response = await authenticatedFetch(`/api/cli/${provider}/status`); - if (!response.ok) { - setProviderStatuses((previous) => ({ - ...previous, - [provider]: { - authenticated: false, - email: null, - loading: false, - error: 'Failed to check authentication status', - }, - })); - return; - } - - const payload = (await response.json()) as { - authenticated?: boolean; - email?: string | null; - error?: string | null; - }; - - setProviderStatuses((previous) => ({ - ...previous, - [provider]: { - authenticated: Boolean(payload.authenticated), - email: payload.email ?? null, - loading: false, - error: payload.error ?? null, - }, - })); - } catch (caughtError) { - console.error(`Error checking ${provider} auth status:`, caughtError); - setProviderStatuses((previous) => ({ - ...previous, - [provider]: { - authenticated: false, - email: null, - loading: false, - error: caughtError instanceof Error ? caughtError.message : 'Unknown error', - }, - })); - } - }, []); - - const refreshAllProviderStatuses = useCallback(async () => { - await Promise.all(cliProviders.map((provider) => checkProviderAuthStatus(provider))); - }, [checkProviderAuthStatus]); + const previousActiveLoginProviderRef = useRef(undefined); const loadGitConfig = useCallback(async () => { try { @@ -99,23 +52,24 @@ export default function Onboarding({ onComplete }: OnboardingProps) { useEffect(() => { void loadGitConfig(); - void refreshAllProviderStatuses(); - }, [loadGitConfig, refreshAllProviderStatuses]); + void refreshProviderAuthStatuses(); + }, [loadGitConfig, refreshProviderAuthStatuses]); useEffect(() => { const previousProvider = previousActiveLoginProviderRef.current; previousActiveLoginProviderRef.current = activeLoginProvider; - const isInitialMount = previousProvider === undefined; - const didCloseModal = previousProvider !== null && activeLoginProvider === null; + const didCloseModal = previousProvider !== undefined + && previousProvider !== null + && activeLoginProvider === null; - // Refresh statuses once on mount and again after the login modal is closed. - if (isInitialMount || didCloseModal) { - void refreshAllProviderStatuses(); + // Refresh statuses after the login modal is closed. + if (didCloseModal) { + void refreshProviderAuthStatuses(); } - }, [activeLoginProvider, refreshAllProviderStatuses]); + }, [activeLoginProvider, refreshProviderAuthStatuses]); - const handleProviderLoginOpen = (provider: CliProvider) => { + const handleProviderLoginOpen = (provider: LLMProvider) => { setActiveLoginProvider(provider); }; @@ -209,7 +163,7 @@ export default function Onboarding({ onComplete }: OnboardingProps) { /> ) : ( )} @@ -279,7 +233,6 @@ export default function Onboarding({ onComplete }: OnboardingProps) { isOpen={Boolean(activeLoginProvider)} onClose={() => setActiveLoginProvider(null)} provider={activeLoginProvider} - project={selectedProject} onComplete={handleLoginComplete} /> )} diff --git a/src/components/onboarding/view/subcomponents/AgentConnectionCard.tsx b/src/components/onboarding/view/subcomponents/AgentConnectionCard.tsx index edf2ef0e..3737ad54 100644 --- a/src/components/onboarding/view/subcomponents/AgentConnectionCard.tsx +++ b/src/components/onboarding/view/subcomponents/AgentConnectionCard.tsx @@ -1,9 +1,10 @@ import { Check } from 'lucide-react'; import SessionProviderLogo from '../../../llm-logo-provider/SessionProviderLogo'; -import type { CliProvider, ProviderAuthStatus } from '../types'; +import type { LLMProvider } from '../../../../types/app'; +import type { ProviderAuthStatus } from '../../../provider-auth/types'; type AgentConnectionCardProps = { - provider: CliProvider; + provider: LLMProvider; title: string; status: ProviderAuthStatus; connectedClassName: string; diff --git a/src/components/onboarding/view/subcomponents/AgentConnectionsStep.tsx b/src/components/onboarding/view/subcomponents/AgentConnectionsStep.tsx index 5bca5d33..3dfb25a9 100644 --- a/src/components/onboarding/view/subcomponents/AgentConnectionsStep.tsx +++ b/src/components/onboarding/view/subcomponents/AgentConnectionsStep.tsx @@ -1,9 +1,10 @@ -import type { CliProvider, ProviderStatusMap } from '../types'; +import type { LLMProvider } from '../../../../types/app'; +import type { ProviderAuthStatusMap } from '../../../provider-auth/types'; import AgentConnectionCard from './AgentConnectionCard'; type AgentConnectionsStepProps = { - providerStatuses: ProviderStatusMap; - onOpenProviderLogin: (provider: CliProvider) => void; + providerStatuses: ProviderAuthStatusMap; + onOpenProviderLogin: (provider: LLMProvider) => void; }; const providerCards = [ diff --git a/src/components/onboarding/view/types.ts b/src/components/onboarding/view/types.ts deleted file mode 100644 index 46800813..00000000 --- a/src/components/onboarding/view/types.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { CliProvider } from '../../provider-auth/types'; - -export type { CliProvider }; - -export type ProviderAuthStatus = { - authenticated: boolean; - email: string | null; - loading: boolean; - error: string | null; -}; - -export type ProviderStatusMap = Record; diff --git a/src/components/onboarding/view/utils.ts b/src/components/onboarding/view/utils.ts index adb3c4bc..0b40ab02 100644 --- a/src/components/onboarding/view/utils.ts +++ b/src/components/onboarding/view/utils.ts @@ -1,24 +1,5 @@ -import { IS_PLATFORM } from '../../../constants/config'; -import type { CliProvider, ProviderStatusMap } from './types'; - -export const cliProviders: CliProvider[] = ['claude', 'cursor', 'codex', 'gemini']; - export const gitEmailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; -export const selectedProject = { - name: 'default', - displayName: 'default', - fullPath: IS_PLATFORM ? '/workspace' : '', - path: IS_PLATFORM ? '/workspace' : '', -}; - -export const createInitialProviderStatuses = (): ProviderStatusMap => ({ - claude: { authenticated: false, email: null, loading: true, error: null }, - cursor: { authenticated: false, email: null, loading: true, error: null }, - codex: { authenticated: false, email: null, loading: true, error: null }, - gemini: { authenticated: false, email: null, loading: true, error: null }, -}); - export const readErrorMessageFromResponse = async (response: Response, fallback: string) => { try { const payload = (await response.json()) as { error?: string }; diff --git a/src/components/provider-auth/hooks/useProviderAuthStatus.ts b/src/components/provider-auth/hooks/useProviderAuthStatus.ts new file mode 100644 index 00000000..83692a2a --- /dev/null +++ b/src/components/provider-auth/hooks/useProviderAuthStatus.ts @@ -0,0 +1,109 @@ +import { useCallback, useState } from 'react'; +import { authenticatedFetch } from '../../../utils/api'; +import type { LLMProvider } from '../../../types/app'; +import { + CLI_AUTH_STATUS_ENDPOINTS, + CLI_PROVIDERS, + createInitialProviderAuthStatusMap, +} from '../types'; +import type { + ProviderAuthStatus, + ProviderAuthStatusMap, +} from '../types'; + +type ProviderAuthStatusPayload = { + authenticated?: boolean; + email?: string | null; + method?: string | null; + error?: string | null; +}; + +const FALLBACK_STATUS_ERROR = 'Failed to check authentication status'; +const FALLBACK_UNKNOWN_ERROR = 'Unknown error'; + +const toErrorMessage = (error: unknown): string => ( + error instanceof Error ? error.message : FALLBACK_UNKNOWN_ERROR +); + +const toProviderAuthStatus = ( + payload: ProviderAuthStatusPayload, + fallbackError: string | null = null, +): ProviderAuthStatus => ({ + authenticated: Boolean(payload.authenticated), + email: payload.email ?? null, + method: payload.method ?? null, + error: payload.error ?? fallbackError, + loading: false, +}); + +type UseProviderAuthStatusOptions = { + initialLoading?: boolean; +}; + +export function useProviderAuthStatus( + { initialLoading = true }: UseProviderAuthStatusOptions = {}, +) { + const [providerAuthStatus, setProviderAuthStatus] = useState(() => ( + createInitialProviderAuthStatusMap(initialLoading) + )); + + const setProviderLoading = useCallback((provider: LLMProvider) => { + setProviderAuthStatus((previous) => ({ + ...previous, + [provider]: { + ...previous[provider], + loading: true, + error: null, + }, + })); + }, []); + + const setProviderStatus = useCallback((provider: LLMProvider, status: ProviderAuthStatus) => { + setProviderAuthStatus((previous) => ({ + ...previous, + [provider]: status, + })); + }, []); + + const checkProviderAuthStatus = useCallback(async (provider: LLMProvider) => { + setProviderLoading(provider); + + try { + const response = await authenticatedFetch(CLI_AUTH_STATUS_ENDPOINTS[provider]); + + if (!response.ok) { + setProviderStatus(provider, { + authenticated: false, + email: null, + method: null, + loading: false, + error: FALLBACK_STATUS_ERROR, + }); + return; + } + + const payload = (await response.json()) as ProviderAuthStatusPayload; + setProviderStatus(provider, toProviderAuthStatus(payload)); + } catch (caughtError) { + console.error(`Error checking ${provider} auth status:`, caughtError); + setProviderStatus(provider, { + authenticated: false, + email: null, + method: null, + loading: false, + error: toErrorMessage(caughtError), + }); + } + }, [setProviderLoading, setProviderStatus]); + + const refreshProviderAuthStatuses = useCallback(async (providers: LLMProvider[] = CLI_PROVIDERS) => { + await Promise.all(providers.map((provider) => checkProviderAuthStatus(provider))); + }, [checkProviderAuthStatus]); + + return { + providerAuthStatus, + setProviderAuthStatus, + checkProviderAuthStatus, + refreshProviderAuthStatuses, + }; +} diff --git a/src/components/provider-auth/types.ts b/src/components/provider-auth/types.ts index e39a9796..bb93199c 100644 --- a/src/components/provider-auth/types.ts +++ b/src/components/provider-auth/types.ts @@ -1 +1,27 @@ -export type CliProvider = 'claude' | 'cursor' | 'codex' | 'gemini'; +import type { LLMProvider } from '../../types/app'; + +export type ProviderAuthStatus = { + authenticated: boolean; + email: string | null; + method: string | null; + error: string | null; + loading: boolean; +}; + +export type ProviderAuthStatusMap = Record; + +export const CLI_PROVIDERS: LLMProvider[] = ['claude', 'cursor', 'codex', 'gemini']; + +export const CLI_AUTH_STATUS_ENDPOINTS: Record = { + claude: '/api/cli/claude/status', + cursor: '/api/cli/cursor/status', + codex: '/api/cli/codex/status', + gemini: '/api/cli/gemini/status', +}; + +export const createInitialProviderAuthStatusMap = (loading = true): ProviderAuthStatusMap => ({ + claude: { authenticated: false, email: null, method: null, error: null, loading }, + cursor: { authenticated: false, email: null, method: null, error: null, loading }, + codex: { authenticated: false, email: null, method: null, error: null, loading }, + gemini: { authenticated: false, email: null, method: null, error: null, loading }, +}); diff --git a/src/components/provider-auth/view/ProviderLoginModal.tsx b/src/components/provider-auth/view/ProviderLoginModal.tsx index 9e80302e..fa1d4407 100644 --- a/src/components/provider-auth/view/ProviderLoginModal.tsx +++ b/src/components/provider-auth/view/ProviderLoginModal.tsx @@ -1,21 +1,12 @@ import { ExternalLink, KeyRound, X } from 'lucide-react'; import StandaloneShell from '../../standalone-shell/view/StandaloneShell'; -import { IS_PLATFORM } from '../../../constants/config'; -import type { CliProvider } from '../types'; - -type LoginModalProject = { - name?: string; - displayName?: string; - fullPath?: string; - path?: string; - [key: string]: unknown; -}; +import { DEFAULT_PROJECT_FOR_EMPTY_SHELL, IS_PLATFORM } from '../../../constants/config'; +import type { LLMProvider } from '../../../types/app'; type ProviderLoginModalProps = { isOpen: boolean; onClose: () => void; - provider?: CliProvider; - project?: LoginModalProject | null; + provider?: LLMProvider; onComplete?: (exitCode: number) => void; customCommand?: string; isAuthenticated?: boolean; @@ -26,7 +17,7 @@ const getProviderCommand = ({ customCommand, isAuthenticated: _isAuthenticated, }: { - provider: CliProvider; + provider: LLMProvider; customCommand?: string; isAuthenticated: boolean; }) => { @@ -49,30 +40,17 @@ const getProviderCommand = ({ return 'gemini status'; }; -const getProviderTitle = (provider: CliProvider) => { +const getProviderTitle = (provider: LLMProvider) => { if (provider === 'claude') return 'Claude CLI Login'; if (provider === 'cursor') return 'Cursor CLI Login'; if (provider === 'codex') return 'Codex CLI Login'; return 'Gemini CLI Configuration'; }; -const normalizeProject = (project?: LoginModalProject | null) => { - const normalizedName = project?.name || 'default'; - const normalizedFullPath = project?.fullPath ?? project?.path ?? (IS_PLATFORM ? '/workspace' : ''); - - return { - name: normalizedName, - displayName: project?.displayName || normalizedName, - fullPath: normalizedFullPath, - path: project?.path ?? normalizedFullPath, - }; -}; - export default function ProviderLoginModal({ isOpen, onClose, provider = 'claude', - project = null, onComplete, customCommand, isAuthenticated = false, @@ -83,7 +61,6 @@ export default function ProviderLoginModal({ const command = getProviderCommand({ provider, customCommand, isAuthenticated }); const title = getProviderTitle(provider); - const shellProject = normalizeProject(project); const handleComplete = (exitCode: number) => { onComplete?.(exitCode); @@ -158,7 +135,7 @@ export default function ProviderLoginModal({ ) : ( - + )} diff --git a/src/components/settings/constants/constants.ts b/src/components/settings/constants/constants.ts index 36f45392..52f16e10 100644 --- a/src/components/settings/constants/constants.ts +++ b/src/components/settings/constants/constants.ts @@ -1,7 +1,6 @@ import type { AgentCategory, AgentProvider, - AuthStatus, ClaudeMcpFormState, CodexMcpFormState, CodeEditorSettingsState, @@ -34,13 +33,6 @@ export const DEFAULT_CODE_EDITOR_SETTINGS: CodeEditorSettingsState = { fontSize: '14', }; -export const DEFAULT_AUTH_STATUS: AuthStatus = { - authenticated: false, - email: null, - loading: true, - error: null, -}; - export const DEFAULT_MCP_TEST_RESULT: McpTestResult = { success: false, message: '', @@ -88,9 +80,3 @@ export const DEFAULT_CURSOR_PERMISSIONS: CursorPermissionsState = { skipPermissions: false, }; -export const AUTH_STATUS_ENDPOINTS: Record = { - claude: '/api/cli/claude/status', - cursor: '/api/cli/cursor/status', - codex: '/api/cli/codex/status', - gemini: '/api/cli/gemini/status', -}; diff --git a/src/components/settings/hooks/useSettingsController.ts b/src/components/settings/hooks/useSettingsController.ts index 293cbceb..09550265 100644 --- a/src/components/settings/hooks/useSettingsController.ts +++ b/src/components/settings/hooks/useSettingsController.ts @@ -1,15 +1,13 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { useTheme } from '../../../contexts/ThemeContext'; import { authenticatedFetch } from '../../../utils/api'; +import { useProviderAuthStatus } from '../../provider-auth/hooks/useProviderAuthStatus'; import { - AUTH_STATUS_ENDPOINTS, - DEFAULT_AUTH_STATUS, DEFAULT_CODE_EDITOR_SETTINGS, DEFAULT_CURSOR_PERMISSIONS, } from '../constants/constants'; import type { AgentProvider, - AuthStatus, ClaudeMcpFormState, ClaudePermissionsState, CodeEditorSettingsState, @@ -23,7 +21,6 @@ import type { NotificationPreferencesState, ProjectSortOrder, SettingsMainTab, - SettingsProject, } from '../types/types'; type ThemeContextValue = { @@ -34,15 +31,6 @@ type ThemeContextValue = { type UseSettingsControllerArgs = { isOpen: boolean; initialTab: string; - projects: SettingsProject[]; - onClose: () => void; -}; - -type StatusApiResponse = { - authenticated?: boolean; - email?: string | null; - error?: string | null; - method?: string; }; type JsonResult = { @@ -166,20 +154,6 @@ const mapCliServersToMcpServers = (servers: McpCliServer[] = []): McpServer[] => })) ); -const getDefaultProject = (projects: SettingsProject[]): SettingsProject => { - if (projects.length > 0) { - return projects[0]; - } - - const cwd = typeof process !== 'undefined' && process.cwd ? process.cwd() : ''; - return { - name: 'default', - displayName: 'default', - fullPath: cwd, - path: cwd, - }; -}; - const toResponseJson = async (response: Response): Promise => response.json() as Promise; const createEmptyClaudePermissions = (): ClaudePermissionsState => ({ @@ -204,7 +178,7 @@ const createDefaultNotificationPreferences = (): NotificationPreferencesState => }, }); -export function useSettingsController({ isOpen, initialTab, projects, onClose }: UseSettingsControllerArgs) { +export function useSettingsController({ isOpen, initialTab }: UseSettingsControllerArgs) { const { isDarkMode, toggleDarkMode } = useTheme() as ThemeContextValue; const closeTimerRef = useRef(null); @@ -242,64 +216,11 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }: const [showLoginModal, setShowLoginModal] = useState(false); const [loginProvider, setLoginProvider] = useState(''); - const [selectedProject, setSelectedProject] = useState(null); - - const [claudeAuthStatus, setClaudeAuthStatus] = useState(DEFAULT_AUTH_STATUS); - const [cursorAuthStatus, setCursorAuthStatus] = useState(DEFAULT_AUTH_STATUS); - const [codexAuthStatus, setCodexAuthStatus] = useState(DEFAULT_AUTH_STATUS); - const [geminiAuthStatus, setGeminiAuthStatus] = useState(DEFAULT_AUTH_STATUS); - - const setAuthStatusByProvider = useCallback((provider: AgentProvider, status: AuthStatus) => { - if (provider === 'claude') { - setClaudeAuthStatus(status); - return; - } - - if (provider === 'cursor') { - setCursorAuthStatus(status); - return; - } - - if (provider === 'gemini') { - setGeminiAuthStatus(status); - return; - } - - setCodexAuthStatus(status); - }, []); - - const checkAuthStatus = useCallback(async (provider: AgentProvider) => { - try { - const response = await authenticatedFetch(AUTH_STATUS_ENDPOINTS[provider]); - - if (!response.ok) { - setAuthStatusByProvider(provider, { - authenticated: false, - email: null, - loading: false, - error: 'Failed to check authentication status', - }); - return; - } - - const data = await toResponseJson(response); - setAuthStatusByProvider(provider, { - authenticated: Boolean(data.authenticated), - email: data.email || null, - loading: false, - error: data.error || null, - method: data.method, - }); - } catch (error) { - console.error(`Error checking ${provider} auth status:`, error); - setAuthStatusByProvider(provider, { - authenticated: false, - email: null, - loading: false, - error: getErrorMessage(error), - }); - } - }, [setAuthStatusByProvider]); + const { + providerAuthStatus, + checkProviderAuthStatus, + refreshProviderAuthStatuses, + } = useProviderAuthStatus(); const fetchCursorMcpServers = useCallback(async () => { try { @@ -724,9 +645,8 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }: const openLoginForProvider = useCallback((provider: AgentProvider) => { setLoginProvider(provider); - setSelectedProject(getDefaultProject(projects)); setShowLoginModal(true); - }, [projects]); + }, []); const handleLoginComplete = useCallback((exitCode: number) => { if (exitCode !== 0 || !loginProvider) { @@ -734,8 +654,8 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }: } setSaveStatus('success'); - void checkAuthStatus(loginProvider); - }, [checkAuthStatus, loginProvider]); + void checkProviderAuthStatus(loginProvider); + }, [checkProviderAuthStatus, loginProvider]); const saveSettings = useCallback(async () => { setSaveStatus(null); @@ -827,11 +747,8 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }: setActiveTab(normalizeMainTab(initialTab)); void loadSettings(); - void checkAuthStatus('claude'); - void checkAuthStatus('cursor'); - void checkAuthStatus('codex'); - void checkAuthStatus('gemini'); - }, [checkAuthStatus, initialTab, isOpen, loadSettings]); + void refreshProviderAuthStatuses(); + }, [initialTab, isOpen, loadSettings, refreshProviderAuthStatuses]); useEffect(() => { localStorage.setItem('codeEditorTheme', codeEditorSettings.theme); @@ -935,17 +852,13 @@ export function useSettingsController({ isOpen, initialTab, projects, onClose }: closeCodexMcpForm, submitCodexMcpForm, handleCodexMcpDelete, - claudeAuthStatus, - cursorAuthStatus, - codexAuthStatus, - geminiAuthStatus, + providerAuthStatus, geminiPermissionMode, setGeminiPermissionMode, openLoginForProvider, showLoginModal, setShowLoginModal, loginProvider, - selectedProject, handleLoginComplete, }; } diff --git a/src/components/settings/types/types.ts b/src/components/settings/types/types.ts index e3af730b..235d0b47 100644 --- a/src/components/settings/types/types.ts +++ b/src/components/settings/types/types.ts @@ -1,7 +1,9 @@ import type { Dispatch, SetStateAction } from 'react'; +import type { LLMProvider } from '../../../types/app'; +import type { ProviderAuthStatus } from '../../provider-auth/types'; export type SettingsMainTab = 'agents' | 'appearance' | 'git' | 'api' | 'tasks' | 'notifications' | 'plugins' | 'about'; -export type AgentProvider = 'claude' | 'cursor' | 'codex' | 'gemini'; +export type AgentProvider = LLMProvider; export type AgentCategory = 'account' | 'permissions' | 'mcp'; export type ProjectSortOrder = 'name' | 'date'; export type SaveStatus = 'success' | 'error' | null; @@ -18,13 +20,7 @@ export type SettingsProject = { path?: string; }; -export type AuthStatus = { - authenticated: boolean; - email: string | null; - loading: boolean; - error: string | null; - method?: string; -}; +export type AuthStatus = ProviderAuthStatus; export type KeyValueMap = Record; diff --git a/src/components/settings/view/Settings.tsx b/src/components/settings/view/Settings.tsx index 70d3ea4d..c1977cf2 100644 --- a/src/components/settings/view/Settings.tsx +++ b/src/components/settings/view/Settings.tsx @@ -56,23 +56,17 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: Set closeCodexMcpForm, submitCodexMcpForm, handleCodexMcpDelete, - claudeAuthStatus, - cursorAuthStatus, - codexAuthStatus, - geminiAuthStatus, + providerAuthStatus, geminiPermissionMode, setGeminiPermissionMode, openLoginForProvider, showLoginModal, setShowLoginModal, loginProvider, - selectedProject, handleLoginComplete, } = useSettingsController({ isOpen, - initialTab, - projects, - onClose, + initialTab }); const { @@ -105,13 +99,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: Set return null; } - const isAuthenticated = loginProvider === 'claude' - ? claudeAuthStatus.authenticated - : loginProvider === 'cursor' - ? cursorAuthStatus.authenticated - : loginProvider === 'codex' - ? codexAuthStatus.authenticated - : false; + const isAuthenticated = Boolean(loginProvider && providerAuthStatus[loginProvider].authenticated); return (
@@ -121,7 +109,7 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: Set

{t('title')}

{saveStatus === 'success' && ( - {t('saveStatus.success')} + {t('saveStatus.success')} )}