@cloudcli-ai/cloudcli 1.37.1 → 1.37.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/dist/assets/abnfDiagram-N423BO3Z-C4UAoHJb.js +1 -0
  2. package/dist/assets/arc-B8jF-ja9.js +1 -0
  3. package/dist/assets/architectureDiagram-T3A2C74G-C_Wur_CU.js +36 -0
  4. package/dist/assets/blockDiagram-VBNYF7ZC-Cq4P1i8t.js +132 -0
  5. package/dist/assets/c4Diagram-5PPSVZJV-C1wG4eUj.js +10 -0
  6. package/dist/assets/channel-_falYbU1.js +1 -0
  7. package/dist/assets/chunk-2GRJ4B5K-B8fnBG6E.js +1 -0
  8. package/dist/assets/chunk-2Q5K7J3B-WdfPooPJ.js +1 -0
  9. package/dist/assets/chunk-5RXB4S5H-DhZJuG2E.js +231 -0
  10. package/dist/assets/chunk-5VM5RSS4-B0rjslal.js +15 -0
  11. package/dist/assets/chunk-6Q2QTUOP-CG2Oj4jL.js +88 -0
  12. package/dist/assets/chunk-GF5L2VYU-rrl2lRxY.js +206 -0
  13. package/dist/assets/chunk-JWPE2WC7-CjVbrVMA.js +1 -0
  14. package/dist/assets/chunk-KBJHAD2P-CRzKgU8S.js +1 -0
  15. package/dist/assets/chunk-RYQCIY6F-DBz4GTZF.js +1 -0
  16. package/dist/assets/chunk-XXDRQBXY-wHxalsXk.js +1 -0
  17. package/dist/assets/classDiagram-JCYQIIEL-Dn-3CfpR.js +1 -0
  18. package/dist/assets/classDiagram-v2-OCEON4UE-Dn-3CfpR.js +1 -0
  19. package/dist/assets/cose-bilkent-JH36ORCC-s1hygY8e.js +1 -0
  20. package/dist/assets/cynefin-VYW2F7L2-C4LGV15i.js +178 -0
  21. package/dist/assets/cynefinDiagram-MW4NZA55-C5uhu7B3.js +62 -0
  22. package/dist/assets/cytoscape.esm-C10L9Icf.js +331 -0
  23. package/dist/assets/dagre-VZM6K2ZE-B-OBTewm.js +4 -0
  24. package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  25. package/dist/assets/diagram-7IWD3JNH-BjGKQxih.js +30 -0
  26. package/dist/assets/diagram-B4RE2ZJO-BMKz1X2L.js +3 -0
  27. package/dist/assets/diagram-LBJQPF4R-DNh-GhKC.js +24 -0
  28. package/dist/assets/diagram-Q27KOJAE-BaTG9jPT.js +24 -0
  29. package/dist/assets/diagram-UB23O5K3-BUwo2OvJ.js +41 -0
  30. package/dist/assets/ebnfDiagram-BXEA7PRR-DAZgfzX2.js +1 -0
  31. package/dist/assets/erDiagram-JOGREHBK-1w1CcGEb.js +85 -0
  32. package/dist/assets/flowDiagram-UKHOOZJN-D4EII8Jj.js +156 -0
  33. package/dist/assets/ganttDiagram-PKOTCBZU-ChvJZAgB.js +292 -0
  34. package/dist/assets/gitGraphDiagram-DS77QQ5N-DtOn41S8.js +106 -0
  35. package/dist/assets/graph-Cx-EqisW.js +1 -0
  36. package/dist/assets/index-CzPLBvXT.css +32 -0
  37. package/dist/assets/{index-DTMWjJj8.js → index-DFkBBuSA.js} +303 -304
  38. package/dist/assets/infoDiagram-6WML65LV-DN_PVEJZ.js +2 -0
  39. package/dist/assets/init-Gi6I4Gst.js +1 -0
  40. package/dist/assets/ishikawaDiagram-WSZJBQD7-DbkF1kXb.js +70 -0
  41. package/dist/assets/journeyDiagram-NVQOT4AX-pJwmhddE.js +139 -0
  42. package/dist/assets/kanban-definition-27J2QSJJ-PPAPgNp5.js +89 -0
  43. package/dist/assets/layout-D6BstSzF.js +1 -0
  44. package/dist/assets/linear-iPR-J9f7.js +1 -0
  45. package/dist/assets/map-CV9uif0g.js +1 -0
  46. package/dist/assets/mermaid.core-BEUL86Z6.js +312 -0
  47. package/dist/assets/mindmap-definition-FAOFIHXS-BvZoBib7.js +96 -0
  48. package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  49. package/dist/assets/pegDiagram-VL7TDLO6-DhYreaUd.js +1 -0
  50. package/dist/assets/pieDiagram-7S7Q4E2Y-CrOHlyD0.js +39 -0
  51. package/dist/assets/quadrantDiagram-CIZ2JOQS-D4g5yH3t.js +7 -0
  52. package/dist/assets/railroadDiagram-AXF67PYL-Dj39lHlf.js +1 -0
  53. package/dist/assets/requirementDiagram-LRYGKXZP-1AS4Zgm_.js +84 -0
  54. package/dist/assets/sankeyDiagram-W5VNT64P-CKlMn19P.js +40 -0
  55. package/dist/assets/sequenceDiagram-SI44F4Z6-B_j3qyvo.js +162 -0
  56. package/dist/assets/sizeCapture-X5ZJPWSS-Bp2Op16v.js +1 -0
  57. package/dist/assets/stateDiagram-OKZ733FA-C0cN6DoY.js +1 -0
  58. package/dist/assets/stateDiagram-v2-UEYNNEHI-BvBxUUrZ.js +1 -0
  59. package/dist/assets/swimlanes-SLNWSIFB-BEvXOV9b.js +2 -0
  60. package/dist/assets/swimlanesDiagram-ULZ7WXOC-CsE-x4YK.js +8 -0
  61. package/dist/assets/timeline-definition-Z64GVDOM-zdJQinvZ.js +120 -0
  62. package/dist/assets/vennDiagram-T6HMQDX7-DuF1uZMY.js +34 -0
  63. package/dist/assets/wardleyDiagram-T6FBY63Y-B9AlLQup.js +78 -0
  64. package/dist/assets/xychartDiagram-ELKLHX3M-DTJX48Qg.js +7 -0
  65. package/dist/index.html +2 -2
  66. package/dist-server/server/index.js +1 -2
  67. package/dist-server/server/index.js.map +1 -1
  68. package/dist-server/server/modules/agent/agent.module.js +2 -1
  69. package/dist-server/server/modules/agent/agent.module.js.map +1 -1
  70. package/dist-server/server/modules/auth/auth.middleware.js +1 -1
  71. package/dist-server/server/modules/auth/auth.middleware.js.map +1 -1
  72. package/dist-server/server/modules/browser-use/browser-use-runtime.js +10 -0
  73. package/dist-server/server/modules/browser-use/browser-use-runtime.js.map +1 -0
  74. package/dist-server/server/modules/browser-use/browser-use.service.js +3 -6
  75. package/dist-server/server/modules/browser-use/browser-use.service.js.map +1 -1
  76. package/dist-server/server/modules/browser-use/index.js +2 -0
  77. package/dist-server/server/modules/browser-use/index.js.map +1 -1
  78. package/dist-server/server/modules/cli/cli.js +9 -3
  79. package/dist-server/server/modules/cli/cli.js.map +1 -1
  80. package/dist-server/server/modules/cli/cli.module.js +0 -1
  81. package/dist-server/server/modules/cli/cli.module.js.map +1 -1
  82. package/dist-server/server/modules/cli/cli.service.js +0 -18
  83. package/dist-server/server/modules/cli/cli.service.js.map +1 -1
  84. package/dist-server/server/modules/cli/tests/cli-environment-bootstrap.test.js +154 -0
  85. package/dist-server/server/modules/cli/tests/cli-environment-bootstrap.test.js.map +1 -0
  86. package/dist-server/server/modules/cli/tests/cli.service.test.js +0 -3
  87. package/dist-server/server/modules/cli/tests/cli.service.test.js.map +1 -1
  88. package/dist-server/server/modules/file-tree/file-tree.service.js +18 -10
  89. package/dist-server/server/modules/file-tree/file-tree.service.js.map +1 -1
  90. package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js +62 -11
  91. package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js.map +1 -1
  92. package/dist-server/server/modules/git/git-branch.service.js +15 -0
  93. package/dist-server/server/modules/git/git-branch.service.js.map +1 -0
  94. package/dist-server/server/modules/git/git.routes.js +15 -7
  95. package/dist-server/server/modules/git/git.routes.js.map +1 -1
  96. package/dist-server/server/modules/git/tests/git-branch.service.test.js +52 -0
  97. package/dist-server/server/modules/git/tests/git-branch.service.test.js.map +1 -0
  98. package/dist-server/server/modules/git/tests/git-init.routes.test.js +84 -0
  99. package/dist-server/server/modules/git/tests/git-init.routes.test.js.map +1 -1
  100. package/dist-server/server/modules/notifications/index.js +3 -1
  101. package/dist-server/server/modules/notifications/index.js.map +1 -1
  102. package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js +22 -1
  103. package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js.map +1 -1
  104. package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js +254 -48
  105. package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js.map +1 -1
  106. package/dist-server/server/modules/providers/provider.routes.js +5 -0
  107. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  108. package/dist-server/server/modules/providers/services/session-conversations-search.service.js +63 -7
  109. package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -1
  110. package/dist-server/server/modules/providers/tests/provider.routes.test.js +35 -0
  111. package/dist-server/server/modules/providers/tests/provider.routes.test.js.map +1 -1
  112. package/dist-server/server/shared/utils.js +8 -0
  113. package/dist-server/server/shared/utils.js.map +1 -1
  114. package/package.json +6 -2
  115. package/scripts/promote-dist-server.mjs +50 -0
  116. package/server/index.ts +1 -2
  117. package/server/modules/agent/agent.module.ts +2 -1
  118. package/server/modules/auth/auth.middleware.ts +2 -2
  119. package/server/modules/browser-use/browser-use-runtime.ts +10 -0
  120. package/server/modules/browser-use/browser-use.service.ts +5 -8
  121. package/server/modules/browser-use/index.ts +3 -0
  122. package/server/modules/cli/cli.module.ts +0 -1
  123. package/server/modules/cli/cli.service.ts +0 -20
  124. package/server/modules/cli/cli.ts +9 -3
  125. package/server/modules/cli/tests/cli-environment-bootstrap.test.ts +186 -0
  126. package/server/modules/cli/tests/cli.service.test.ts +0 -3
  127. package/server/modules/file-tree/file-tree.service.ts +21 -10
  128. package/server/modules/file-tree/tests/file-tree.service.test.ts +65 -11
  129. package/server/modules/git/git-branch.service.ts +43 -0
  130. package/server/modules/git/git.routes.ts +16 -9
  131. package/server/modules/git/tests/git-branch.service.test.ts +60 -0
  132. package/server/modules/git/tests/git-init.routes.test.ts +83 -0
  133. package/server/modules/notifications/index.ts +2 -0
  134. package/server/modules/notifications/services/notification-orchestrator.service.js +24 -1
  135. package/server/modules/providers/list/claude/claude-runtime.provider.js +275 -54
  136. package/server/modules/providers/provider.routes.ts +5 -0
  137. package/server/modules/providers/services/session-conversations-search.service.ts +94 -7
  138. package/server/modules/providers/tests/provider.routes.test.ts +61 -0
  139. package/server/shared/types.ts +0 -1
  140. package/server/shared/utils.ts +10 -0
  141. package/server/tsconfig.json +6 -2
  142. package/README.de.md +0 -257
  143. package/README.ja.md +0 -249
  144. package/README.ko.md +0 -249
  145. package/README.md +0 -269
  146. package/README.ru.md +0 -257
  147. package/README.tr.md +0 -258
  148. package/README.zh-CN.md +0 -249
  149. package/README.zh-TW.md +0 -249
  150. package/dist/assets/index-D3jfzuKN.css +0 -32
  151. package/dist-server/server/constants/config.js +0 -6
  152. package/dist-server/server/constants/config.js.map +0 -1
  153. package/server/constants/config.ts +0 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudcli-ai/cloudcli",
