@pixelbyte-software/pixcode 1.38.5 → 1.39.1

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 (52) hide show
  1. package/CODE_OF_CONDUCT.md +41 -0
  2. package/CONTRIBUTING.md +156 -0
  3. package/README.md +223 -92
  4. package/SECURITY.md +46 -0
  5. package/dist/assets/{index-DHC6THrb.js → index-B4DKVLz5.js} +158 -158
  6. package/dist/assets/index-DoOHFfYP.css +32 -0
  7. package/dist/docs.html +17 -3
  8. package/dist/features.html +28 -7
  9. package/dist/index.html +2 -2
  10. package/dist/landing.html +162 -109
  11. package/dist/llms-full.txt +9 -3
  12. package/dist/llms.txt +3 -2
  13. package/dist/site.css +158 -2
  14. package/dist/sitemap.xml +8 -8
  15. package/dist-server/server/gemini-cli.js +9 -1
  16. package/dist-server/server/gemini-cli.js.map +1 -1
  17. package/dist-server/server/modules/orchestration/a2a/routes.js +16 -14
  18. package/dist-server/server/modules/orchestration/a2a/routes.js.map +1 -1
  19. package/dist-server/server/modules/orchestration/workspace/docker-workspace.js +2 -1
  20. package/dist-server/server/modules/orchestration/workspace/docker-workspace.js.map +1 -1
  21. package/dist-server/server/modules/orchestration/workspace/workspace-manager.js +4 -0
  22. package/dist-server/server/modules/orchestration/workspace/workspace-manager.js.map +1 -1
  23. package/dist-server/server/modules/orchestration/workspace/worktree-workspace.js +2 -1
  24. package/dist-server/server/modules/orchestration/workspace/worktree-workspace.js.map +1 -1
  25. package/dist-server/server/opencode-cli.js +9 -1
  26. package/dist-server/server/opencode-cli.js.map +1 -1
  27. package/dist-server/server/qwen-code-cli.js +9 -1
  28. package/dist-server/server/qwen-code-cli.js.map +1 -1
  29. package/dist-server/server/routes/git.js +7 -1
  30. package/dist-server/server/routes/git.js.map +1 -1
  31. package/dist-server/server/routes/taskmaster.js +74 -11
  32. package/dist-server/server/routes/taskmaster.js.map +1 -1
  33. package/dist-server/server/services/taskmaster-config.js +128 -0
  34. package/dist-server/server/services/taskmaster-config.js.map +1 -0
  35. package/package.json +7 -2
  36. package/scripts/smoke/changes-panel-layout.mjs +48 -0
  37. package/scripts/smoke/chat-composer-fixed-layout.mjs +45 -0
  38. package/scripts/smoke/command-center-agent-writes.mjs +27 -1
  39. package/scripts/smoke/orchestration-mobile-scroll.mjs +29 -0
  40. package/scripts/smoke/orchestration-runtime-guards.mjs +48 -0
  41. package/scripts/smoke/taskmaster-config.mjs +59 -0
  42. package/server/gemini-cli.js +9 -1
  43. package/server/modules/orchestration/a2a/routes.ts +16 -14
  44. package/server/modules/orchestration/workspace/docker-workspace.ts +2 -1
  45. package/server/modules/orchestration/workspace/workspace-manager.ts +5 -0
  46. package/server/modules/orchestration/workspace/worktree-workspace.ts +2 -1
  47. package/server/opencode-cli.js +9 -1
  48. package/server/qwen-code-cli.js +9 -1
  49. package/server/routes/git.js +7 -1
  50. package/server/routes/taskmaster.js +83 -11
  51. package/server/services/taskmaster-config.js +146 -0
  52. package/dist/assets/index-B-OgjpDF.css +0 -32
@@ -28,13 +28,26 @@ assert.ok(
28
28
  'Changed-files monitor should ingest the latest realtime agent message in addition to polling git/filesystem status.',
29
29
  );
30
30
 
