@nexus-ai-nexoralabs/cli 0.1.3 → 0.1.4
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/.env.example +16 -16
- package/LICENSE +21 -21
- package/README.md +135 -106
- package/dist/api/index.d.ts +4 -0
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +9 -3
- package/dist/api/index.js.map +1 -1
- package/dist/chat/chatService.d.ts.map +1 -1
- package/dist/chat/chatService.js +22 -3
- package/dist/chat/chatService.js.map +1 -1
- package/dist/chat/conversationService.d.ts +7 -1
- package/dist/chat/conversationService.d.ts.map +1 -1
- package/dist/chat/conversationService.js +48 -15
- package/dist/chat/conversationService.js.map +1 -1
- package/dist/cli/agentRunner.d.ts.map +1 -1
- package/dist/cli/agentRunner.js +88 -8
- package/dist/cli/agentRunner.js.map +1 -1
- package/dist/cli/chatRunner.d.ts +65 -0
- package/dist/cli/chatRunner.d.ts.map +1 -0
- package/dist/cli/chatRunner.js +289 -0
- package/dist/cli/chatRunner.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/inputEngine.d.ts +10 -0
- package/dist/cli/inputEngine.d.ts.map +1 -1
- package/dist/cli/inputEngine.js +28 -1
- package/dist/cli/inputEngine.js.map +1 -1
- package/dist/cli/main.d.ts +4 -0
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +89 -25
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/uiFormatters.d.ts +11 -1
- package/dist/cli/uiFormatters.d.ts.map +1 -1
- package/dist/cli/uiFormatters.js +253 -15
- package/dist/cli/uiFormatters.js.map +1 -1
- package/dist/intelligence/modelGateway.d.ts +1 -0
- package/dist/intelligence/modelGateway.d.ts.map +1 -1
- package/dist/intelligence/modelGateway.js +63 -6
- package/dist/intelligence/modelGateway.js.map +1 -1
- package/dist/intelligence/modelRegistry.d.ts +2 -2
- package/dist/intelligence/modelRegistry.d.ts.map +1 -1
- package/dist/intelligence/modelRegistry.js +72 -31
- package/dist/intelligence/modelRegistry.js.map +1 -1
- package/dist/intelligence/modelRouter.d.ts.map +1 -1
- package/dist/intelligence/modelRouter.js +62 -16
- package/dist/intelligence/modelRouter.js.map +1 -1
- package/dist/intelligence/providers/embeddedInferenceProvider.d.ts.map +1 -1
- package/dist/intelligence/providers/embeddedInferenceProvider.js +2224 -6
- package/dist/intelligence/providers/embeddedInferenceProvider.js.map +1 -1
- package/dist/intelligence/runtime/runtimeManager.d.ts +5 -0
- package/dist/intelligence/runtime/runtimeManager.d.ts.map +1 -1
- package/dist/intelligence/runtime/runtimeManager.js +64 -4
- package/dist/intelligence/runtime/runtimeManager.js.map +1 -1
- package/dist/orchestration/agentExecutionService.d.ts.map +1 -1
- package/dist/orchestration/agentExecutionService.js +22 -0
- package/dist/orchestration/agentExecutionService.js.map +1 -1
- package/dist/orchestration/agentTaskService.d.ts +24 -10
- package/dist/orchestration/agentTaskService.d.ts.map +1 -1
- package/dist/orchestration/agentTaskService.js +76 -13
- package/dist/orchestration/agentTaskService.js.map +1 -1
- package/dist/orchestration/codingAgentService.d.ts.map +1 -1
- package/dist/orchestration/codingAgentService.js +2 -0
- package/dist/orchestration/codingAgentService.js.map +1 -1
- package/dist/orchestration/goalCompletionVerifier.d.ts +9 -0
- package/dist/orchestration/goalCompletionVerifier.d.ts.map +1 -1
- package/dist/orchestration/goalCompletionVerifier.js +131 -9
- package/dist/orchestration/goalCompletionVerifier.js.map +1 -1
- package/dist/orchestration/index.d.ts +16 -2
- package/dist/orchestration/index.d.ts.map +1 -1
- package/dist/orchestration/index.js +20 -4
- package/dist/orchestration/index.js.map +1 -1
- package/dist/orchestration/placeholderDetector.d.ts +74 -0
- package/dist/orchestration/placeholderDetector.d.ts.map +1 -0
- package/dist/orchestration/placeholderDetector.js +741 -0
- package/dist/orchestration/placeholderDetector.js.map +1 -0
- package/dist/orchestration/planner.d.ts.map +1 -1
- package/dist/orchestration/planner.js +94 -55
- package/dist/orchestration/planner.js.map +1 -1
- package/dist/orchestration/projectOrchestrator.d.ts +63 -0
- package/dist/orchestration/projectOrchestrator.d.ts.map +1 -0
- package/dist/orchestration/projectOrchestrator.js +555 -0
- package/dist/orchestration/projectOrchestrator.js.map +1 -0
- package/dist/orchestration/taskDecompositionService.d.ts +36 -0
- package/dist/orchestration/taskDecompositionService.d.ts.map +1 -0
- package/dist/orchestration/taskDecompositionService.js +367 -0
- package/dist/orchestration/taskDecompositionService.js.map +1 -0
- package/dist/orchestration/taskDecompositionTypes.d.ts +62 -0
- package/dist/orchestration/taskDecompositionTypes.d.ts.map +1 -0
- package/dist/orchestration/taskDecompositionTypes.js +5 -0
- package/dist/orchestration/taskDecompositionTypes.js.map +1 -0
- package/dist/preview/previewServer.d.ts.map +1 -1
- package/dist/preview/previewServer.js +28 -7
- package/dist/preview/previewServer.js.map +1 -1
- package/dist/preview/previewService.d.ts +32 -3
- package/dist/preview/previewService.d.ts.map +1 -1
- package/dist/preview/previewService.js +370 -108
- package/dist/preview/previewService.js.map +1 -1
- package/dist/preview/previewTypes.d.ts +1 -1
- package/dist/preview/previewTypes.d.ts.map +1 -1
- package/dist/storage/index.d.ts +2 -0
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +3 -0
- package/dist/storage/index.js.map +1 -1
- package/dist/storage/migration.d.ts.map +1 -1
- package/dist/storage/migration.js +79 -0
- package/dist/storage/migration.js.map +1 -1
- package/dist/storage/repositories/agentTaskRepository.d.ts +13 -0
- package/dist/storage/repositories/agentTaskRepository.d.ts.map +1 -1
- package/dist/storage/repositories/agentTaskRepository.js +87 -25
- package/dist/storage/repositories/agentTaskRepository.js.map +1 -1
- package/dist/storage/repositories/conversationRepository.d.ts +5 -1
- package/dist/storage/repositories/conversationRepository.d.ts.map +1 -1
- package/dist/storage/repositories/conversationRepository.js +69 -0
- package/dist/storage/repositories/conversationRepository.js.map +1 -1
- package/dist/storage/repositories/messageRepository.d.ts +3 -0
- package/dist/storage/repositories/messageRepository.d.ts.map +1 -1
- package/dist/storage/repositories/messageRepository.js +29 -0
- package/dist/storage/repositories/messageRepository.js.map +1 -1
- package/dist/storage/repositories/taskArtifactRepository.d.ts +19 -0
- package/dist/storage/repositories/taskArtifactRepository.d.ts.map +1 -0
- package/dist/storage/repositories/taskArtifactRepository.js +82 -0
- package/dist/storage/repositories/taskArtifactRepository.js.map +1 -0
- package/dist/storage/repositories/types.d.ts +32 -0
- package/dist/storage/repositories/types.d.ts.map +1 -1
- package/dist/tools/adapters/filesystemAdapter.d.ts.map +1 -1
- package/dist/tools/adapters/filesystemAdapter.js +8 -1
- package/dist/tools/adapters/filesystemAdapter.js.map +1 -1
- package/dist/ui/public/app.js +777 -777
- package/dist/ui/public/index.html +117 -117
- package/dist/ui/public/styles.css +413 -413
- package/package.json +64 -63
|
@@ -6,27 +6,56 @@
|
|
|
6
6
|
* safe browser launching.
|
|
7
7
|
*/
|
|
8
8
|
import type { AgentTaskRepository } from '../storage/repositories/agentTaskRepository.js';
|
|
9
|
+
import type { TaskArtifactRepository } from '../storage/repositories/taskArtifactRepository.js';
|
|
9
10
|
import type { AgentTask } from '../storage/repositories/types.js';
|
|
10
11
|
import type { PreviewResult, PreviewStatus, DiscoveredArtifacts } from './previewTypes.js';
|
|
11
12
|
export declare class PreviewService {
|
|
12
13
|
private taskRepo;
|
|
13
14
|
private workspaceRoot;
|
|
15
|
+
private artifactRepo?;
|
|
14
16
|
private server;
|
|
15
17
|
private logger;
|
|
16
18
|
private activeTask;
|
|
17
|
-
constructor(taskRepo: AgentTaskRepository, workspaceRoot: string, logLevel?: import('../common/logger.js').LogLevel);
|
|
19
|
+
constructor(taskRepo: AgentTaskRepository, workspaceRoot: string, logLevel?: import('../common/logger.js').LogLevel, artifactRepo?: TaskArtifactRepository | undefined);
|
|
20
|
+
setArtifactRepository(artifactRepo: TaskArtifactRepository): void;
|
|
21
|
+
/**
|
|
22
|
+
* Resolve target task based on explicit index (number), task ID (string), or latest task (undefined).
|
|
23
|
+
*
|
|
24
|
+
* 1. No argument: targets the latest task across ALL states in the task repository.
|
|
25
|
+
* 2. Number / Numeric string (e.g. 2, "2", "#2"): targets the N-th task (1-indexed) from deterministic task history.
|
|
26
|
+
* 3. Task ID string: targets the specific task by its unique ID.
|
|
27
|
+
*/
|
|
28
|
+
resolveTargetTask(target?: string | number): {
|
|
29
|
+
task: AgentTask | null;
|
|
30
|
+
targetType: 'latest' | 'index' | 'id';
|
|
31
|
+
targetIndex?: number;
|
|
32
|
+
error?: string;
|
|
33
|
+
};
|
|
18
34
|
/**
|
|
19
35
|
* Resolve latest completed task or specific task by ID.
|
|
36
|
+
* Maintained for backwards compatibility.
|
|
20
37
|
*/
|
|
21
38
|
resolveCompletedTask(taskId?: string): AgentTask | null;
|
|
22
39
|
/**
|
|
23
40
|
* Discover files and project entry point created by a completed task.
|
|
41
|
+
*
|
|
42
|
+
* PRIORITY 1: Task's persisted artifact record in SQLite (TaskArtifactRepository).
|
|
43
|
+
* PRIORITY 2: Task-scoped plan steps (legacy tasks only).
|
|
44
|
+
* PRIORITY 3: FAIL SAFELY. Never fall back to arbitrary workspace scanning, mtime, or other tasks.
|
|
24
45
|
*/
|
|
25
46
|
discoverArtifacts(task: AgentTask): DiscoveredArtifacts;
|
|
26
47
|
/**
|
|
27
|
-
* Launch preview for latest
|
|
48
|
+
* Launch preview for latest task, specific task number (#N), or specified task ID.
|
|
49
|
+
*
|
|
50
|
+
* STRICT SEMANTICS:
|
|
51
|
+
* 1. If no target is given, resolves the user's latest task.
|
|
52
|
+
* 2. If target is numeric (e.g. 2, "2", "#2"), resolves task #N from /tasks list.
|
|
53
|
+
* 3. If target is string, resolves task by ID.
|
|
54
|
+
* 4. If target task is FAILED -> returns status TASK_FAILED with reason, blocks preview, NO FALLBACK.
|
|
55
|
+
* 5. If target task is INCOMPLETE -> returns status TASK_INCOMPLETE, blocks preview, NO FALLBACK.
|
|
56
|
+
* 6. If target task is COMPLETED -> verifies artifact for THAT task, then launches preview.
|
|
28
57
|
*/
|
|
29
|
-
preview(
|
|
58
|
+
preview(target?: string | number): Promise<PreviewResult>;
|
|
30
59
|
/**
|
|
31
60
|
* Stop active preview server.
|
|
32
61
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"previewService.d.ts","sourceRoot":"","sources":["../../src/preview/previewService.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gDAAgD,CAAC;AAC1F,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;
|
|
1
|
+
{"version":3,"file":"previewService.d.ts","sourceRoot":"","sources":["../../src/preview/previewService.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gDAAgD,CAAC;AAC1F,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mDAAmD,CAAC;AAChG,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAElE,OAAO,KAAK,EACV,aAAa,EACb,aAAa,EACb,mBAAmB,EACpB,MAAM,mBAAmB,CAAC;AAW3B,qBAAa,cAAc;IAMvB,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,aAAa;IAErB,OAAO,CAAC,YAAY,CAAC;IARvB,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,UAAU,CAA0B;gBAGlC,QAAQ,EAAE,mBAAmB,EAC7B,aAAa,EAAE,MAAM,EAC7B,QAAQ,GAAE,OAAO,qBAAqB,EAAE,QAAiB,EACjD,YAAY,CAAC,EAAE,sBAAsB,YAAA;IAM/C,qBAAqB,CAAC,YAAY,EAAE,sBAAsB,GAAG,IAAI;IAIjE;;;;;;OAMG;IACH,iBAAiB,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG;QAC3C,IAAI,EAAE,SAAS,GAAG,IAAI,CAAC;QACvB,UAAU,EAAE,QAAQ,GAAG,OAAO,GAAG,IAAI,CAAC;QACtC,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB;IAwDD;;;OAGG;IACH,oBAAoB,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAQvD;;;;;;OAMG;IACH,iBAAiB,CAAC,IAAI,EAAE,SAAS,GAAG,mBAAmB;IAmTvD;;;;;;;;;;OAUG;IACG,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAoI/D;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,aAAa,CAAC;IAoBpC;;OAEG;IACH,SAAS,IAAI,aAAa;IAc1B;;OAEG;IACG,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAqCjD"}
|
|
@@ -9,139 +9,344 @@ import * as fs from 'fs';
|
|
|
9
9
|
import * as path from 'path';
|
|
10
10
|
import { execFile } from 'child_process';
|
|
11
11
|
import { PreviewServer } from './previewServer.js';
|
|
12
|
+
import { verifyHtmlContentSubstance } from '../orchestration/placeholderDetector.js';
|
|
12
13
|
import { Logger } from '../common/logger.js';
|
|
14
|
+
function isSubpathOrEqual(parentDir, childPath) {
|
|
15
|
+
const isWin = process.platform === 'win32';
|
|
16
|
+
const p = isWin ? parentDir.toLowerCase() : parentDir;
|
|
17
|
+
const c = isWin ? childPath.toLowerCase() : childPath;
|
|
18
|
+
return c === p || c.startsWith(p + (isWin ? '\\' : '/')) || c.startsWith(p + path.sep);
|
|
19
|
+
}
|
|
13
20
|
export class PreviewService {
|
|
14
21
|
taskRepo;
|
|
15
22
|
workspaceRoot;
|
|
23
|
+
artifactRepo;
|
|
16
24
|
server;
|
|
17
25
|
logger;
|
|
18
26
|
activeTask = null;
|
|
19
|
-
constructor(taskRepo, workspaceRoot, logLevel = 'info') {
|
|
27
|
+
constructor(taskRepo, workspaceRoot, logLevel = 'info', artifactRepo) {
|
|
20
28
|
this.taskRepo = taskRepo;
|
|
21
29
|
this.workspaceRoot = workspaceRoot;
|
|
30
|
+
this.artifactRepo = artifactRepo;
|
|
22
31
|
this.logger = new Logger('PreviewService', logLevel);
|
|
23
32
|
this.server = new PreviewServer(logLevel);
|
|
24
33
|
}
|
|
34
|
+
setArtifactRepository(artifactRepo) {
|
|
35
|
+
this.artifactRepo = artifactRepo;
|
|
36
|
+
}
|
|
25
37
|
/**
|
|
26
|
-
* Resolve
|
|
38
|
+
* Resolve target task based on explicit index (number), task ID (string), or latest task (undefined).
|
|
39
|
+
*
|
|
40
|
+
* 1. No argument: targets the latest task across ALL states in the task repository.
|
|
41
|
+
* 2. Number / Numeric string (e.g. 2, "2", "#2"): targets the N-th task (1-indexed) from deterministic task history.
|
|
42
|
+
* 3. Task ID string: targets the specific task by its unique ID.
|
|
27
43
|
*/
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
44
|
+
resolveTargetTask(target) {
|
|
45
|
+
const allTasks = this.taskRepo.listAll();
|
|
46
|
+
if (allTasks.length === 0) {
|
|
47
|
+
return {
|
|
48
|
+
task: null,
|
|
49
|
+
targetType: 'latest',
|
|
50
|
+
error: 'No tasks recorded yet.',
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (target === undefined || target === null || (typeof target === 'string' && target.trim() === '')) {
|
|
54
|
+
return {
|
|
55
|
+
task: allTasks[0],
|
|
56
|
+
targetType: 'latest',
|
|
57
|
+
targetIndex: 1,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const str = String(target).trim();
|
|
61
|
+
// Check if target is a numeric index (e.g. 1, 2, "1", "2", "#1", "#2")
|
|
62
|
+
if (/^#?\d+$/.test(str)) {
|
|
63
|
+
const idx = parseInt(str.replace(/^#/, ''), 10);
|
|
64
|
+
if (idx >= 1) {
|
|
65
|
+
if (idx > allTasks.length) {
|
|
66
|
+
return {
|
|
67
|
+
task: null,
|
|
68
|
+
targetType: 'index',
|
|
69
|
+
targetIndex: idx,
|
|
70
|
+
error: `Task #${idx} does not exist. Available tasks: 1 to ${allTasks.length}. Run /tasks to view all tasks.`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
task: allTasks[idx - 1],
|
|
75
|
+
targetType: 'index',
|
|
76
|
+
targetIndex: idx,
|
|
77
|
+
};
|
|
34
78
|
}
|
|
35
|
-
return null;
|
|
36
79
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
80
|
+
// Otherwise, treat as explicit taskId
|
|
81
|
+
const task = this.taskRepo.findById(str);
|
|
82
|
+
if (!task) {
|
|
83
|
+
return {
|
|
84
|
+
task: null,
|
|
85
|
+
targetType: 'id',
|
|
86
|
+
error: `Task with ID '${str}' not found.`,
|
|
87
|
+
};
|
|
40
88
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
89
|
+
return {
|
|
90
|
+
task,
|
|
91
|
+
targetType: 'id',
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Resolve latest completed task or specific task by ID.
|
|
96
|
+
* Maintained for backwards compatibility.
|
|
97
|
+
*/
|
|
98
|
+
resolveCompletedTask(taskId) {
|
|
99
|
+
const resolved = this.resolveTargetTask(taskId);
|
|
100
|
+
if (resolved.task && resolved.task.state === 'completed') {
|
|
101
|
+
return resolved.task;
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
48
104
|
}
|
|
49
105
|
/**
|
|
50
106
|
* Discover files and project entry point created by a completed task.
|
|
107
|
+
*
|
|
108
|
+
* PRIORITY 1: Task's persisted artifact record in SQLite (TaskArtifactRepository).
|
|
109
|
+
* PRIORITY 2: Task-scoped plan steps (legacy tasks only).
|
|
110
|
+
* PRIORITY 3: FAIL SAFELY. Never fall back to arbitrary workspace scanning, mtime, or other tasks.
|
|
51
111
|
*/
|
|
52
112
|
discoverArtifacts(task) {
|
|
53
113
|
const canonicalWorkspace = fs.realpathSync(path.resolve(this.workspaceRoot));
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
114
|
+
// ── PRIORITY 1: Persisted Artifact Metadata from SQLite ────────────────────
|
|
115
|
+
if (this.artifactRepo) {
|
|
116
|
+
let persistedArtifact = this.artifactRepo.findByTaskId(task.id);
|
|
117
|
+
// If this is a parent task, check artifacts owned by its child tasks
|
|
118
|
+
if (!persistedArtifact) {
|
|
119
|
+
const childTasks = this.taskRepo.findByParentTaskId(task.id);
|
|
120
|
+
if (childTasks.length > 0) {
|
|
121
|
+
const childArtifacts = childTasks
|
|
122
|
+
.map((c) => this.artifactRepo.findByTaskId(c.id))
|
|
123
|
+
.filter((a) => !!a);
|
|
124
|
+
if (childArtifacts.length > 0) {
|
|
125
|
+
const mergedFiles = Array.from(new Set(childArtifacts.flatMap((a) => a.files)));
|
|
126
|
+
const primary = childArtifacts[childArtifacts.length - 1];
|
|
127
|
+
persistedArtifact = {
|
|
128
|
+
...primary,
|
|
129
|
+
id: `art-${task.id}`,
|
|
130
|
+
taskId: task.id,
|
|
131
|
+
files: mergedFiles,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// If this is a child task, check parent artifact
|
|
137
|
+
if (!persistedArtifact && task.parentTaskId) {
|
|
138
|
+
persistedArtifact = this.artifactRepo.findByTaskId(task.parentTaskId);
|
|
139
|
+
}
|
|
140
|
+
if (persistedArtifact) {
|
|
141
|
+
let canonicalProjectDir = persistedArtifact.projectRoot;
|
|
142
|
+
try {
|
|
143
|
+
if (fs.existsSync(canonicalProjectDir)) {
|
|
144
|
+
canonicalProjectDir = fs.realpathSync(canonicalProjectDir);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// keep path
|
|
149
|
+
}
|
|
150
|
+
const entryPath = path.join(canonicalProjectDir, persistedArtifact.entryPoint || 'index.html');
|
|
151
|
+
const entryExists = fs.existsSync(entryPath) && fs.statSync(entryPath).isFile();
|
|
152
|
+
if (entryExists) {
|
|
153
|
+
// Scan project directory for associated web files
|
|
154
|
+
const allProjectFiles = [];
|
|
155
|
+
const scanDir = (currentDir, relPrefix) => {
|
|
156
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
157
|
+
for (const entry of entries) {
|
|
158
|
+
if (['node_modules', '.git', 'dist', 'build', '.nexus'].includes(entry.name))
|
|
159
|
+
continue;
|
|
160
|
+
const relPath = relPrefix ? `${relPrefix}/${entry.name}` : entry.name;
|
|
161
|
+
if (entry.isDirectory()) {
|
|
162
|
+
scanDir(path.join(currentDir, entry.name), relPath);
|
|
163
|
+
}
|
|
164
|
+
else if (entry.isFile()) {
|
|
165
|
+
allProjectFiles.push(relPath.replace(/\\/g, '/'));
|
|
66
166
|
}
|
|
67
167
|
}
|
|
168
|
+
};
|
|
169
|
+
try {
|
|
170
|
+
scanDir(canonicalProjectDir, '');
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// ignore scan failure
|
|
68
174
|
}
|
|
175
|
+
const uniqueFiles = Array.from(new Set([...persistedArtifact.files, ...allProjectFiles]));
|
|
176
|
+
this.logger.info(`Resolved preview artifact from SQLite metadata for task '${task.id}'`, {
|
|
177
|
+
projectDir: canonicalProjectDir,
|
|
178
|
+
entryFile: persistedArtifact.entryPoint,
|
|
179
|
+
fileCount: uniqueFiles.length,
|
|
180
|
+
});
|
|
181
|
+
return {
|
|
182
|
+
projectDir: canonicalProjectDir,
|
|
183
|
+
entryFile: persistedArtifact.entryPoint || 'index.html',
|
|
184
|
+
files: uniqueFiles,
|
|
185
|
+
hasIndexHtml: true,
|
|
186
|
+
isWebProject: true,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
this.logger.warn(`Persisted artifact entry point missing on disk for task '${task.id}'`, { entryPath });
|
|
191
|
+
return {
|
|
192
|
+
projectDir: canonicalProjectDir,
|
|
193
|
+
entryFile: persistedArtifact.entryPoint || 'index.html',
|
|
194
|
+
files: [],
|
|
195
|
+
hasIndexHtml: false,
|
|
196
|
+
isWebProject: false,
|
|
197
|
+
};
|
|
69
198
|
}
|
|
70
199
|
}
|
|
200
|
+
}
|
|
201
|
+
// ── PRIORITY 2: Task-Scoped Plan Steps (Legacy Plan Fallback) ──────────────
|
|
202
|
+
const createdFiles = [];
|
|
203
|
+
const createdDirs = [];
|
|
204
|
+
const extractFromObj = (obj) => {
|
|
205
|
+
if (!obj || typeof obj !== 'object')
|
|
206
|
+
return;
|
|
207
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
208
|
+
if (typeof value === 'string' && value.trim()) {
|
|
209
|
+
const val = value.trim();
|
|
210
|
+
const lowerKey = key.toLowerCase();
|
|
211
|
+
if (lowerKey.includes('path') ||
|
|
212
|
+
lowerKey.includes('file') ||
|
|
213
|
+
lowerKey.includes('dir') ||
|
|
214
|
+
lowerKey.includes('dest') ||
|
|
215
|
+
lowerKey === 'target' ||
|
|
216
|
+
val.endsWith('.html') ||
|
|
217
|
+
val.endsWith('.css') ||
|
|
218
|
+
val.endsWith('.js')) {
|
|
219
|
+
if (val.endsWith('.html') || val.endsWith('.css') || val.endsWith('.js') || val.includes('/') || val.includes('\\')) {
|
|
220
|
+
createdFiles.push(val);
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
createdDirs.push(val);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
else if (typeof value === 'object' && value !== null) {
|
|
228
|
+
extractFromObj(value);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
if (task.plan) {
|
|
233
|
+
try {
|
|
234
|
+
const planObj = typeof task.plan === 'string' ? JSON.parse(task.plan) : task.plan;
|
|
235
|
+
extractFromObj(planObj);
|
|
236
|
+
}
|
|
71
237
|
catch (err) {
|
|
72
238
|
this.logger.warn(`Failed to parse plan JSON for task '${task.id}'`, { error: String(err) });
|
|
73
239
|
}
|
|
74
240
|
}
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
241
|
+
// Extract folder name from task title if explicit or matches existing subdirectories
|
|
242
|
+
try {
|
|
243
|
+
const wsEntries = fs.readdirSync(canonicalWorkspace, { withFileTypes: true });
|
|
244
|
+
const dirNames = wsEntries
|
|
245
|
+
.filter((d) => d.isDirectory() && !d.name.startsWith('.') && d.name !== 'node_modules')
|
|
246
|
+
.map((d) => d.name);
|
|
247
|
+
const folderMatch = task.title.match(/(?:folder|directory|project|called|named|create)\s+[`"']?([a-zA-Z0-9_\-]+)[`"']?/i);
|
|
248
|
+
if (folderMatch && folderMatch[1]) {
|
|
249
|
+
const candidate = folderMatch[1].trim();
|
|
250
|
+
if (dirNames.some((d) => d.toLowerCase() === candidate.toLowerCase())) {
|
|
251
|
+
createdDirs.push(candidate);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const titleWords = task.title.toLowerCase().replace(/[^a-z0-9_\-\s]/g, ' ').split(/\s+/).filter((w) => w.length > 2 && w !== 'task' && w !== 'the');
|
|
255
|
+
for (const d of dirNames) {
|
|
256
|
+
const lowerName = d.toLowerCase();
|
|
257
|
+
if (titleWords.some((w) => lowerName.includes(w) || (w.length > 3 && w.includes(lowerName)))) {
|
|
258
|
+
createdDirs.push(d);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// If task title mentions subdirectory/folder/app and exactly one subdirectory with index.html exists
|
|
262
|
+
if (createdDirs.length === 0 && /(?:subdirectory|folder|project|app|site|web)/i.test(task.title)) {
|
|
263
|
+
const subdirsWithIndex = dirNames.filter((d) => fs.existsSync(path.join(canonicalWorkspace, d, 'index.html')));
|
|
264
|
+
if (subdirsWithIndex.length === 1) {
|
|
265
|
+
createdDirs.push(subdirsWithIndex[0]);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
84
268
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
269
|
+
catch {
|
|
270
|
+
/* ignore */
|
|
271
|
+
}
|
|
272
|
+
const validAbsFiles = [];
|
|
273
|
+
for (const f of createdFiles) {
|
|
274
|
+
const abs = path.resolve(canonicalWorkspace, f);
|
|
275
|
+
if (isSubpathOrEqual(canonicalWorkspace, abs) && fs.existsSync(abs) && fs.statSync(abs).isFile()) {
|
|
276
|
+
validAbsFiles.push(fs.realpathSync(abs));
|
|
90
277
|
}
|
|
91
278
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
canonicalProjectDir = canonicalWorkspace;
|
|
279
|
+
const validAbsDirs = [];
|
|
280
|
+
for (const d of createdDirs) {
|
|
281
|
+
const abs = path.resolve(canonicalWorkspace, d);
|
|
282
|
+
if (isSubpathOrEqual(canonicalWorkspace, abs) && fs.existsSync(abs) && fs.statSync(abs).isDirectory()) {
|
|
283
|
+
validAbsDirs.push(fs.realpathSync(abs));
|
|
284
|
+
}
|
|
99
285
|
}
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const subdirsWithIndex = [];
|
|
107
|
-
try {
|
|
108
|
-
const entries = fs.readdirSync(canonicalWorkspace, { withFileTypes: true });
|
|
109
|
-
for (const entry of entries) {
|
|
110
|
-
if (entry.isDirectory() &&
|
|
111
|
-
!entry.name.startsWith('.') &&
|
|
112
|
-
!['node_modules', 'dist', 'build', 'coverage', 'src', 'data', 'bin', 'docs'].includes(entry.name)) {
|
|
113
|
-
const subDir = path.join(canonicalWorkspace, entry.name);
|
|
114
|
-
const indexPath = path.join(subDir, 'index.html');
|
|
115
|
-
if (fs.existsSync(indexPath) && fs.statSync(indexPath).isFile()) {
|
|
116
|
-
const stat = fs.statSync(indexPath);
|
|
117
|
-
subdirsWithIndex.push({
|
|
118
|
-
dir: subDir,
|
|
119
|
-
mtime: stat.mtimeMs,
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
}
|
|
286
|
+
// Root workspace fallback ONLY when task is a web task and no specific subdirectory was claimed and root index.html exists
|
|
287
|
+
if (validAbsFiles.length === 0 && validAbsDirs.length === 0) {
|
|
288
|
+
if (!/(?:python|data|analysis|backend|fibonacci|non-web)/i.test(task.title)) {
|
|
289
|
+
const rootIndex = path.join(canonicalWorkspace, 'index.html');
|
|
290
|
+
if (fs.existsSync(rootIndex) && fs.statSync(rootIndex).isFile()) {
|
|
291
|
+
validAbsFiles.push(fs.realpathSync(rootIndex));
|
|
123
292
|
}
|
|
124
293
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
294
|
+
}
|
|
295
|
+
let targetProjectDir = null;
|
|
296
|
+
let entryFile = 'index.html';
|
|
297
|
+
const indexAbs = validAbsFiles.find((f) => path.basename(f).toLowerCase() === 'index.html');
|
|
298
|
+
if (indexAbs) {
|
|
299
|
+
targetProjectDir = path.dirname(indexAbs);
|
|
300
|
+
entryFile = path.basename(indexAbs);
|
|
301
|
+
}
|
|
302
|
+
if (!targetProjectDir && validAbsFiles.length > 0) {
|
|
303
|
+
const fileDirs = validAbsFiles.map((f) => path.dirname(f));
|
|
304
|
+
const candidateDirs = Array.from(new Set([...fileDirs, ...validAbsDirs]));
|
|
305
|
+
for (const dir of candidateDirs) {
|
|
306
|
+
const indexPath = path.join(dir, 'index.html');
|
|
307
|
+
if (fs.existsSync(indexPath) && fs.statSync(indexPath).isFile()) {
|
|
308
|
+
targetProjectDir = dir;
|
|
309
|
+
entryFile = 'index.html';
|
|
310
|
+
break;
|
|
134
311
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
312
|
+
}
|
|
313
|
+
if (!targetProjectDir) {
|
|
314
|
+
targetProjectDir = fileDirs[0];
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (!targetProjectDir && validAbsDirs.length > 0) {
|
|
318
|
+
for (const dir of validAbsDirs) {
|
|
319
|
+
const indexPath = path.join(dir, 'index.html');
|
|
320
|
+
if (fs.existsSync(indexPath) && fs.statSync(indexPath).isFile()) {
|
|
321
|
+
targetProjectDir = dir;
|
|
322
|
+
entryFile = 'index.html';
|
|
323
|
+
break;
|
|
141
324
|
}
|
|
142
325
|
}
|
|
326
|
+
if (!targetProjectDir) {
|
|
327
|
+
targetProjectDir = validAbsDirs[0];
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
// If NO task-specific artifact could be identified for THIS task, FAIL SAFELY.
|
|
331
|
+
// NEVER fall back to global workspace, root index.html, or arbitrary mtime directories.
|
|
332
|
+
if (!targetProjectDir) {
|
|
333
|
+
this.logger.info(`No task-specific previewable artifact found for task '${task.id}'`);
|
|
334
|
+
return {
|
|
335
|
+
projectDir: canonicalWorkspace,
|
|
336
|
+
entryFile: 'index.html',
|
|
337
|
+
files: [],
|
|
338
|
+
hasIndexHtml: false,
|
|
339
|
+
isWebProject: false,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
// Canonicalize target project directory
|
|
343
|
+
let canonicalProjectDir = fs.existsSync(targetProjectDir)
|
|
344
|
+
? fs.realpathSync(targetProjectDir)
|
|
345
|
+
: canonicalWorkspace;
|
|
346
|
+
if (!isSubpathOrEqual(canonicalWorkspace, canonicalProjectDir)) {
|
|
347
|
+
canonicalProjectDir = canonicalWorkspace;
|
|
143
348
|
}
|
|
144
|
-
// Scan selected projectDir for
|
|
349
|
+
// Scan selected projectDir for files
|
|
145
350
|
const allProjectFiles = [];
|
|
146
351
|
let hasIndexHtml = false;
|
|
147
352
|
let hasWebFiles = false;
|
|
@@ -175,8 +380,8 @@ export class PreviewService {
|
|
|
175
380
|
this.logger.warn(`Error scanning directory '${canonicalProjectDir}'`, { error: String(err) });
|
|
176
381
|
}
|
|
177
382
|
}
|
|
178
|
-
|
|
179
|
-
const uniqueFiles = Array.from(new Set([...
|
|
383
|
+
const relativeCreatedFiles = validAbsFiles.map((abs) => path.relative(canonicalProjectDir, abs));
|
|
384
|
+
const uniqueFiles = Array.from(new Set([...relativeCreatedFiles, ...allProjectFiles]));
|
|
180
385
|
return {
|
|
181
386
|
projectDir: canonicalProjectDir,
|
|
182
387
|
entryFile,
|
|
@@ -186,24 +391,59 @@ export class PreviewService {
|
|
|
186
391
|
};
|
|
187
392
|
}
|
|
188
393
|
/**
|
|
189
|
-
* Launch preview for latest
|
|
394
|
+
* Launch preview for latest task, specific task number (#N), or specified task ID.
|
|
395
|
+
*
|
|
396
|
+
* STRICT SEMANTICS:
|
|
397
|
+
* 1. If no target is given, resolves the user's latest task.
|
|
398
|
+
* 2. If target is numeric (e.g. 2, "2", "#2"), resolves task #N from /tasks list.
|
|
399
|
+
* 3. If target is string, resolves task by ID.
|
|
400
|
+
* 4. If target task is FAILED -> returns status TASK_FAILED with reason, blocks preview, NO FALLBACK.
|
|
401
|
+
* 5. If target task is INCOMPLETE -> returns status TASK_INCOMPLETE, blocks preview, NO FALLBACK.
|
|
402
|
+
* 6. If target task is COMPLETED -> verifies artifact for THAT task, then launches preview.
|
|
190
403
|
*/
|
|
191
|
-
async preview(
|
|
192
|
-
const
|
|
193
|
-
if (!task) {
|
|
194
|
-
if (taskId) {
|
|
195
|
-
return {
|
|
196
|
-
success: false,
|
|
197
|
-
status: 'NO_COMPLETED_TASK',
|
|
198
|
-
message: `Task '${taskId}' is not a completed task or does not exist.`,
|
|
199
|
-
};
|
|
200
|
-
}
|
|
404
|
+
async preview(target) {
|
|
405
|
+
const resolved = this.resolveTargetTask(target);
|
|
406
|
+
if (!resolved.task) {
|
|
201
407
|
return {
|
|
202
408
|
success: false,
|
|
203
409
|
status: 'NO_COMPLETED_TASK',
|
|
204
|
-
message: 'No
|
|
410
|
+
message: resolved.error || 'No task is available to preview.',
|
|
205
411
|
};
|
|
206
412
|
}
|
|
413
|
+
const task = resolved.task;
|
|
414
|
+
const targetLabel = resolved.targetType === 'latest'
|
|
415
|
+
? 'Latest task'
|
|
416
|
+
: resolved.targetType === 'index'
|
|
417
|
+
? `Task #${resolved.targetIndex}`
|
|
418
|
+
: `Task '${task.id}'`;
|
|
419
|
+
// 1. Check if task failed -> NEVER FALL BACK TO AN OLDER TASK
|
|
420
|
+
if (task.state === 'failed') {
|
|
421
|
+
this.logger.warn(`Preview blocked: ${targetLabel} is in FAILED state`, {
|
|
422
|
+
taskId: task.id,
|
|
423
|
+
errorCategory: task.errorCategory,
|
|
424
|
+
});
|
|
425
|
+
return {
|
|
426
|
+
success: false,
|
|
427
|
+
status: 'TASK_FAILED',
|
|
428
|
+
task,
|
|
429
|
+
error: task.errorCategory || 'Execution Failed',
|
|
430
|
+
message: `Preview unavailable: ${targetLabel} '${task.title}' failed (${task.errorCategory || 'Execution Failed'}).`,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
// 2. Check if task is incomplete -> NEVER FALL BACK
|
|
434
|
+
if (task.state !== 'completed') {
|
|
435
|
+
this.logger.warn(`Preview blocked: ${targetLabel} is not completed (state: ${task.state})`, {
|
|
436
|
+
taskId: task.id,
|
|
437
|
+
state: task.state,
|
|
438
|
+
});
|
|
439
|
+
return {
|
|
440
|
+
success: false,
|
|
441
|
+
status: 'TASK_INCOMPLETE',
|
|
442
|
+
task,
|
|
443
|
+
message: `Preview unavailable: ${targetLabel} '${task.title}' is currently ${task.state.toUpperCase()}. Only completed tasks can be previewed.`,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
// 3. Task is COMPLETED: Discover artifacts for THIS task only
|
|
207
447
|
const artifacts = this.discoverArtifacts(task);
|
|
208
448
|
if (!artifacts.isWebProject) {
|
|
209
449
|
return {
|
|
@@ -211,7 +451,7 @@ export class PreviewService {
|
|
|
211
451
|
status: 'NOT_WEB_PROJECT',
|
|
212
452
|
task,
|
|
213
453
|
artifactsSummary: artifacts.files,
|
|
214
|
-
message:
|
|
454
|
+
message: `${targetLabel} '${task.title}' does not contain a previewable web project.`,
|
|
215
455
|
};
|
|
216
456
|
}
|
|
217
457
|
if (!artifacts.hasIndexHtml) {
|
|
@@ -220,12 +460,12 @@ export class PreviewService {
|
|
|
220
460
|
status: 'MISSING_INDEX',
|
|
221
461
|
task,
|
|
222
462
|
detectedFiles: artifacts.files,
|
|
223
|
-
message:
|
|
463
|
+
message: `No index.html entry point was found for task '${task.title}'.`,
|
|
224
464
|
};
|
|
225
465
|
}
|
|
226
466
|
// Ensure projectDir is inside workspace
|
|
227
467
|
const canonicalWorkspace = fs.realpathSync(path.resolve(this.workspaceRoot));
|
|
228
|
-
if (!artifacts.projectDir
|
|
468
|
+
if (!isSubpathOrEqual(canonicalWorkspace, artifacts.projectDir)) {
|
|
229
469
|
return {
|
|
230
470
|
success: false,
|
|
231
471
|
status: 'ERROR',
|
|
@@ -233,6 +473,25 @@ export class PreviewService {
|
|
|
233
473
|
error: 'Security Error: Preview directory is outside workspace boundaries.',
|
|
234
474
|
};
|
|
235
475
|
}
|
|
476
|
+
// Verify artifact is a real implementation and passes content integrity checks
|
|
477
|
+
const entryFullPath = path.join(artifacts.projectDir, artifacts.entryFile || 'index.html');
|
|
478
|
+
if (fs.existsSync(entryFullPath)) {
|
|
479
|
+
const content = fs.readFileSync(entryFullPath, 'utf8');
|
|
480
|
+
const contentVerif = verifyHtmlContentSubstance(content, task.title);
|
|
481
|
+
if (!contentVerif.valid) {
|
|
482
|
+
this.logger.warn(`Preview blocked: artifact failed content verification for task '${task.id}'`, {
|
|
483
|
+
category: contentVerif.errorCategory,
|
|
484
|
+
reason: contentVerif.reason,
|
|
485
|
+
});
|
|
486
|
+
return {
|
|
487
|
+
success: false,
|
|
488
|
+
status: 'PLACEHOLDER_ARTIFACT_BLOCKED',
|
|
489
|
+
task,
|
|
490
|
+
detectedFiles: artifacts.files,
|
|
491
|
+
message: `Preview blocked: generated artifact failed integrity/content verification. ${contentVerif.reason || ''}`.trim(),
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
}
|
|
236
495
|
try {
|
|
237
496
|
const serverInfo = await this.server.start(artifacts.projectDir, artifacts.entryFile || 'index.html');
|
|
238
497
|
this.activeTask = task;
|
|
@@ -296,6 +555,9 @@ export class PreviewService {
|
|
|
296
555
|
* Open local preview URL safely in default browser without shell injection risk.
|
|
297
556
|
*/
|
|
298
557
|
async openBrowser(url) {
|
|
558
|
+
if (process.env.NODE_ENV === 'test' || process.env.VITEST) {
|
|
559
|
+
return true;
|
|
560
|
+
}
|
|
299
561
|
if (!url.startsWith('http://127.0.0.1:')) {
|
|
300
562
|
this.logger.warn(`Refusing to open non-loopback URL: '${url}'`);
|
|
301
563
|
return false;
|