@cliangdev/flux-plugin 0.0.0-dev.cbdf207 → 0.0.0-dev.df3e9bb

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 (86) hide show
  1. package/README.md +8 -4
  2. package/bin/install.cjs +150 -16
  3. package/package.json +7 -11
  4. package/src/__tests__/version.test.ts +37 -0
  5. package/src/adapters/local/.gitkeep +0 -0
  6. package/src/server/__tests__/config.test.ts +163 -0
  7. package/src/server/adapters/__tests__/a-client-linear.test.ts +197 -0
  8. package/src/server/adapters/__tests__/adapter-factory.test.ts +230 -0
  9. package/src/server/adapters/__tests__/dependency-ops.test.ts +395 -0
  10. package/src/server/adapters/__tests__/document-ops.test.ts +306 -0
  11. package/src/server/adapters/__tests__/linear-adapter.test.ts +91 -0
  12. package/src/server/adapters/__tests__/linear-config.test.ts +425 -0
  13. package/src/server/adapters/__tests__/linear-criteria-parser.test.ts +287 -0
  14. package/src/server/adapters/__tests__/linear-description-test.ts +238 -0
  15. package/src/server/adapters/__tests__/linear-epic-crud.test.ts +496 -0
  16. package/src/server/adapters/__tests__/linear-mappers-description.test.ts +276 -0
  17. package/src/server/adapters/__tests__/linear-mappers-epic.test.ts +294 -0
  18. package/src/server/adapters/__tests__/linear-mappers-prd.test.ts +300 -0
  19. package/src/server/adapters/__tests__/linear-mappers-task.test.ts +197 -0
  20. package/src/server/adapters/__tests__/linear-prd-crud.test.ts +620 -0
  21. package/src/server/adapters/__tests__/linear-stats.test.ts +450 -0
  22. package/src/server/adapters/__tests__/linear-task-crud.test.ts +534 -0
  23. package/src/server/adapters/__tests__/linear-types.test.ts +243 -0
  24. package/src/server/adapters/__tests__/status-ops.test.ts +441 -0
  25. package/src/server/adapters/factory.ts +90 -0
  26. package/src/server/adapters/index.ts +9 -0
  27. package/src/server/adapters/linear/adapter.ts +1136 -0
  28. package/src/server/adapters/linear/client.ts +169 -0
  29. package/src/server/adapters/linear/config.ts +152 -0
  30. package/src/server/adapters/linear/helpers/criteria-parser.ts +197 -0
  31. package/src/server/adapters/linear/helpers/index.ts +7 -0
  32. package/src/server/adapters/linear/index.ts +16 -0
  33. package/src/server/adapters/linear/mappers/description.ts +136 -0
  34. package/src/server/adapters/linear/mappers/epic.ts +81 -0
  35. package/src/server/adapters/linear/mappers/index.ts +27 -0
  36. package/src/server/adapters/linear/mappers/prd.ts +178 -0
  37. package/src/server/adapters/linear/mappers/task.ts +82 -0
  38. package/src/server/adapters/linear/types.ts +264 -0
  39. package/src/server/adapters/local-adapter.ts +968 -0
  40. package/src/server/adapters/types.ts +293 -0
  41. package/src/server/config.ts +73 -0
  42. package/src/server/db/__tests__/queries.test.ts +472 -0
  43. package/src/server/db/ids.ts +17 -0
  44. package/src/server/db/index.ts +69 -0
  45. package/src/server/db/queries.ts +142 -0
  46. package/src/server/db/refs.ts +60 -0
  47. package/src/server/db/schema.ts +88 -0
  48. package/src/server/db/sqlite.ts +10 -0
  49. package/src/server/index.ts +83 -0
  50. package/src/server/tools/__tests__/crud.test.ts +301 -0
  51. package/src/server/tools/__tests__/get-version.test.ts +27 -0
  52. package/src/server/tools/__tests__/mcp-interface.test.ts +388 -0
  53. package/src/server/tools/__tests__/query.test.ts +353 -0
  54. package/src/server/tools/__tests__/z-configure-linear.test.ts +511 -0
  55. package/src/server/tools/__tests__/z-get-linear-url.test.ts +108 -0
  56. package/src/server/tools/configure-linear.ts +373 -0
  57. package/src/server/tools/create-epic.ts +35 -0
  58. package/src/server/tools/create-prd.ts +31 -0
  59. package/src/server/tools/create-task.ts +38 -0
  60. package/src/server/tools/criteria.ts +50 -0
  61. package/src/server/tools/delete-entity.ts +76 -0
  62. package/src/server/tools/dependencies.ts +55 -0
  63. package/src/server/tools/get-entity.ts +238 -0
  64. package/src/server/tools/get-linear-url.ts +28 -0
  65. package/src/server/tools/get-project-context.ts +33 -0
  66. package/src/server/tools/get-stats.ts +52 -0
  67. package/src/server/tools/get-version.ts +20 -0
  68. package/src/server/tools/index.ts +114 -0
  69. package/src/server/tools/init-project.ts +108 -0
  70. package/src/server/tools/query-entities.ts +167 -0
  71. package/src/server/tools/render-status.ts +201 -0
  72. package/src/server/tools/update-entity.ts +140 -0
  73. package/src/server/tools/update-status.ts +166 -0
  74. package/src/server/utils/__tests__/mcp-response.test.ts +331 -0
  75. package/src/server/utils/logger.ts +9 -0
  76. package/src/server/utils/mcp-response.ts +254 -0
  77. package/src/server/utils/status-transitions.ts +160 -0
  78. package/src/status-line/__tests__/status-line.test.ts +215 -0
  79. package/src/status-line/index.ts +147 -0
  80. package/src/utils/__tests__/chalk-import.test.ts +32 -0
  81. package/src/utils/__tests__/display.test.ts +97 -0
  82. package/src/utils/__tests__/status-renderer.test.ts +310 -0
  83. package/src/utils/display.ts +62 -0
  84. package/src/utils/status-renderer.ts +188 -0
  85. package/src/version.ts +5 -0
  86. package/dist/server/index.js +0 -87063
