@pixelbyte-software/pixcode 1.38.5 → 1.39.0
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.
- package/CODE_OF_CONDUCT.md +41 -0
- package/CONTRIBUTING.md +156 -0
- package/README.md +223 -92
- package/SECURITY.md +46 -0
- package/dist/assets/{index-DHC6THrb.js → index-Bf_F3oaY.js} +165 -165
- package/dist/assets/index-H1zs5zWL.css +32 -0
- package/dist/docs.html +17 -3
- package/dist/features.html +28 -7
- package/dist/index.html +2 -2
- package/dist/landing.html +162 -109
- package/dist/llms-full.txt +9 -3
- package/dist/llms.txt +3 -2
- package/dist/site.css +158 -2
- package/dist/sitemap.xml +8 -8
- package/dist-server/server/gemini-cli.js +9 -1
- package/dist-server/server/gemini-cli.js.map +1 -1
- package/dist-server/server/modules/orchestration/a2a/routes.js +16 -14
- package/dist-server/server/modules/orchestration/a2a/routes.js.map +1 -1
- package/dist-server/server/modules/orchestration/workspace/docker-workspace.js +2 -1
- package/dist-server/server/modules/orchestration/workspace/docker-workspace.js.map +1 -1
- package/dist-server/server/modules/orchestration/workspace/workspace-manager.js +4 -0
- package/dist-server/server/modules/orchestration/workspace/workspace-manager.js.map +1 -1
- package/dist-server/server/modules/orchestration/workspace/worktree-workspace.js +2 -1
- package/dist-server/server/modules/orchestration/workspace/worktree-workspace.js.map +1 -1
- package/dist-server/server/opencode-cli.js +9 -1
- package/dist-server/server/opencode-cli.js.map +1 -1
- package/dist-server/server/qwen-code-cli.js +9 -1
- package/dist-server/server/qwen-code-cli.js.map +1 -1
- package/dist-server/server/routes/git.js +7 -1
- package/dist-server/server/routes/git.js.map +1 -1
- package/dist-server/server/routes/taskmaster.js +74 -11
- package/dist-server/server/routes/taskmaster.js.map +1 -1
- package/dist-server/server/services/taskmaster-config.js +128 -0
- package/dist-server/server/services/taskmaster-config.js.map +1 -0
- package/package.json +7 -2
- package/scripts/smoke/chat-composer-fixed-layout.mjs +34 -0
- package/scripts/smoke/command-center-agent-writes.mjs +27 -1
- package/scripts/smoke/orchestration-mobile-scroll.mjs +29 -0
- package/scripts/smoke/orchestration-runtime-guards.mjs +48 -0
- package/scripts/smoke/taskmaster-config.mjs +59 -0
- package/server/gemini-cli.js +9 -1
- package/server/modules/orchestration/a2a/routes.ts +16 -14
- package/server/modules/orchestration/workspace/docker-workspace.ts +2 -1
- package/server/modules/orchestration/workspace/workspace-manager.ts +5 -0
- package/server/modules/orchestration/workspace/worktree-workspace.ts +2 -1
- package/server/opencode-cli.js +9 -1
- package/server/qwen-code-cli.js +9 -1
- package/server/routes/git.js +7 -1
- package/server/routes/taskmaster.js +83 -11
- package/server/services/taskmaster-config.js +146 -0
- package/dist/assets/index-B-OgjpDF.css +0 -32
|
@@ -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');
|
package/server/gemini-cli.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/server/opencode-cli.js
CHANGED
|
@@ -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 =
|
|
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') ||
|
package/server/qwen-code-cli.js
CHANGED
|
@@ -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 =
|
|
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') ||
|
package/server/routes/git.js
CHANGED
|
@@ -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
|
|
41
|
-
const taskMasterPath = findExecutableOnPath('task-master', env);
|
|
65
|
+
const executable = findTaskMasterExecutable(env);
|
|
42
66
|
|
|
43
|
-
if (!
|
|
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(
|
|
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:
|
|
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:
|
|
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 = '';
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
const CONFIG_FILE = path.join(os.homedir(), '.pixcode', 'taskmaster-config.json');
|
|
6
|
+
|
|
7
|
+
export const TASKMASTER_CONFIG_FIELDS = Object.freeze({
|
|
8
|
+
anthropicApiKey: { env: 'ANTHROPIC_API_KEY', secret: true },
|
|
9
|
+
anthropicBaseUrl: { env: 'ANTHROPIC_BASE_URL', secret: false },
|
|
10
|
+
perplexityApiKey: { env: 'PERPLEXITY_API_KEY', secret: true },
|
|
11
|
+
openaiApiKey: { env: 'OPENAI_API_KEY', secret: true },
|
|
12
|
+
openaiBaseUrl: { env: 'OPENAI_BASE_URL', secret: false },
|
|
13
|
+
openaiCompatibleApiKey: {
|
|
14
|
+
env: 'OPENAI_API_KEY',
|
|
15
|
+
secret: true,
|
|
16
|
+
aliases: ['OPENAI_COMPATIBLE_API_KEY', 'CUSTOM_OPENAI_API_KEY'],
|
|
17
|
+
},
|
|
18
|
+
openaiCompatibleBaseUrl: {
|
|
19
|
+
env: 'OPENAI_BASE_URL',
|
|
20
|
+
secret: false,
|
|
21
|
+
aliases: ['OPENAI_COMPATIBLE_BASE_URL', 'CUSTOM_OPENAI_BASE_URL'],
|
|
22
|
+
},
|
|
23
|
+
openaiCompatibleModel: {
|
|
24
|
+
env: 'OPENAI_MODEL',
|
|
25
|
+
secret: false,
|
|
26
|
+
aliases: ['OPENAI_COMPATIBLE_MODEL', 'TASKMASTER_OPENAI_COMPATIBLE_MODEL'],
|
|
27
|
+
},
|
|
28
|
+
googleApiKey: { env: 'GOOGLE_API_KEY', secret: true, aliases: ['GEMINI_API_KEY'] },
|
|
29
|
+
openrouterApiKey: { env: 'OPENROUTER_API_KEY', secret: true },
|
|
30
|
+
azureOpenaiApiKey: { env: 'AZURE_OPENAI_API_KEY', secret: true },
|
|
31
|
+
azureOpenaiEndpoint: { env: 'AZURE_OPENAI_ENDPOINT', secret: false },
|
|
32
|
+
ollamaApiKey: { env: 'OLLAMA_API_KEY', secret: true },
|
|
33
|
+
ollamaBaseUrl: { env: 'OLLAMA_BASE_URL', secret: false },
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
async function readStore() {
|
|
37
|
+
try {
|
|
38
|
+
const raw = await fs.readFile(CONFIG_FILE, 'utf8');
|
|
39
|
+
const parsed = JSON.parse(raw);
|
|
40
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
41
|
+
} catch {
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function writeStore(next) {
|
|
47
|
+
await fs.mkdir(path.dirname(CONFIG_FILE), { recursive: true });
|
|
48
|
+
await fs.writeFile(CONFIG_FILE, JSON.stringify(next, null, 2), { mode: 0o600 });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function summarizeConfig(store) {
|
|
52
|
+
const fields = {};
|
|
53
|
+
for (const [key, definition] of Object.entries(TASKMASTER_CONFIG_FIELDS)) {
|
|
54
|
+
const value = typeof store[key] === 'string' ? store[key].trim() : '';
|
|
55
|
+
fields[key] = definition.secret
|
|
56
|
+
? { hasValue: Boolean(value), updatedAt: store.updatedAt || null }
|
|
57
|
+
: { value: value || '', hasValue: Boolean(value), updatedAt: store.updatedAt || null };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
fields,
|
|
62
|
+
updatedAt: store.updatedAt || null,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getManagedEnvKeys() {
|
|
67
|
+
const keys = new Set();
|
|
68
|
+
for (const definition of Object.values(TASKMASTER_CONFIG_FIELDS)) {
|
|
69
|
+
keys.add(definition.env);
|
|
70
|
+
for (const alias of definition.aliases || []) {
|
|
71
|
+
keys.add(alias);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return keys;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function buildTaskMasterConfigEnvValues(store = {}) {
|
|
78
|
+
const values = new Map();
|
|
79
|
+
for (const [key, definition] of Object.entries(TASKMASTER_CONFIG_FIELDS)) {
|
|
80
|
+
const value = typeof store[key] === 'string' ? store[key].trim() : '';
|
|
81
|
+
if (!value) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
values.set(definition.env, value);
|
|
86
|
+
for (const alias of definition.aliases || []) {
|
|
87
|
+
values.set(alias, value);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return values;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function getTaskMasterConfigSummary() {
|
|
95
|
+
return summarizeConfig(await readStore());
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function saveTaskMasterConfig(input = {}) {
|
|
99
|
+
const current = await readStore();
|
|
100
|
+
const next = { ...current };
|
|
101
|
+
|
|
102
|
+
for (const key of Object.keys(TASKMASTER_CONFIG_FIELDS)) {
|
|
103
|
+
if (!Object.prototype.hasOwnProperty.call(input, key)) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const value = typeof input[key] === 'string' ? input[key].trim() : '';
|
|
108
|
+
if (!value) {
|
|
109
|
+
delete next[key];
|
|
110
|
+
} else {
|
|
111
|
+
next[key] = value;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
next.updatedAt = new Date().toISOString();
|
|
116
|
+
await writeStore(next);
|
|
117
|
+
await applyTaskMasterConfigToEnv();
|
|
118
|
+
return summarizeConfig(next);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function buildTaskMasterEnv(baseEnv = process.env) {
|
|
122
|
+
const store = await readStore();
|
|
123
|
+
const env = { ...baseEnv };
|
|
124
|
+
const values = buildTaskMasterConfigEnvValues(store);
|
|
125
|
+
|
|
126
|
+
for (const [envKey, value] of values.entries()) {
|
|
127
|
+
env[envKey] = value;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return env;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function applyTaskMasterConfigToEnv() {
|
|
134
|
+
const store = await readStore();
|
|
135
|
+
const values = buildTaskMasterConfigEnvValues(store);
|
|
136
|
+
|
|
137
|
+
for (const envKey of getManagedEnvKeys()) {
|
|
138
|
+
if (!values.has(envKey)) {
|
|
139
|
+
delete process.env[envKey];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
for (const [envKey, value] of values.entries()) {
|
|
144
|
+
process.env[envKey] = value;
|
|
145
|
+
}
|
|
146
|
+
}
|