31
+ assert.ok(
32
+ changedFilesHook.includes("ChangedFilesTrackingMode = 'local' | 'git'")
33
+ && changedFilesHook.includes('mode: trackingMode')
34
+ && changedFilesHook.includes("trackingMode !== 'local'"),
35
+ 'Changed-files monitor should support separate Local changes and Git changes modes.',
36
+ );
37
+
31
38
  assert.ok(
32
39
  changedFilesHook.includes('mergeChangedFiles'),
33
40
  'Changed-files monitor should merge direct agent writes with polled git/filesystem changes instead of replacing them.',
34
41
  );
35
42
 
36
43
  assert.ok(
37
- mainContent.includes('useChangedFilesMonitor(selectedProject, changeAwareness, latestMessage)'),
44
+ changedFilesHook.includes("data.trackingMode === 'filesystem'")
45
+ && changedFilesHook.includes('mergeChangedFiles(polledChangedFilesRef.current, detectedChangedFiles)'),
46
+ 'Filesystem tracking should keep detected files visible after the next empty poll instead of flashing for one interval.',
47
+ );
48
+
49
+ assert.ok(
50
+ mainContent.includes('useChangedFilesMonitor(selectedProject, changeAwareness, latestMessage, changeTrackingMode)'),
38
51
  'MainContent should pass latestMessage into the changed-files monitor.',
39
52
  );
40
53
 
@@ -43,6 +56,19 @@ assert.ok(
43
56
  'Clicking a changed file should open the editor with diff context, not only focus the Files panel.',
44
57
  );
45
58
 