package/README.md CHANGED
@@ -2,10 +2,14 @@
2
2
 
3
3
  Agent-orchestrated, spec-driven workflow for Claude Code.
4
4
 
5
+ ## Prerequisites
6
+
7
+ This plugin requires [Bun](https://bun.sh). If you don't have Bun installed, the installer will offer to install it for you.
8
+
5
9
  ## Installation
6
10
 
7
11
  ```bash
8
- npx @cliangdev/flux-plugin
12
+ bunx @cliangdev/flux-plugin
9
13
  ```
10
14
 
11
15
  This installs:
@@ -16,8 +20,8 @@ This installs:
16
20
  ### Options
17
21
 
18
22
  ```bash
19
- npx @cliangdev/flux-plugin --global # Install to ~/.claude (all projects)
20
- npx @cliangdev/flux-plugin --local # Install to ./.claude (current project)
23
+ bunx @cliangdev/flux-plugin --global # Install to ~/.claude (all projects)
24
+ bunx @cliangdev/flux-plugin --local # Install to ./.claude (current project)
21
25
  ```
22
26
 
23
27
  ### What Gets Configured
@@ -121,7 +125,7 @@ your-project/
121
125
  To update to the latest version, simply re-run the installer:
122
126
 
123
127
  ```bash
124
- npx @cliangdev/flux-plugin@latest --global
128
+ bunx @cliangdev/flux-plugin@latest --global
125
129
  ```
126
130
 
127
131
  This will:
package/bin/install.cjs CHANGED
@@ -4,22 +4,45 @@ const fs = require("fs");
4
4
  const path = require("path");
5
5
  const os = require("os");
6
6
  const readline = require("readline");
7
+ const { execSync, spawn } = require("child_process");
7
8
 
8
9
  const args = process.argv.slice(2);
9
10
 
10
11
  if (args[0] === "serve") {
11
- import("../dist/server/index.js").catch((err) => {
12
+ const serverSrc = path.join(__dirname, "..", "src", "server", "index.ts");
13
+ const bunPath = getBunPath();
14
+ if (!bunPath) {
15
+ console.error("Failed to start Flux MCP server: Bun is required but not found");
16
+ process.exit(1);
17
+ }
18
+ const child = spawn(bunPath, ["run", serverSrc], { stdio: "inherit" });
19
+ child.on("error", (err) => {
12
20
  console.error("Failed to start Flux MCP server:", err.message);
13
21
  process.exit(1);
14
22
  });
23
+ child.on("close", (code) => process.exit(code || 0));
15
24
  } else {
16
25
  runInstaller();
17
26
  }
18
27
 
28
+ function getBunPath() {
29
+ const bunDir = path.join(os.homedir(), ".bun", "bin");
30
+ const bunBinary = process.platform === "win32" ? "bun.exe" : "bun";
31
+ const localBunPath = path.join(bunDir, bunBinary);
32
+ if (fs.existsSync(localBunPath)) return localBunPath;
33
+ try {
34
+ execSync("bun --version", { stdio: "ignore" });
35
+ return "bun";
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
19
41
  function runInstaller() {
20
42
  const cyan = "\x1b[36m";
21
43
  const green = "\x1b[32m";
22
44
  const yellow = "\x1b[33m";
45
+ const red = "\x1b[31m";
23
46
  const dim = "\x1b[2m";
24
47
  const reset = "\x1b[0m";
25
48
  const pkg = require("../package.json");
@@ -43,7 +66,7 @@ ${cyan} ███████╗██╗ ██╗ ██╗██╗
43
66
  console.log(banner);
44
67
 
45
68
  if (hasHelp) {
46
- console.log(` ${yellow}Usage:${reset} npx @cliangdev/flux-plugin [options]
69
+ console.log(` ${yellow}Usage:${reset} bunx @cliangdev/flux-plugin [options]
47
70
 
48
71
  ${yellow}Options:${reset}
49
72
  ${cyan}-g, --global${reset} Install globally (to ~/.claude)
@@ -52,17 +75,123 @@ ${cyan} ███████╗██╗ ██╗ ██╗██╗
52
75
 
53
76
  ${yellow}Examples:${reset}
54
77
  ${dim}# Interactive installation${reset}
55
- npx @cliangdev/flux-plugin
78
+ bunx @cliangdev/flux-plugin
56
79
 
57
80
  ${dim}# Install globally (all projects)${reset}
58
- npx @cliangdev/flux-plugin --global
81
+ bunx @cliangdev/flux-plugin --global
59
82
 
60
83
  ${dim}# Install locally (current project only)${reset}
61
- npx @cliangdev/flux-plugin --local
84
+ bunx @cliangdev/flux-plugin --local
85
+
86
+ ${yellow}Note:${reset} This plugin requires Bun. Install from https://bun.sh
62
87
  `);
63
88
  process.exit(0);
64
89
  }
65
90
 
91
+ function isBunInstalled() {
92
+ const bunDir = path.join(os.homedir(), ".bun", "bin");
93
+ const envPath = process.env.PATH || "";
94
+ const pathWithBun = envPath.includes(bunDir)
95
+ ? envPath
96
+ : `${bunDir}${path.delimiter}${envPath}`;
97
+
98
+ try {
99
+ execSync("bun --version", {
100
+ stdio: "ignore",
101
+ env: { ...process.env, PATH: pathWithBun },
102
+ });
103
+ return true;
104
+ } catch {
105
+ return false;
106
+ }
107
+ }
108
+
109
+ function installBun() {
110
+ return new Promise((resolve, reject) => {
111
+ const platform = os.platform();
112
+ const installCmd = platform === "win32" ? "powershell" : "/bin/sh";
113
+ const installArgs = platform === "win32"
114
+ ? ["-c", "irm bun.sh/install.ps1 | iex"]
115
+ : ["-c", "curl -fsSL https://bun.sh/install | bash"];
116
+
117
+ console.log(`\n ${cyan}Installing Bun...${reset}\n`);
118
+
119
+ const child = spawn(installCmd, installArgs, {
120
+ stdio: "inherit",
121
+ shell: false,
122
+ });
123
+
124
+ child.on("close", (code) => {
125
+ if (code === 0) {
126
+ console.log(`\n ${green}✓${reset} Bun installed successfully\n`);
127
+ resolve(true);
128
+ } else {
129
+ reject(new Error(`Installation exited with code ${code}`));
130
+ }
131
+ });
132
+
133
+ child.on("error", (err) => {
134
+ reject(err);
135
+ });
136
+ });
137
+ }
138
+
139
+ function showBunInstallInstructions() {
140
+ console.log(`
141
+ ${yellow}Bun is required but not installed.${reset}
142
+
143
+ Install Bun manually:
144
+
145
+ ${cyan}macOS/Linux:${reset}
146
+ curl -fsSL https://bun.sh/install | bash
147
+
148
+ ${cyan}Windows:${reset}
149
+ powershell -c "irm bun.sh/install.ps1 | iex"
150
+
151
+ Then restart your terminal and run this installer again.
152
+
153
+ ${dim}Learn more: https://bun.sh${reset}
154
+ `);
155
+ }
156
+
157
+ async function checkBunAndContinue(callback) {
158
+ if (isBunInstalled()) {
159
+ callback();
160
+ return;
161
+ }
162
+
163
+ const rl = readline.createInterface({
164
+ input: process.stdin,
165
+ output: process.stdout,
166
+ });
167
+
168
+ console.log(` ${yellow}Bun is required but not installed.${reset}\n`);
169
+
170
+ rl.question(` Install Bun now? ${dim}[Y/n]${reset}: `, async (answer) => {
171
+ rl.close();
172
+ const shouldInstall = answer.trim().toLowerCase() !== "n";
173
+
174
+ if (shouldInstall) {
175
+ try {
176
+ await installBun();
177
+ if (isBunInstalled()) {
178
+ callback();
179
+ } else {
180
+ console.log(` ${yellow}Please restart your terminal to use Bun, then run the installer again.${reset}\n`);
181
+ process.exit(0);
182
+ }
183
+ } catch (err) {
184
+ console.log(`\n ${red}Failed to install Bun:${reset} ${err.message}\n`);
185
+ showBunInstallInstructions();
186
+ process.exit(1);
187
+ }
188
+ } else {
189
+ showBunInstallInstructions();
190
+ process.exit(1);
191
+ }
192
+ });
193
+ }
194
+
66
195
  function copyDir(src, dest) {
67
196
  fs.mkdirSync(dest, { recursive: true });
68
197
  const entries = fs.readdirSync(src, { withFileTypes: true });
@@ -177,9 +306,10 @@ ${cyan} ███████╗██╗ ██╗ ██╗██╗
177
306
  mcpConfig.mcpServers = {};
178
307
  }
179
308
 
309
+ const versionTag = pkg.version.includes("-dev.") ? "latest" : pkg.version;
180
310
  mcpConfig.mcpServers.flux = {
181
- command: "npx",
182
- args: ["-y", `@cliangdev/flux-plugin@${pkg.version}`, "serve"],
311
+ command: "bunx",
312
+ args: [`@cliangdev/flux-plugin@${versionTag}`, "serve"],
183
313
  };
184
314
 
185
315
  writeJson(mcpConfigPath, mcpConfig);
@@ -222,14 +352,18 @@ ${cyan} ███████╗██╗ ██╗ ██╗██╗
222
352
  });
223
353
  }
224
354
 
225
- if (hasGlobal && hasLocal) {
226
- console.error(` ${yellow}Cannot specify both --global and --local${reset}`);
227
- process.exit(1);
228
- } else if (hasGlobal) {
229
- install(true);
230
- } else if (hasLocal) {
231
- install(false);
232
- } else {
233
- promptLocation();
355
+ function startInstallation() {
356
+ if (hasGlobal && hasLocal) {
357
+ console.error(` ${yellow}Cannot specify both --global and --local${reset}`);
358
+ process.exit(1);
359
+ } else if (hasGlobal) {
360
+ install(true);
361
+ } else if (hasLocal) {
362
+ install(false);
363
+ } else {
364
+ promptLocation();
365
+ }
234
366
  }
367
+
368
+ checkBunAndContinue(startInstallation);
235
369
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cliangdev/flux-plugin",
3
- "version": "0.0.0-dev.cbdf207",
3
+ "version": "0.0.0-dev.df3e9bb",
4
4
  "description": "Claude Code plugin for AI-first workflow orchestration with MCP server",
5
5
  "type": "module",
6
6
  "main": "./dist/server/index.js",
@@ -8,8 +8,8 @@
8
8
  "flux-plugin": "./bin/install.cjs"
9
9
  },
10
10
  "files": [
11
- "bin/",
12
- "dist/",
11
+ "bin/install.cjs",
12
+ "src/",
13
13
  "skills/",
14
14
  "commands/",
15
15
  "agents/",
@@ -17,14 +17,12 @@
17
17
  ],
18
18
  "scripts": {
19
19
  "dev": "bun run src/server/index.ts",
20
- "build": "bun build src/server/index.ts --outdir dist/server --target node --external better-sqlite3",
21
- "postbuild": "node -e \"const fs=require('fs');const f='dist/server/index.js';const c=fs.readFileSync(f,'utf-8');if(!c.startsWith('#!/usr/bin/env node')){fs.writeFileSync(f,'#!/usr/bin/env node\\n'+c)}\"",
22
- "build:compile": "bun build --compile --outfile bin/flux-server src/server/index.ts && bun build --compile --outfile bin/flux-status src/status-line/index.ts",
23
- "build:compile:server": "bun build --compile --outfile bin/flux-server src/server/index.ts",
24
- "build:compile:status": "bun build --compile --outfile bin/flux-status src/status-line/index.ts",
20
+ "build": "bun build --compile --outfile bin/flux-server src/server/index.ts && bun build --compile --outfile bin/flux-status src/status-line/index.ts",
21
+ "build:server": "bun build --compile --outfile bin/flux-server src/server/index.ts",
22
+ "build:status": "bun build --compile --outfile bin/flux-status src/status-line/index.ts",
25
23
  "validate": "node scripts/validate-structure.cjs",
26
24
  "test:integration": "bun test scripts/__tests__/integration.test.ts --timeout 120000",
27
- "prepublishOnly": "bun run validate && bun run build && bun run test:integration",
25
+ "prepublishOnly": "bun run validate && bun run test:integration",
28
26
  "test": "bun test",
29
27
  "test:linear-description": "bun run src/server/adapters/__tests__/linear-description-test.ts",
30
28
  "typecheck": "tsc --noEmit",
@@ -53,14 +51,12 @@
53
51
  "license": "MIT",
54
52
  "devDependencies": {
55
53
  "@biomejs/biome": "^2.3.11",
56
- "@types/better-sqlite3": "^7.6.13",
57
54
  "@types/bun": "^1.3.6",
58
55
  "typescript": "^5.0.0"
59
56
  },
60
57
  "dependencies": {
61
58
  "@linear/sdk": "^70.0.0",
62
59
  "@modelcontextprotocol/sdk": "^1.25.2",
63
- "better-sqlite3": "^12.6.2",
64
60
  "chalk": "^5.4.1",
65
61
  "zod": "^4.3.5"
66
62
  },
@@ -0,0 +1,37 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+
5
+ describe("version module", () => {
6
+ test("VERSION is exported from src/version.ts", async () => {
7
+ // Dynamic import to get the VERSION constant
8
+ const versionModule = await import("../version.js");
9
+
10
+ expect(versionModule.VERSION).toBeDefined();
11
+ expect(typeof versionModule.VERSION).toBe("string");
12
+ });
13
+
14
+ test("VERSION equals package.json version", async () => {
15
+ // Read package.json version
16
+ const packageJsonPath = join(process.cwd(), "package.json");
17
+ expect(existsSync(packageJsonPath)).toBe(true);
18
+
19
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
20
+ const packageVersion = packageJson.version;
21
+
22
+ expect(packageVersion).toBeDefined();
23
+ expect(typeof packageVersion).toBe("string");
24
+
25
+ // Import VERSION and compare
26
+ const versionModule = await import("../version.js");
27
+ expect(versionModule.VERSION).toBe(packageVersion);
28
+ });
29
+
30
+ test("VERSION is a valid semver format", async () => {
31
+ const versionModule = await import("../version.js");
32
+
33
+ // Basic semver format check (e.g., "0.1.0", "1.2.3-beta.1")
34
+ const semverPattern = /^\d+\.\d+\.\d+(-[\w.]+)?$/;
35
+ expect(semverPattern.test(versionModule.VERSION)).toBe(true);
36
+ });
37
+ });
File without changes
@@ -0,0 +1,163 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import {
3
+ existsSync,
4
+ mkdirSync,
5
+ realpathSync,
6
+ rmSync,
7
+ writeFileSync,
8
+ } from "node:fs";
9
+ import { tmpdir } from "node:os";
10
+
11
+ describe("config", () => {
12
+ const originalEnv = process.env.FLUX_PROJECT_ROOT;
13
+ const originalCwd = process.cwd();
14
+ // Use os.tmpdir() to get the real temp path (handles /tmp -> /private/tmp on macOS)
15
+ const TEST_DIR = `${realpathSync(tmpdir())}/flux-config-test-${Date.now()}`;
16
+ const NESTED_DIR = `${TEST_DIR}/subdir/nested`;
17
+
18
+ beforeEach(async () => {
19
+ // Clean up any previous test directory
20
+ if (existsSync(TEST_DIR)) {
21
+ rmSync(TEST_DIR, { recursive: true });
22
+ }
23
+
24
+ // Create test directory structure
25
+ mkdirSync(NESTED_DIR, { recursive: true });
26
+
27
+ // Clear the config cache before each test
28
+ const { config } = await import("../config.js");
29
+ config.clearCache();
30
+ });
31
+
32
+ afterEach(async () => {
33
+ // Restore original env
34
+ if (originalEnv !== undefined) {
35
+ process.env.FLUX_PROJECT_ROOT = originalEnv;
36
+ } else {
37
+ delete process.env.FLUX_PROJECT_ROOT;
38
+ }
39
+
40
+ // Restore original cwd
41
+ process.chdir(originalCwd);
42
+
43
+ // Clear the config cache
44
+ const { config } = await import("../config.js");
45
+ config.clearCache();
46
+
47
+ // Clean up test directory
48
+ if (existsSync(TEST_DIR)) {
49
+ rmSync(TEST_DIR, { recursive: true });
50
+ }
51
+ });
52
+
53
+ test("uses env var when properly set", async () => {
54
+ process.env.FLUX_PROJECT_ROOT = "/some/valid/path";
55
+
56
+ const { config } = await import("../config.js");
57
+ config.clearCache();
58
+
59
+ expect(config.projectRoot).toBe("/some/valid/path");
60
+ });
61
+
62
+ test("ignores unresolved template variable and walks up directories", async () => {
63
+ // Create a .flux folder at TEST_DIR
64
+ mkdirSync(`${TEST_DIR}/.flux`, { recursive: true });
65
+ writeFileSync(
66
+ `${TEST_DIR}/.flux/project.json`,
67
+ JSON.stringify({ name: "test" }),
68
+ );
69
+
70
+ // Simulate Claude Code passing unresolved template variable
71
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: intentionally testing literal template string
72
+ process.env.FLUX_PROJECT_ROOT = "${CLAUDE_PROJECT_DIR}";
73
+
74
+ // Change to nested directory
75
+ process.chdir(NESTED_DIR);
76
+
77
+ const { config } = await import("../config.js");
78
+ config.clearCache();
79
+
80
+ // Should walk up and find TEST_DIR, not use the literal "${CLAUDE_PROJECT_DIR}"
81
+ expect(config.projectRoot).not.toContain("${");
82
+ expect(config.projectRoot).toBe(TEST_DIR);
83
+ });
84
+
85
+ test("walks up directories to find .flux folder", async () => {
86
+ // Create a .flux folder at TEST_DIR (parent of NESTED_DIR)
87
+ mkdirSync(`${TEST_DIR}/.flux`, { recursive: true });
88
+ writeFileSync(
89
+ `${TEST_DIR}/.flux/project.json`,
90
+ JSON.stringify({ name: "test" }),
91
+ );
92
+
93
+ // No env var set
94
+ delete process.env.FLUX_PROJECT_ROOT;
95
+
96
+ // Change to nested directory
97
+ process.chdir(NESTED_DIR);
98
+
99
+ const { config } = await import("../config.js");
100
+ config.clearCache();
101
+
102
+ // Should walk up and find TEST_DIR
103
+ expect(config.projectRoot).toBe(TEST_DIR);
104
+ });
105
+
106
+ test("falls back to cwd when no .flux folder found", async () => {
107
+ // No .flux folder anywhere in TEST_DIR hierarchy
108
+ // No env var set
109
+ delete process.env.FLUX_PROJECT_ROOT;
110
+
111
+ // Change to test directory (which has no .flux)
112
+ process.chdir(TEST_DIR);
113
+
114
+ const { config } = await import("../config.js");
115
+ config.clearCache();
116
+
117
+ // Should fall back to cwd
118
+ expect(config.projectRoot).toBe(TEST_DIR);
119
+ });
120
+
121
+ test("projectExists returns true when project.json exists", async () => {
122
+ mkdirSync(`${TEST_DIR}/.flux`, { recursive: true });
123
+ writeFileSync(
124
+ `${TEST_DIR}/.flux/project.json`,
125
+ JSON.stringify({ name: "test" }),
126
+ );
127
+
128
+ process.env.FLUX_PROJECT_ROOT = TEST_DIR;
129
+
130
+ const { config } = await import("../config.js");
131
+ config.clearCache();
132
+
133
+ expect(config.projectExists).toBe(true);
134
+ });
135
+
136
+ test("projectExists returns false when project.json does not exist", async () => {
137
+ // No .flux folder
138
+ process.env.FLUX_PROJECT_ROOT = TEST_DIR;
139
+
140
+ const { config } = await import("../config.js");
141
+ config.clearCache();
142
+
143
+ expect(config.projectExists).toBe(false);
144
+ });
145
+
146
+ test("caches project root for performance", async () => {
147
+ process.env.FLUX_PROJECT_ROOT = "/first/path";
148
+
149
+ const { config } = await import("../config.js");
150
+ config.clearCache();
151
+
152
+ // First call caches the value
153
+ expect(config.projectRoot).toBe("/first/path");
154
+
155
+ // Changing env var should not affect cached value
156
+ process.env.FLUX_PROJECT_ROOT = "/second/path";
157
+ expect(config.projectRoot).toBe("/first/path");
158
+
159
+ // After clearing cache, should use new env var
160
+ config.clearCache();
161
+ expect(config.projectRoot).toBe("/second/path");
162
+ });
163
+ });