3
- "version": "1.37.1",
3
+ "version": "1.37.2",
4
4
  "productName": "CloudCLI",
5
5
  "description": "A web-based UI for Claude Code CLI",
6
6
  "type": "module",
@@ -28,6 +28,7 @@
28
28
  },
29
29
  "scripts": {
30
30
  "dev": "concurrently --kill-others \"npm run server:dev\" \"npm run client\"",
31
+ "preserver": "node scripts/promote-dist-server.mjs recover",
31
32
  "server": "node dist-server/server/index.js",
32
33
  "server:dev": "tsx --tsconfig server/tsconfig.json server/index.ts",
33
34
  "server:dev-watch": "tsx watch --tsconfig server/tsconfig.json server/index.ts",
@@ -42,11 +43,13 @@
42
43
  "desktop:icon:mac": "node electron/scripts/generate-macos-icon.js",
43
44
  "build": "npm run build:client && npm run build:server",
44
45
  "build:client": "vite build",
45
- "prebuild:server": "node -e \"require('node:fs').rmSync('dist-server', { recursive: true, force: true })\"",
46
+ "prebuild:server": "node -e \"require('node:fs').rmSync('dist-server.next', { recursive: true, force: true })\"",
46
47
  "build:server": "tsc -p server/tsconfig.json && tsc-alias -p server/tsconfig.json",
48
+ "postbuild:server": "node scripts/promote-dist-server.mjs",
47
49
  "preview": "vite preview",
48
50
  "typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p server/tsconfig.json",
49
51
  "test": "tsx --tsconfig server/tsconfig.json --test \"server/**/*.test.ts\" \"server/**/*.test.js\"",
52
+ "test:client": "tsx --test \"src/**/*.test.ts\" \"src/**/*.test.tsx\"",
50
53
  "lint": "eslint src/ server/",
51
54
  "lint:fix": "eslint src/ server/ --fix",
52
55
  "start": "npm run build && npm run server",
@@ -175,6 +178,7 @@
175
178
  "jszip": "^3.10.1",
176
179
  "katex": "^0.16.25",
177
180
  "lucide-react": "^0.515.0",
181
+ "mermaid": "^11.16.1",
178
182
  "mime-types": "^3.0.1",
179
183
  "multer": "^2.0.1",
180
184
  "node-fetch": "^2.7.0",
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+ // The server build stages into dist-server.next and this script promotes it
3
+ // into place. The live dist-server must never be deleted before the new build
4
+ // is complete: an interrupted build would otherwise leave the installation
5
+ // without a server entrypoint, crash-looping on MODULE_NOT_FOUND at every
6
+ // start until someone rebuilds it by hand.
7
+ import fs from 'node:fs';
8
+
9
+ const NEXT = 'dist-server.next';
10
+ const LIVE = 'dist-server';
11
+ const OLD = 'dist-server.old';
12
+ const ENTRY = 'server/index.js';
13
+
14
+ const mode = process.argv[2] ?? 'promote';
15
+
16
+ if (mode === 'recover') {
17
+ // Ran before every server start: if a promotion was interrupted between the
18
+ // two renames, put the previous build back so the server can boot.
19
+ if (!fs.existsSync(`${LIVE}/${ENTRY}`) && fs.existsSync(`${OLD}/${ENTRY}`)) {
20
+ console.error('promote-dist-server: restoring previous dist-server after an interrupted promotion.');
21
+ fs.rmSync(LIVE, { recursive: true, force: true });
22
+ fs.renameSync(OLD, LIVE);
23
+ }
24
+ process.exit(0);
25
+ }
26
+
27
+ if (mode !== 'promote') {
28
+ console.error(`promote-dist-server: unknown mode "${mode}" (expected "promote" or "recover").`);
29
+ process.exit(64);
30
+ }
31
+
32
+ if (!fs.existsSync(`${NEXT}/${ENTRY}`)) {
33
+ console.error(`promote-dist-server: ${NEXT}/${ENTRY} is missing; leaving the current ${LIVE} untouched.`);
34
+ process.exit(1);
35
+ }
36
+
37
+ fs.rmSync(OLD, { recursive: true, force: true });
38
+ if (fs.existsSync(LIVE)) {
39
+ fs.renameSync(LIVE, OLD);
40
+ }
41
+ try {
42
+ fs.renameSync(NEXT, LIVE);
43
+ } catch (error) {
44
+ // Put the previous build back before failing so the installation still boots.
45
+ if (fs.existsSync(OLD) && !fs.existsSync(LIVE)) {
46
+ fs.renameSync(OLD, LIVE);
47
+ }
48
+ throw error;
49
+ }
50
+ fs.rmSync(OLD, { recursive: true, force: true });
package/server/index.ts CHANGED
@@ -9,7 +9,7 @@ import http from 'http';
9
9
  import express, { type NextFunction, type Request, type Response } from 'express';
10
10
  import cors from 'cors';
11
11
 
12
- import { AppError, findApplicationRoot, getModuleDirectory, terminalTextStyles } from '@/shared/utils.js';
12
+ import { AppError, findApplicationRoot, getModuleDirectory, IS_PLATFORM, terminalTextStyles } from '@/shared/utils.js';
13
13
  import {
14
14
  closeSessionsWatcher,
15
15
  initializeSessionsWatcher,
@@ -50,7 +50,6 @@ import browserUseMcpRoutes from './modules/browser-use/browser-use-mcp.routes.js
50
50
  import { browserUseService } from './modules/browser-use/browser-use.service.js';
51
51
  import { initializeDatabase, sessionsDb } from './modules/database/index.js';
52
52
  import { configureWebPush } from './modules/notifications/index.js';
53
- import { IS_PLATFORM } from './constants/config.js';
54
53
 
55
54
  const __dirname = getModuleDirectory(import.meta.url);
56
55
  // The server source runs from /server, while the compiled output runs from /dist-server/server.
@@ -12,6 +12,7 @@ import {
12
12
  userDb,
13
13
  } from '@/modules/database/index.js';
14
14
  import { providerModelsService } from '@/modules/providers/index.js';
15
+ import { IS_PLATFORM } from '@/shared/utils.js';
15
16
 
16
17
  import { createAgentRouter } from './agent.routes.js';
17
18
 
@@ -30,7 +31,7 @@ export function createAgentModule(externalDependencies: AgentExternalDependencie
30
31
  crypto,
31
32
  homeDirectory: os.homedir,
32
33
  spawnProcess: spawn,
33
- platformMode: process.env.VITE_IS_PLATFORM === 'true',
34
+ platformMode: IS_PLATFORM,
34
35
  users: {
35
36
  getFirstUser: () => userDb.getFirstUser(),
36
37
  },
@@ -1,9 +1,9 @@
1
1
  // @ts-nocheck -- JWT request augmentation is narrowed by Auth route contracts.
2
2
  import jwt from 'jsonwebtoken';
3
3
 
4
- import { userDb, appConfigDb } from '../database/index.js';
4
+ import { IS_PLATFORM } from '@/shared/utils.js';
5
5
 
6
- const IS_PLATFORM = process.env.VITE_IS_PLATFORM === 'true';
6
+ import { userDb, appConfigDb } from '../database/index.js';
7
7
 
8
8
  // Use env var if set, otherwise auto-generate a unique secret per installation
9
9
  const JWT_SECRET = process.env.JWT_SECRET || appConfigDb.getOrCreateJwtSecret();
@@ -0,0 +1,10 @@
1
+ import { IS_PLATFORM } from '@/shared/utils.js';
2
+
3
+ /**
4
+ * Resolves the runtime label exposed by Browser Use status and session APIs.
5
+ * The Browser Use service and CLI environment-mode regression coverage consume
6
+ * this after the server or CLI entrypoint has completed environment bootstrapping.
7
+ */
8
+ export function getBrowserUseRuntime(): 'cloud' | 'local' {
9
+ return IS_PLATFORM ? 'cloud' : 'local';
10
+ }
@@ -11,15 +11,16 @@ import { appConfigDb } from '@/modules/database/index.js';
11
11
  import { providerMcpService } from '@/modules/providers/index.js';
12
12
  import { getModuleDirectory } from '@/shared/utils.js';
13
13
 
14
+ import { getBrowserUseRuntime } from './browser-use-runtime.js';
15
+
14
16
  const require = createRequire(import.meta.url);
15
17
  const __dirname = getModuleDirectory(import.meta.url);
16
- const IS_PLATFORM = process.env.VITE_IS_PLATFORM === 'true';
17
18
  const MAX_SESSIONS_PER_OWNER = Number.parseInt(process.env.CLOUDCLI_BROWSER_USE_MAX_SESSIONS_PER_OWNER || '3', 10);
18
19
  const SESSION_TTL_MS = Number.parseInt(process.env.CLOUDCLI_BROWSER_USE_SESSION_TTL_MS || String(30 * 60 * 1000), 10);
19
20
  const BROWSER_USE_SETTINGS_KEY = 'browser_use_settings';
20
21
  const BROWSER_USE_MCP_TOKEN_KEY = 'browser_use_mcp_token';
21
22
 
22
- type BrowserUseRuntime = 'cloud' | 'local';
23
+ type BrowserUseRuntime = ReturnType<typeof getBrowserUseRuntime>;
23
24
  type BrowserUseSessionStatus = 'ready' | 'stopped' | 'unavailable';
24
25
 
25
26
  type BrowserUseSession = {
@@ -85,10 +86,6 @@ const MCP_SERVER_NAME = 'cloudcli-browser';
85
86
  const LEGACY_MCP_SERVER_NAMES = ['cloudcli-browser-use'];
86
87
  const RUNTIME_READINESS_CACHE_TTL_MS = 30_000;
87
88
 
88
- function getRuntime(): BrowserUseRuntime {
89
- return IS_PLATFORM ? 'cloud' : 'local';
90
- }
91
-
92
89
  function readSettings(): BrowserUseSettings {
93
90
  try {
94
91
  const raw = appConfigDb.get(BROWSER_USE_SETTINGS_KEY);
@@ -446,7 +443,7 @@ export const browserUseService = {
446
443
 
447
444
  return {
448
445
  enabled: settings.enabled,
449
- runtime: getRuntime(),
446
+ runtime: getBrowserUseRuntime(),
450
447
  available,
451
448
  playwrightInstalled: readiness.playwrightInstalled,
452
449
  chromiumInstalled: readiness.chromiumInstalled,
@@ -515,7 +512,7 @@ export const browserUseService = {
515
512
  id: randomUUID(),
516
513
  ownerId: AGENT_OWNER_ID,
517
514
  createdBy: 'agent',
518
- runtime: getRuntime(),
515
+ runtime: getBrowserUseRuntime(),
519
516
  status: 'unavailable',
520
517
  url: null,
521
518
  title: null,
@@ -6,3 +6,6 @@
6
6
  export async function startBrowserUseMcp(): Promise<void> {
7
7
  await import('./browser-use-mcp.js');
8
8
  }
9
+
10
+ // getBrowserUseRuntime: used by Browser Use services and environment-mode regression coverage.
11
+ export { getBrowserUseRuntime } from './browser-use-runtime.js';
@@ -28,7 +28,6 @@ export function createCliApplication(): CliApplication {
28
28
  bugsUrl: packageMetadataJson.bugs?.url,
29
29
  };
30
30
  const fileSystem = {
31
- readTextFile: (filePath: string) => fs.readFileSync(filePath, 'utf8'),
32
31
  pathExists: (filePath: string) => fs.existsSync(filePath),
33
32
  getFileStats: (filePath: string) => {
34
33
  const stats = fs.statSync(filePath);
@@ -77,27 +77,7 @@ function isNewerVersion(candidateVersion: string, currentVersion: string): boole
77
77
  return false;
78
78
  }
79
79
 
80
- function loadEnvironmentFile(dependencies: CliServiceDependencies): void {
81
- try {
82
- const environmentFile = dependencies.fileSystem.readTextFile(
83
- path.join(dependencies.applicationRoot, '.env'),
84
- );
85
- for (const line of environmentFile.split('\n')) {
86
- const trimmedLine = line.trim();
87
- if (!trimmedLine || trimmedLine.startsWith('#')) continue;
88
-
89
- const [key, ...valueParts] = trimmedLine.split('=');
90
- if (key && valueParts.length > 0 && !dependencies.environment[key]) {
91
- dependencies.environment[key] = valueParts.join('=').trim();
92
- }
93
- }
94
- } catch {
95
- // The repository-level .env file is optional for every CLI command.
96
- }
97
- }
98
-
99
80
  function showStatus(dependencies: CliServiceDependencies): void {
100
- loadEnvironmentFile(dependencies);
101
81
  const { environment, fileSystem, output } = dependencies;
102
82
  const databasePath = environment.DATABASE_PATH || dependencies.defaultDatabasePath;
103
83
  const databaseExists = fileSystem.pathExists(databasePath);
@@ -1,10 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { createCliApplication } from './index.js';
3
+ // The CLI can reach shared environment constants through its composition root,
4
+ // so finish the root environment bootstrap before loading that import graph.
5
+ // eslint-disable-next-line boundaries/no-unknown
6
+ import '../../load-env.js';
4
7
 
5
- const cliApplication = createCliApplication();
8
+ async function runCli(): Promise<number> {
9
+ const { createCliApplication } = await import('./index.js');
10
+ return createCliApplication().run(process.argv.slice(2));
11
+ }
6
12
 
7
- cliApplication.run(process.argv.slice(2)).then((exitCode) => {
13
+ runCli().then((exitCode) => {
8
14
  process.exitCode = exitCode;
9
15
  }).catch((error: unknown) => {
10
16
  const message = error instanceof Error ? error.message : String(error);
@@ -0,0 +1,186 @@
1
+ import assert from 'node:assert/strict';
2
+ import { execFileSync } from 'node:child_process';
3
+ import { copyFile, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import test from 'node:test';
7
+ import { fileURLToPath, pathToFileURL } from 'node:url';
8
+
9
+ type BootstrapResult = {
10
+ command: string | null;
11
+ environmentFileReads: number;
12
+ mode: string | null;
13
+ authNextCalled: boolean;
14
+ authStatus: number;
15
+ authenticatedUsername: string | null;
16
+ runtime: string;
17
+ };
18
+
19
+ const RESULT_PREFIX = '__CLOUDCLI_BOOTSTRAP_RESULT__';
20
+ const testDirectory = path.dirname(fileURLToPath(import.meta.url));
21
+ const applicationRoot = path.resolve(testDirectory, '..', '..', '..', '..');
22
+
23
+ function sourceModuleUrl(...segments: string[]): string {
24
+ return pathToFileURL(path.join(applicationRoot, ...segments)).href;
25
+ }
26
+
27
+ async function runCliBootstrapFixture(isPlatform: boolean): Promise<BootstrapResult> {
28
+ const fixtureRoot = await mkdtemp(path.join(os.tmpdir(), 'cloudcli-cli-bootstrap-'));
29
+ const fixtureServerDirectory = path.join(fixtureRoot, 'server');
30
+ const fixtureCliDirectory = path.join(fixtureServerDirectory, 'modules', 'cli');
31
+ const fixtureDatabasePath = path.join(fixtureRoot, 'auth.db');
32
+
33
+ try {
34
+ await mkdir(fixtureCliDirectory, { recursive: true });
35
+ await Promise.all([
36
+ copyFile(
37
+ path.join(applicationRoot, 'server', 'load-env.ts'),
38
+ path.join(fixtureServerDirectory, 'load-env.ts'),
39
+ ),
40
+ copyFile(
41
+ path.join(applicationRoot, 'server', 'modules', 'cli', 'cli.ts'),
42
+ path.join(fixtureCliDirectory, 'cli.ts'),
43
+ ),
44
+ ]);
45
+ await writeFile(
46
+ path.join(fixtureRoot, '.env'),
47
+ [
48
+ `VITE_IS_PLATFORM=${String(isPlatform)}`,
49
+ `DATABASE_PATH=${fixtureDatabasePath.replace(/\\/g, '/')}`,
50
+ ].join('\n'),
51
+ 'utf8',
52
+ );
53
+
54
+ const fixtureIndexSource = `
55
+ import { createCliService } from ${JSON.stringify(sourceModuleUrl('server', 'modules', 'cli', 'cli.service.ts'))};
56
+ import { authenticateToken } from ${JSON.stringify(sourceModuleUrl('server', 'modules', 'auth', 'index.ts'))};
57
+ import { getBrowserUseRuntime } from ${JSON.stringify(sourceModuleUrl('server', 'modules', 'browser-use', 'index.ts'))};
58
+ import { closeConnection, initializeDatabase, userDb } from ${JSON.stringify(sourceModuleUrl('server', 'modules', 'database', 'index.ts'))};
59
+
60
+ let environmentFileReads = 0;
61
+
62
+ export function createCliApplication() {
63
+ const service = createCliService({
64
+ applicationRoot: ${JSON.stringify(fixtureRoot)},
65
+ defaultDatabasePath: ${JSON.stringify(fixtureDatabasePath)},
66
+ homeDirectory: ${JSON.stringify(fixtureRoot)},
67
+ packageMetadata: { version: '1.0.0' },
68
+ environment: process.env,
69
+ fileSystem: {
70
+ readTextFile: () => {
71
+ environmentFileReads += 1;
72
+ return '';
73
+ },
74
+ pathExists: () => false,
75
+ getFileStats: () => ({ size: 0, modifiedAt: new Date(0) }),
76
+ },
77
+ output: { log: () => undefined, error: () => undefined },
78
+ sandboxService: { execute: async () => 0 },
79
+ getLatestPackageVersion: async () => '1.0.0',
80
+ updateGlobalPackage: () => undefined,
81
+ startServer: async () => undefined,
82
+ startBrowserUseMcp: async () => undefined,
83
+ });
84
+
85
+ return {
86
+ async run(argumentsList: string[]) {
87
+ const exitCode = await service.run(argumentsList);
88
+ await initializeDatabase();
89
+ userDb.createUser('bootstrap-user', 'password-hash');
90
+
91
+ const request: any = { headers: {}, query: {} };
92
+ const response: any = {
93
+ statusCode: 200,
94
+ body: null,
95
+ headers: {},
96
+ setHeader(name: string, value: string) {
97
+ this.headers[name] = value;
98
+ },
99
+ status(statusCode: number) {
100
+ this.statusCode = statusCode;
101
+ return this;
102
+ },
103
+ json(body: unknown) {
104
+ this.body = body;
105
+ return this;
106
+ },
107
+ };
108
+ let authNextCalled = false;
109
+ await authenticateToken(request, response, () => {
110
+ authNextCalled = true;
111
+ });
112
+ const runtime = getBrowserUseRuntime();
113
+
114
+ console.log(${JSON.stringify(RESULT_PREFIX)} + JSON.stringify({
115
+ command: argumentsList[0] ?? null,
116
+ environmentFileReads,
117
+ mode: process.env.VITE_IS_PLATFORM ?? null,
118
+ authNextCalled,
119
+ authStatus: response.statusCode,
120
+ authenticatedUsername: request.user?.username ?? null,
121
+ runtime,
122
+ }));
123
+ closeConnection();
124
+ return exitCode;
125
+ },
126
+ };
127
+ }
128
+ `;
129
+ await writeFile(path.join(fixtureCliDirectory, 'index.ts'), fixtureIndexSource, 'utf8');
130
+
131
+ const childEnvironment = { ...process.env };
132
+ delete childEnvironment.VITE_IS_PLATFORM;
133
+ delete childEnvironment.DATABASE_PATH;
134
+ const stdout = execFileSync(
135
+ process.execPath,
136
+ [
137
+ path.join(applicationRoot, 'node_modules', 'tsx', 'dist', 'cli.mjs'),
138
+ '--tsconfig',
139
+ path.join(applicationRoot, 'server', 'tsconfig.json'),
140
+ path.join(fixtureCliDirectory, 'cli.ts'),
141
+ 'status',
142
+ ],
143
+ {
144
+ cwd: applicationRoot,
145
+ encoding: 'utf8',
146
+ env: childEnvironment,
147
+ timeout: 30_000,
148
+ },
149
+ );
150
+ const resultLine = stdout
151
+ .split(/\r?\n/)
152
+ .find((line) => line.startsWith(RESULT_PREFIX));
153
+ assert.ok(resultLine, `Missing bootstrap result in child output:\n${stdout}`);
154
+ return JSON.parse(resultLine.slice(RESULT_PREFIX.length)) as BootstrapResult;
155
+ } finally {
156
+ await rm(fixtureRoot, { recursive: true, force: true });
157
+ }
158
+ }
159
+
160
+ test('CLI bootstrap selects Platform authentication and cloud runtime from .env', async () => {
161
+ const result = await runCliBootstrapFixture(true);
162
+
163
+ assert.deepEqual(result, {
164
+ command: 'status',
165
+ environmentFileReads: 0,
166
+ mode: 'true',
167
+ authNextCalled: true,
168
+ authStatus: 200,
169
+ authenticatedUsername: 'bootstrap-user',
170
+ runtime: 'cloud',
171
+ });
172
+ });
173
+
174
+ test('CLI bootstrap selects OSS authentication and local runtime from .env', async () => {
175
+ const result = await runCliBootstrapFixture(false);
176
+
177
+ assert.deepEqual(result, {
178
+ command: 'status',
179
+ environmentFileReads: 0,
180
+ mode: 'false',
181
+ authNextCalled: false,
182
+ authStatus: 401,
183
+ authenticatedUsername: null,
184
+ runtime: 'local',
185
+ });
186
+ });
@@ -26,9 +26,6 @@ function createHarness() {
26
26
  },
27
27
  environment,
28
28
  fileSystem: {
29
- readTextFile: () => {
30
- throw new Error('missing');
31
- },
32
29
  pathExists: () => false,
33
30
  getFileStats: () => ({ size: 0, modifiedAt: new Date(0) }),
34
31
  },
@@ -11,9 +11,13 @@ import type {
11
11
  } from '@/shared/types.js';
12
12
  import { AppError, FORBIDDEN_WORKSPACE_PATHS, normalizeProjectPath } from '@/shared/utils.js';
13
13
 
14
+ const HARD_EXCLUDED_DIRECTORY_NAMES = new Set([
15
+ 'node_modules', '.git', '.svn', '.hg',
16
+ ]);
17
+
14
18
  const IGNORED_DIRECTORY_NAMES = new Set([
15
- 'node_modules', 'dist', 'build', '.next', '.nuxt', '.cache', '.parcel-cache',
16
- '.git', '.svn', '.hg',
19
+ ...HARD_EXCLUDED_DIRECTORY_NAMES,
20
+ 'dist', 'build', '.next', '.nuxt', '.cache', '.parcel-cache',
17
21
  '__pycache__', '.pytest_cache', '.mypy_cache', '.tox', 'venv', '.venv',
18
22
  'target', 'vendor',
19
23
  '.gradle', '.idea', 'coverage', '.nyc_output',
@@ -36,6 +40,15 @@ const MAXIMUM_FILE_TREE_ENTRIES = 10_000;
36
40
 
37
41
  type FileTreeEntryFilter = (entryPath: string, isDirectory: boolean) => boolean;
38
42
 
43
+ function includeEntryByHardExclusions(entryPath: string, isDirectory: boolean): boolean {
44
+ return !isDirectory || !HARD_EXCLUDED_DIRECTORY_NAMES.has(path.basename(entryPath));
45
+ }
46
+
47
+ function includeEntryByFallbackDirectoryNames(entryPath: string, isDirectory: boolean): boolean {
48
+ return includeEntryByHardExclusions(entryPath, isDirectory)
49
+ && (!isDirectory || !IGNORED_DIRECTORY_NAMES.has(path.basename(entryPath)));
50
+ }
51
+
39
52
  function createFileTreeError(message: string, statusCode: number, code: string): AppError {
40
53
  return new AppError(message, { statusCode, code });
41
54
  }
@@ -146,6 +159,8 @@ function createGitignoreEntryFilter(
146
159
  const gitignore = ignore().add(gitignoreContent);
147
160
 
148
161
  return (entryPath, isDirectory) => {
162
+ if (!includeEntryByHardExclusions(entryPath, isDirectory)) return false;
163
+
149
164
  const relativePath = path.relative(projectRoot, entryPath).split(path.sep).join('/');
150
165
  const matchPath = isDirectory ? `${relativePath}/` : relativePath;
151
166
  return !gitignore.ignores(matchPath);
@@ -191,17 +206,13 @@ export function createFileTreeService(dependencies: FileTreeServiceDependencies)
191
206
  try {
192
207
  for await (const entry of fileSystem.openDirectory(directoryPath)) {
193
208
  const isDirectory = entry.isDirectory();
194
- if (isDirectory && IGNORED_DIRECTORY_NAMES.has(entry.name)) {
195
- continue;
196
- }
197
209
  if (!includeEntry(path.join(directoryPath, entry.name), isDirectory)) {
198
210
  continue;
199
211
  }
200
212
 
201
- // Charged after ignore filtering so generated directories and
202
- // .gitignore exclusions never consume the budget. Every recursive
203
- // branch shares one counter, so the cap applies to the whole tree
204
- // rather than per directory.
213
+ // Charged after visibility filtering so ignored entries never consume
214
+ // the budget. Every recursive branch shares one counter, so the cap
215
+ // applies to the whole tree rather than per directory.
205
216
  if (remainingEntries.value <= 0) {
206
217
  throw createFileTreeError(
207
218
  `Project file tree exceeds the ${MAXIMUM_FILE_TREE_ENTRIES.toLocaleString()} entry limit. Choose a narrower project directory or add ignore rules.`,
@@ -233,7 +244,7 @@ export function createFileTreeService(dependencies: FileTreeServiceDependencies)
233
244
  directoryPath: string,
234
245
  maximumDepth: number,
235
246
  currentDepth = 0,
236
- includeEntry: FileTreeEntryFilter = () => true,
247
+ includeEntry: FileTreeEntryFilter = includeEntryByFallbackDirectoryNames,
237
248
  remainingEntries = { value: MAXIMUM_FILE_TREE_ENTRIES },
238
249
  ): Promise<FileTreeNode[]> {
239
250
  const visibleEntries = await collectVisibleEntries(directoryPath, includeEntry, remainingEntries);
@@ -87,37 +87,67 @@ function createDependencies(
87
87
  };
88
88
  }
89
89
 
90
- test('listProjectFiles builds a sorted tree and skips generated directories', async () => {
90
+ test('listProjectFiles applies gitignore alongside hard directory exclusions', async () => {
91
91
  const projectRoot = path.resolve('file-tree-test-project');
92
+ const documentationDirectory = path.join(projectRoot, 'docs');
93
+ const buildDocumentationDirectory = path.join(documentationDirectory, 'build');
94
+ const gitDirectory = path.join(projectRoot, '.git');
95
+ const nodeModulesDirectory = path.join(projectRoot, 'node_modules');
92
96
  const sourceDirectory = path.join(projectRoot, 'src');
97
+ const readDirectories: string[] = [];
93
98
  const fileSystem = createFakeFileSystem({
94
99
  access: async () => undefined,
100
+ readTextFile: async (filePath) => {
101
+ assert.equal(filePath, path.join(projectRoot, '.gitignore'));
102
+ return '*.log';
103
+ },
95
104
  openDirectory: createDirectoryReader((directoryPath) => {
105
+ readDirectories.push(directoryPath);
96
106
  if (directoryPath === projectRoot) {
97
107
  return [
108
+ createDirectoryEntry('.git', true),
98
109
  createDirectoryEntry('node_modules', true),
99
110
  createDirectoryEntry('README.md', false),
111
+ createDirectoryEntry('docs', true),
100
112
  createDirectoryEntry('src', true),
101
113
  ];
102
114
  }
115
+ if (directoryPath === documentationDirectory) {
116
+ return [createDirectoryEntry('build', true)];
117
+ }
118
+ if (directoryPath === buildDocumentationDirectory) {
119
+ return [createDirectoryEntry('foo.md', false)];
120
+ }
103
121
  if (directoryPath === sourceDirectory) {
104
122
  return [createDirectoryEntry('index.ts', false)];
105
123
  }
106
124
  return [];
107
125
  }),
108
- lstat: async (candidatePath) => createStats(candidatePath === sourceDirectory, 0o754),
126
+ lstat: async (candidatePath) => createStats(
127
+ candidatePath === documentationDirectory
128
+ || candidatePath === buildDocumentationDirectory
129
+ || candidatePath === sourceDirectory
130
+ || candidatePath === gitDirectory
131
+ || candidatePath === nodeModulesDirectory,
132
+ 0o754,
133
+ ),
109
134
  });
110
135
  const service = createFileTreeService(createDependencies(fileSystem, projectRoot));
111
136
 
112
- const tree = await service.listProjectFiles('project-1');
137
+ const tree = await service.listProjectFiles('project-1', { respectGitignore: true });
113
138
 
114
- assert.deepEqual(tree.map((entry) => entry.name), ['src', 'README.md']);
115
- const sourceEntry = tree[0];
139
+ assert.deepEqual(tree.map((entry) => entry.name), ['docs', 'src', 'README.md']);
140
+ const documentationEntry = tree[0];
141
+ assert.deepEqual(documentationEntry?.children?.map((entry) => entry.name), ['build']);
142
+ assert.deepEqual(documentationEntry?.children?.[0]?.children?.map((entry) => entry.name), ['foo.md']);
143
+ const sourceEntry = tree[1];
116
144
  assert.ok(sourceEntry);
117
145
  assert.equal(sourceEntry.type, 'directory');
118
146
  assert.equal(sourceEntry.permissions, '754');
119
147
  assert.equal(sourceEntry.permissionsRwx, 'rwxr-xr--');
120
148
  assert.deepEqual(sourceEntry.children?.map((entry) => entry.name), ['index.ts']);
149
+ assert.equal(readDirectories.includes(gitDirectory), false);
150
+ assert.equal(readDirectories.includes(nodeModulesDirectory), false);
121
151
  });
122
152
 
123
153
  test('listProjectFiles excludes gitignored entries only when requested', async () => {
@@ -167,23 +197,47 @@ test('listProjectFiles excludes gitignored entries only when requested', async (
167
197
  assert.equal(readDirectories.includes(cacheDirectory), false);
168
198
  });
169
199
 
170
- test('listProjectFiles returns the normal tree when no gitignore exists', async () => {
200
+ test('listProjectFiles falls back to conventional directory names when no gitignore exists', async () => {
171
201
  const projectRoot = path.resolve('file-tree-test-project');
202
+ const documentationDirectory = path.join(projectRoot, 'docs');
203
+ const buildDocumentationDirectory = path.join(documentationDirectory, 'build');
204
+ const nodeModulesDirectory = path.join(projectRoot, 'node_modules');
205
+ const readDirectories: string[] = [];
172
206
  const fileSystem = createFakeFileSystem({
173
207
  access: async () => undefined,
174
208
  readTextFile: async () => {
175
209
  throw Object.assign(new Error('missing'), { code: 'ENOENT' });
176
210
  },
177
- openDirectory: createDirectoryReader((directoryPath) => directoryPath === projectRoot
178
- ? [createDirectoryEntry('debug.log', false)]
179
- : []),
180
- lstat: async () => createStats(false, 0o644),
211
+ openDirectory: createDirectoryReader((directoryPath) => {
212
+ readDirectories.push(directoryPath);
213
+ if (directoryPath === projectRoot) {
214
+ return [
215
+ createDirectoryEntry('debug.log', false),
216
+ createDirectoryEntry('docs', true),
217
+ createDirectoryEntry('node_modules', true),
218
+ ];
219
+ }
220
+ if (directoryPath === documentationDirectory) {
221
+ return [
222
+ createDirectoryEntry('build', true),
223
+ createDirectoryEntry('guide.md', false),
224
+ ];
225
+ }
226
+ if (directoryPath === buildDocumentationDirectory) {
227
+ return [createDirectoryEntry('generated.md', false)];
228
+ }
229
+ return [];
230
+ }),
231
+ lstat: async (candidatePath) => createStats(candidatePath === documentationDirectory, 0o644),
181
232
  });
182
233
  const service = createFileTreeService(createDependencies(fileSystem, projectRoot));
183
234
 
184
235
  const tree = await service.listProjectFiles('project-1', { respectGitignore: true });
185
236
 
186
- assert.deepEqual(tree.map((entry) => entry.name), ['debug.log']);
237
+ assert.deepEqual(tree.map((entry) => entry.name), ['docs', 'debug.log']);
238
+ assert.deepEqual(tree[0]?.children?.map((entry) => entry.name), ['guide.md']);
239
+ assert.equal(readDirectories.includes(nodeModulesDirectory), false);
240
+ assert.equal(readDirectories.includes(buildDocumentationDirectory), false);
187
241
  });
188
242
 
189
243
  test('listProjectFiles rejects a tree that exceeds the server entry limit', async () => {