59
+ assert.ok(
60
+ mainContent.includes('/api/git/file-with-diff') && mainContent.includes('old_string') && mainContent.includes('new_string'),
61
+ 'Clicking a changed file without realtime diff metadata should hydrate editor diff context before focusing the changed chunk.',
62
+ );
63
+
64
+ assert.ok(
65
+ rail.includes('trackingMode: ChangedFilesTrackingMode')
66
+ && rail.includes('Local changes')
67
+ && rail.includes('Git changes')
68
+ && rail.includes('onTrackingModeChange'),
69
+ 'Command Center rail should expose a visible switch between Local changes and Git changes.',
70
+ );
71
+
46
72
  assert.ok(
47
73
  rail.includes('onOpenFile: (file: ChangedFileEntry) => void') && rail.includes('onClick={() => onOpenFile(file)}'),
48
74
  'ChangedFilesActivityRail should pass the full changed-file entry, including diffInfo, to the open handler.',
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+
3
+ import assert from 'node:assert/strict';
4
+ import { readFileSync } from 'node:fs';
5
+
6
+ const page = readFileSync('src/components/orchestration/OrchestrationPage.tsx', 'utf8');
7
+ const runPanel = readFileSync('src/components/orchestration/workflows/WorkflowRunPanel.tsx', 'utf8');
8
+
9
+ assert.ok(
10
+ page.includes('overflow-y-auto lg:overflow-hidden'),
11
+ 'Orchestration mobile layout should allow page-level vertical scrolling before desktop split mode.',
12
+ );
13
+
14
+ assert.ok(
15
+ /<aside[^>]+className="[^"]*overflow-visible[^"]*lg:overflow-auto/.test(page),
16
+ 'Orchestration setup pane should not trap scrolling on mobile.',
17
+ );
18
+
19
+ assert.ok(
20
+ /<section[^>]+className="[^"]*overflow-visible[^"]*lg:overflow-hidden/.test(page),
21
+ 'Orchestration run section should stay reachable in mobile document flow.',
22
+ );
23
+
24
+ assert.ok(
25
+ /className="[^"]*overflow-visible[^"]*xl:overflow-auto/.test(runPanel),
26
+ 'Workflow run panel should avoid nested scroll traps before its desktop two-column layout.',
27
+ );
28
+
29
+ console.log('orchestration mobile scroll smoke passed');
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+
3
+ import assert from 'node:assert/strict';
4
+ import { readFileSync } from 'node:fs';
5
+
6
+ const a2aRoutes = readFileSync('server/modules/orchestration/a2a/routes.ts', 'utf8');
7
+ const hostWorkspace = readFileSync('server/modules/orchestration/workspace/workspace-manager.ts', 'utf8');
8
+ const worktreeWorkspace = readFileSync('server/modules/orchestration/workspace/worktree-workspace.ts', 'utf8');
9
+ const dockerWorkspace = readFileSync('server/modules/orchestration/workspace/docker-workspace.ts', 'utf8');
10
+ const opencodeCli = readFileSync('server/opencode-cli.js', 'utf8');
11
+ const geminiCli = readFileSync('server/gemini-cli.js', 'utf8');
12
+ const qwenCli = readFileSync('server/qwen-code-cli.js', 'utf8');
13
+
14
+ assert.ok(
15
+ hostWorkspace.includes("rev-parse', '--is-inside-work-tree")
16
+ && hostWorkspace.includes("return '';"),
17
+ 'Host orchestration workspace diff should skip git diff when the project is not a git work tree.',
18
+ );
19
+
20
+ for (const [name, source] of [
21
+ ['worktree', worktreeWorkspace],
22
+ ['docker', dockerWorkspace],
23
+ ]) {
24
+ assert.ok(
25
+ source.includes('not a git repository|usage: git diff --no-index') && source.includes("return /not a git repository"),
26
+ `${name} orchestration workspace diff should suppress noisy git --no-index usage output.`,
27
+ );
28
+ }
29
+
30
+ assert.ok(
31
+ a2aRoutes.includes('if (diff.trim())') && a2aRoutes.includes("type: 'file-diff'"),
32
+ 'A2A finalization should not publish empty or noisy workspace diff artifacts.',
33
+ );
34
+
35
+ for (const [name, source] of [
36
+ ['opencode', opencodeCli],
37
+ ['gemini', geminiCli],
38
+ ['qwen', qwenCli],
39
+ ]) {
40
+ assert.ok(
41
+ source.includes('DEFAULT_CLI_IDLE_TIMEOUT_MS = 600000')
42
+ && source.includes('PIXCODE_CLI_IDLE_TIMEOUT_MS')
43
+ && source.includes('if (timeoutMs === 0) return;'),
44
+ `${name} CLI runtime should use a longer configurable idle timeout.`,
45
+ );
46
+ }
47
+
48
+ console.log('orchestration runtime guards smoke passed');
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+
3
+ import assert from 'node:assert/strict';
4
+ import { readFileSync } from 'node:fs';
5
+
6
+ const taskmasterRoutes = readFileSync('server/routes/taskmaster.js', 'utf8');
7
+ const taskmasterConfig = readFileSync('server/services/taskmaster-config.js', 'utf8');
8
+ const tasksSettingsContext = readFileSync('src/contexts/TasksSettingsContext.jsx', 'utf8');
9
+ const tasksSettingsTab = readFileSync('src/components/settings/view/tabs/tasks-settings/TasksSettingsTab.tsx', 'utf8');
10
+ const mainContentState = readFileSync('src/components/main-content/view/subcomponents/MainContentStateView.tsx', 'utf8');
11
+
12
+ assert.ok(
13
+ taskmasterConfig.includes('TASKMASTER_CONFIG_FIELDS') && taskmasterConfig.includes('ANTHROPIC_API_KEY'),
14
+ 'TaskMaster should have a dedicated config store for provider environment variables.',
15
+ );
16
+
17
+ assert.ok(
18
+ taskmasterConfig.includes('OPENAI_BASE_URL') && taskmasterConfig.includes('AZURE_OPENAI_ENDPOINT'),
19
+ 'TaskMaster config should support API URL / endpoint fields, not only keys.',
20
+ );
21
+
22
+ assert.ok(
23
+ taskmasterConfig.includes('openaiCompatibleApiKey')
24
+ && taskmasterConfig.includes('OPENAI_COMPATIBLE_BASE_URL')
25
+ && taskmasterConfig.includes('CUSTOM_OPENAI_API_KEY')
26
+ && taskmasterConfig.includes('buildTaskMasterConfigEnvValues'),
27
+ 'TaskMaster config should support custom OpenAI-compatible API keys, API URLs, model values, and shared env resolution.',
28
+ );
29
+
30
+ assert.ok(
31
+ taskmasterRoutes.includes("router.get('/config'") && taskmasterRoutes.includes("router.put('/config'"),
32
+ 'TaskMaster routes should expose authenticated config read/write endpoints.',
33
+ );
34
+
35
+ assert.ok(
36
+ taskmasterRoutes.includes('buildTaskMasterCliEnv') && taskmasterRoutes.includes('task-master-ai'),
37
+ 'TaskMaster CLI execution should receive saved env config and detect both task-master and task-master-ai binaries.',
38
+ );
39
+
40
+ assert.ok(
41
+ tasksSettingsContext.includes('refreshTaskMasterInstallation'),
42
+ 'Task settings context should expose a manual TaskMaster installation refresh action.',
43
+ );
44
+
45
+ assert.ok(
46
+ tasksSettingsTab.includes('/api/taskmaster/config')
47
+ && tasksSettingsTab.includes('ANTHROPIC_API_KEY')
48
+ && tasksSettingsTab.includes('OPENAI_BASE_URL')
49
+ && tasksSettingsTab.includes('Custom OpenAI-compatible')
50
+ && tasksSettingsTab.includes('OPENAI_COMPATIBLE_MODEL'),
51
+ 'Task settings tab should let users save TaskMaster API keys, API URLs, and custom OpenAI-compatible provider settings.',
52
+ );
53
+
54
+ assert.ok(
55
+ mainContentState.includes('pixcode:create-project') && mainContentState.includes('mainContent.landing.taskSystem'),
56
+ 'Landing Task system card should be actionable instead of a static locked-looking panel.',
57
+ );
58
+
59
+ console.log('taskmaster config smoke passed');
@@ -15,6 +15,12 @@ import { createNormalizedMessage } from './shared/utils.js';
15
15
  // Use cross-spawn on Windows for correct .cmd resolution (same pattern as cursor-cli.js)
16
16
  const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn;
17
17
  let activeGeminiProcesses = new Map(); // Track active processes by session ID
18
+ const DEFAULT_CLI_IDLE_TIMEOUT_MS = 600000;
19
+
20
+ function readCliIdleTimeoutMs() {
21
+ const configured = Number.parseInt(process.env.PIXCODE_CLI_IDLE_TIMEOUT_MS || '', 10);
22
+ return Number.isFinite(configured) && configured >= 0 ? configured : DEFAULT_CLI_IDLE_TIMEOUT_MS;
23
+ }
18
24
 
19
25
  /**
20
26
  * Auto-create `~/.gemini/settings.json` when the user has signed in via OAuth
@@ -287,11 +293,12 @@ async function spawnGemini(command, options = {}, ws) {
287
293
  geminiProcess.stdin.end();
288
294
 
289
295
  // Add timeout handler
290
- const timeoutMs = 120000; // 120 seconds for slower models
296
+ const timeoutMs = readCliIdleTimeoutMs();
291
297
  let timeout;
292
298
 
293
299
  const startTimeout = () => {
294
300
  if (timeout) clearTimeout(timeout);
301
+ if (timeoutMs === 0) return;
295
302
  timeout = setTimeout(() => {
296
303
  const socketSessionId = typeof ws.getSessionId === 'function' ? ws.getSessionId() : (capturedSessionId || sessionId || processKey);
297
304
  terminalFailureReason = `Gemini CLI timeout - no response received for ${timeoutMs / 1000} seconds`;
@@ -400,6 +407,7 @@ async function spawnGemini(command, options = {}, ws) {
400
407
  // Handle stderr
401
408
  geminiProcess.stderr.on('data', (data) => {
402
409
  const errorMsg = data.toString();
410
+ startTimeout();
403
411
 
404
412
  // Filter out deprecation warnings and "Loaded cached credentials" message
405
413
  if (errorMsg.includes('[DEP0040]') ||
@@ -179,21 +179,23 @@ async function finalizeTerminalTask(task: Task): Promise<void> {
179
179
  try {
180
180
  if (workspace) {
181
181
  const diff = await workspace.diff();
182
- a2aBus.publish({
183
- kind: 'artifact',
184
- taskId: task.id,
185
- artifact: {
186
- artifactId: newId('art'),
187
- type: 'file-diff',
188
- parts: [{ kind: 'text', text: diff }],
189
- metadata: {
190
- source: 'workspace-diff',
191
- workspaceId: workspace.id,
192
- workspaceKind: workspace.kind,
193
- baseRef: workspace.baseRef,
182
+ if (diff.trim()) {
183
+ a2aBus.publish({
184
+ kind: 'artifact',
185
+ taskId: task.id,
186
+ artifact: {
187
+ artifactId: newId('art'),
188
+ type: 'file-diff',
189
+ parts: [{ kind: 'text', text: diff }],
190
+ metadata: {
191
+ source: 'workspace-diff',
192
+ workspaceId: workspace.id,
193
+ workspaceKind: workspace.kind,
194
+ baseRef: workspace.baseRef,
195
+ },
194
196
  },
195
- },
196
- });
197
+ });
198
+ }
197
199
 
198
200
  const keepAfterCompletion = task.metadata?.workspace &&
199
201
  typeof task.metadata.workspace === 'object' &&
@@ -125,7 +125,8 @@ export class DockerWorkspace implements WorkspaceHandle {
125
125
  return String(stdout);
126
126
  } catch (error) {
127
127
  const err = error as Error & { stderr?: string };
128
- return String(err.stderr ?? err.message);
128
+ const output = String(err.stderr ?? err.message);
129
+ return /not a git repository|usage: git diff --no-index/i.test(output) ? '' : output;
129
130
  }
130
131
  }
131
132
 
@@ -56,6 +56,11 @@ class HostWorkspace implements WorkspaceHandle {
56
56
  }
57
57
 
58
58
  async diff(): Promise<string> {
59
+ const insideWorkTree = await this.exec('git', ['rev-parse', '--is-inside-work-tree']);
60
+ if (insideWorkTree.exitCode !== 0 || insideWorkTree.stdout.trim() !== 'true') {
61
+ return '';
62
+ }
63
+
59
64
  const result = await this.exec('git', ['diff', `${this.baseRef}...HEAD`]);
60
65
  return result.exitCode === 0 ? result.stdout : result.stderr || result.stdout;
61
66
  }
@@ -109,7 +109,8 @@ export class WorktreeWorkspace implements WorkspaceHandle {
109
109
  async diff(): Promise<string> {
110
110
  const result = await run('git', ['diff', `${this.baseRef}...HEAD`], this.path);
111
111
  if (result.exitCode !== 0) {
112
- return result.stderr || result.stdout;
112
+ const output = result.stderr || result.stdout;
113
+ return /not a git repository|usage: git diff --no-index/i.test(output) ? '' : output;
113
114
  }
114
115
  return result.stdout;
115
116
  }
@@ -37,6 +37,12 @@ import { createNormalizedMessage } from './shared/utils.js';
37
37
  const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn;
38
38
 
39
39
  const activeOpencodeProcesses = new Map();
40
+ const DEFAULT_CLI_IDLE_TIMEOUT_MS = 600000;
41
+
42
+ function readCliIdleTimeoutMs() {
43
+ const configured = Number.parseInt(process.env.PIXCODE_CLI_IDLE_TIMEOUT_MS || '', 10);
44
+ return Number.isFinite(configured) && configured >= 0 ? configured : DEFAULT_CLI_IDLE_TIMEOUT_MS;
45
+ }
40
46
 
41
47
  function mapPermissionModeToArgs(permissionMode, skipPermissions) {
42
48
  // OpenCode's permission model is per-tool/per-pattern (see opencode.json
@@ -233,10 +239,11 @@ async function spawnOpencode(command, options = {}, ws) {
233
239
 
234
240
  opencodeProcess.stdin.end();
235
241
 
236
- const timeoutMs = 120000;
242
+ const timeoutMs = readCliIdleTimeoutMs();
237
243
  let timeout;
238
244
  const startTimeout = () => {
239
245
  if (timeout) clearTimeout(timeout);
246
+ if (timeoutMs === 0) return;
240
247
  timeout = setTimeout(() => {
241
248
  const socketSessionId = typeof ws.getSessionId === 'function' ? ws.getSessionId() : (capturedSessionId || sessionId || processKey);
242
249
  terminalFailureReason = `OpenCode CLI timeout - no response received for ${timeoutMs / 1000} seconds`;
@@ -333,6 +340,7 @@ async function spawnOpencode(command, options = {}, ws) {
333
340
 
334
341
  opencodeProcess.stderr.on('data', (data) => {
335
342
  const rawMsg = data.toString();
343
+ startTimeout();
336
344
  // Suppress known cosmetic noise.
337
345
  if (rawMsg.includes('[DEP0040]') ||
338
346
  rawMsg.includes('DeprecationWarning') ||
@@ -26,6 +26,12 @@ import { createNormalizedMessage } from './shared/utils.js';
26
26
  const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn;
27
27
 
28
28
  const activeQwenProcesses = new Map();
29
+ const DEFAULT_CLI_IDLE_TIMEOUT_MS = 600000;
30
+
31
+ function readCliIdleTimeoutMs() {
32
+ const configured = Number.parseInt(process.env.PIXCODE_CLI_IDLE_TIMEOUT_MS || '', 10);
33
+ return Number.isFinite(configured) && configured >= 0 ? configured : DEFAULT_CLI_IDLE_TIMEOUT_MS;
34
+ }
29
35
 
30
36
  async function spawnQwen(command, options = {}, ws) {
31
37
  const { sessionId, projectPath, cwd, toolsSettings, permissionMode, images, sessionSummary } = options;
@@ -170,10 +176,11 @@ async function spawnQwen(command, options = {}, ws) {
170
176
 
171
177
  qwenProcess.stdin.end();
172
178
 
173
- const timeoutMs = 120000;
179
+ const timeoutMs = readCliIdleTimeoutMs();
174
180
  let timeout;
175
181
  const startTimeout = () => {
176
182
  if (timeout) clearTimeout(timeout);
183
+ if (timeoutMs === 0) return;
177
184
  timeout = setTimeout(() => {
178
185
  const socketSessionId = typeof ws.getSessionId === 'function' ? ws.getSessionId() : (capturedSessionId || sessionId || processKey);
179
186
  terminalFailureReason = `Qwen Code CLI timeout - no response received for ${timeoutMs / 1000} seconds`;
@@ -267,6 +274,7 @@ async function spawnQwen(command, options = {}, ws) {
267
274
 
268
275
  qwenProcess.stderr.on('data', (data) => {
269
276
  const errorMsg = data.toString();
277
+ startTimeout();
270
278
  if (errorMsg.includes('[DEP0040]') ||
271
279
  errorMsg.includes('DeprecationWarning') ||
272
280
  errorMsg.includes('--trace-deprecation') ||
@@ -436,6 +436,8 @@ async function resolveRepositoryFilePath(projectPath, filePath) {
436
436
  // Get git status for a project
437
437
  router.get('/status', async (req, res) => {
438
438
  const { project } = req.query;
439
+ const requestedTrackingMode = String(req.query.mode || req.query.trackingMode || '').toLowerCase();
440
+ const gitOnly = requestedTrackingMode === 'git';
439
441
 
440
442
  if (!project) {
441
443
  return res.status(400).json({ error: 'Project name is required' });
@@ -477,6 +479,8 @@ router.get('/status', async (req, res) => {
477
479
  });
478
480
 
479
481
  res.json({
482
+ isGitRepository: true,
483
+ trackingMode: 'git',
480
484
  branch,
481
485
  hasCommits,
482
486
  modified,
@@ -485,7 +489,7 @@ router.get('/status', async (req, res) => {
485
489
  untracked
486
490
  });
487
491
  } catch (error) {
488
- if (projectPath && isNotGitRepositoryMessage(error.message)) {
492
+ if (projectPath && !gitOnly && isNotGitRepositoryMessage(error.message)) {
489
493
  try {
490
494
  res.json(await buildFilesystemStatus(projectPath));
491
495
  return;
@@ -496,6 +500,8 @@ router.get('/status', async (req, res) => {
496
500
 
497
501
  console.error('Git status error:', error);
498
502
  res.json({
503
+ isGitRepository: false,
504
+ trackingMode: gitOnly ? 'git' : undefined,
499
505
  error: error.message.includes('not a git repository') || error.message.includes('Project directory is not a git repository')
500
506
  ? error.message
501
507
  : 'Git operation failed',
@@ -26,6 +26,11 @@ import {
26
26
  getInstallJob,
27
27
  snapshotDonePayload
28
28
  } from '../services/install-jobs.js';
29
+ import {
30
+ buildTaskMasterEnv,
31
+ getTaskMasterConfigSummary,
32
+ saveTaskMasterConfig
33
+ } from '../services/taskmaster-config.js';
29
34
  import { broadcastTaskMasterProjectUpdate, broadcastTaskMasterTasksUpdate } from '../utils/taskmaster-websocket.js';
30
35
  import { detectTaskMasterMCPServer } from '../utils/mcp-detector.js';
31
36
 
@@ -35,22 +40,42 @@ const router = express.Router();
35
40
  * Check if TaskMaster CLI is installed globally
36
41
  * @returns {Promise<Object>} Installation status result
37
42
  */
43
+ async function buildTaskMasterCliEnv(baseEnv = process.env) {
44
+ return buildTaskMasterEnv(buildCliSpawnEnv(baseEnv));
45
+ }
46
+
47
+ function findTaskMasterExecutable(env) {
48
+ const taskMasterPath = findExecutableOnPath('task-master', env);
49
+ if (taskMasterPath) {
50
+ return { path: taskMasterPath, name: 'task-master' };
51
+ }
52
+
53
+ const legacyPath = findExecutableOnPath('task-master-ai', env);
54
+ if (legacyPath) {
55
+ return { path: legacyPath, name: 'task-master-ai' };
56
+ }
57
+
58
+ return null;
59
+ }
60
+
38
61
  async function checkTaskMasterInstallation() {
62
+ const env = await buildTaskMasterCliEnv();
63
+
39
64
  return new Promise((resolve) => {
40
- const env = buildCliSpawnEnv();
41
- const taskMasterPath = findExecutableOnPath('task-master', env);
65
+ const executable = findTaskMasterExecutable(env);
42
66
 
43
- if (!taskMasterPath) {
67
+ if (!executable) {
44
68
  resolve({
45
69
  isInstalled: false,
46
70
  installPath: null,
47
71
  version: null,
72
+ binary: null,
48
73
  reason: 'TaskMaster CLI not found in PATH'
49
74
  });
50
75
  return;
51
76
  }
52
77
 
53
- const versionChild = crossSpawn(taskMasterPath, ['--version'], {
78
+ const versionChild = crossSpawn(executable.path, ['--version'], {
54
79
  stdio: ['ignore', 'pipe', 'pipe'],
55
80
  env,
56
81
  windowsHide: true,
@@ -65,7 +90,8 @@ async function checkTaskMasterInstallation() {
65
90
  versionChild.on('close', (versionCode) => {
66
91
  resolve({
67
92
  isInstalled: true,
68
- installPath: taskMasterPath,
93
+ installPath: executable.path,
94
+ binary: executable.name,
69
95
  version: versionCode === 0 ? versionOutput.trim() : 'unknown',
70
96
  reason: null
71
97
  });
@@ -74,7 +100,8 @@ async function checkTaskMasterInstallation() {
74
100
  versionChild.on('error', () => {
75
101
  resolve({
76
102
  isInstalled: true,
77
- installPath: taskMasterPath,
103
+ installPath: executable.path,
104
+ binary: executable.name,
78
105
  version: 'unknown',
79
106
  reason: null
80
107
  });
@@ -165,6 +192,46 @@ function buildTaskMasterQueueSummary(projectName, projectPath, tasks) {
165
192
 
166
193
  // API Routes
167
194
 
195
+ /**
196
+ * GET /api/taskmaster/config
197
+ * Return redacted TaskMaster provider env configuration.
198
+ */
199
+ router.get('/config', async (req, res) => {
200
+ try {
201
+ res.json({
202
+ success: true,
203
+ config: await getTaskMasterConfigSummary()
204
+ });
205
+ } catch (error) {
206
+ console.error('Error reading TaskMaster config:', error);
207
+ res.status(500).json({
208
+ success: false,
209
+ error: 'Failed to read TaskMaster configuration',
210
+ message: error.message
211
+ });
212
+ }
213
+ });
214
+
215
+ /**
216
+ * PUT /api/taskmaster/config
217
+ * Persist TaskMaster provider env configuration.
218
+ */
219
+ router.put('/config', async (req, res) => {
220
+ try {
221
+ res.json({
222
+ success: true,
223
+ config: await saveTaskMasterConfig(req.body || {})
224
+ });
225
+ } catch (error) {
226
+ console.error('Error saving TaskMaster config:', error);
227
+ res.status(500).json({
228
+ success: false,
229
+ error: 'Failed to save TaskMaster configuration',
230
+ message: error.message
231
+ });
232
+ }
233
+ });
234
+
168
235
  /**
169
236
  * GET /api/taskmaster/installation-status
170
237
  * Check if TaskMaster CLI is installed on the system
@@ -814,7 +881,8 @@ router.post('/init/:projectName', async (req, res) => {
814
881
  // Run taskmaster init command
815
882
  const initProcess = spawn('npx', ['task-master', 'init'], {
816
883
  cwd: projectPath,
817
- stdio: ['pipe', 'pipe', 'pipe']
884
+ stdio: ['pipe', 'pipe', 'pipe'],
885
+ env: await buildTaskMasterCliEnv()
818
886
  });
819
887
 
820
888
  let stdout = '';
@@ -917,7 +985,8 @@ router.post('/add-task/:projectName', async (req, res) => {
917
985
  // Run task-master add-task command
918
986
  const addTaskProcess = spawn('npx', args, {
919
987
  cwd: projectPath,
920
- stdio: ['pipe', 'pipe', 'pipe']
988
+ stdio: ['pipe', 'pipe', 'pipe'],
989
+ env: await buildTaskMasterCliEnv()
921
990
  });
922
991
 
923
992
  let stdout = '';
@@ -997,7 +1066,8 @@ router.put('/update-task/:projectName/:taskId', async (req, res) => {
997
1066
  if (status && Object.keys(req.body).length === 1) {
998
1067
  const setStatusProcess = spawn('npx', ['task-master-ai', 'set-status', `--id=${taskId}`, `--status=${status}`], {
999
1068
  cwd: projectPath,
1000
- stdio: ['pipe', 'pipe', 'pipe']
1069
+ stdio: ['pipe', 'pipe', 'pipe'],
1070
+ env: await buildTaskMasterCliEnv()
1001
1071
  });
1002
1072
 
1003
1073
  let stdout = '';
@@ -1049,7 +1119,8 @@ router.put('/update-task/:projectName/:taskId', async (req, res) => {
1049
1119
 
1050
1120
  const updateProcess = spawn('npx', ['task-master-ai', 'update-task', `--id=${taskId}`, `--prompt=${prompt}`], {
1051
1121
  cwd: projectPath,
1052
- stdio: ['pipe', 'pipe', 'pipe']
1122
+ stdio: ['pipe', 'pipe', 'pipe'],
1123
+ env: await buildTaskMasterCliEnv()
1053
1124
  });
1054
1125
 
1055
1126
  let stdout = '';
@@ -1148,7 +1219,8 @@ router.post('/parse-prd/:projectName', async (req, res) => {
1148
1219
  // Run task-master parse-prd command
1149
1220
  const parsePRDProcess = spawn('npx', args, {
1150
1221
  cwd: projectPath,
1151
- stdio: ['pipe', 'pipe', 'pipe']
1222
+ stdio: ['pipe', 'pipe', 'pipe'],
1223
+ env: await buildTaskMasterCliEnv()
1152
1224
  });
1153
1225
 
1154
1226
  let stdout = '';