@ronkovic/aad 0.6.1 → 0.6.3
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/package.json +1 -1
- package/src/modules/cli/commands/__tests__/task-dispatch-handler.test.ts +34 -0
- package/src/modules/cli/commands/task-dispatch-handler.ts +107 -2
- package/src/modules/dashboard/ui/dashboard.html +31 -30
- package/src/modules/git-workspace/__tests__/dependency-installer.test.ts +17 -0
- package/src/modules/git-workspace/dependency-installer.ts +6 -2
- package/src/modules/task-execution/__tests__/tester-verify.test.ts +83 -2
- package/src/modules/task-execution/phases/tester-verify.ts +8 -2
package/package.json
CHANGED
|
@@ -143,3 +143,37 @@ describe("registerTaskDispatchHandler", () => {
|
|
|
143
143
|
app.worktreeManager.createTaskWorktree = originalCreateTaskWorktree;
|
|
144
144
|
}, 5000);
|
|
145
145
|
});
|
|
146
|
+
|
|
147
|
+
describe("detectWorkspace (monorepo support)", () => {
|
|
148
|
+
test("detects sub-workspace from filesToModify in monorepo", async () => {
|
|
149
|
+
// Since we can't easily mock the planning module imports,
|
|
150
|
+
// we verify the code implementation instead
|
|
151
|
+
const handlerCode = await Bun.file("src/modules/cli/commands/task-dispatch-handler.ts").text();
|
|
152
|
+
|
|
153
|
+
// Verify monorepo detection logic exists
|
|
154
|
+
expect(handlerCode).toContain("const monorepo = await isMonorepo");
|
|
155
|
+
expect(handlerCode).toContain("if (monorepo && filesToModify && filesToModify.length > 0)");
|
|
156
|
+
expect(handlerCode).toContain("resolveSubWorkspace");
|
|
157
|
+
expect(handlerCode).toContain("Resolved sub-workspace for task");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("resolveSubWorkspace helper function logic", async () => {
|
|
161
|
+
// Verify resolveSubWorkspace function exists and has correct logic
|
|
162
|
+
const handlerCode = await Bun.file("src/modules/cli/commands/task-dispatch-handler.ts").text();
|
|
163
|
+
|
|
164
|
+
expect(handlerCode).toContain("function resolveSubWorkspace");
|
|
165
|
+
expect(handlerCode).toContain("longest path prefix matching");
|
|
166
|
+
expect(handlerCode).toContain("matchCounts");
|
|
167
|
+
expect(handlerCode).toContain("file.startsWith(ws");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("fallback to root detection when not monorepo", async () => {
|
|
171
|
+
// When not a monorepo, should use root-level detection
|
|
172
|
+
// (We verify this by checking the code structure)
|
|
173
|
+
const handlerCode = await Bun.file("src/modules/cli/commands/task-dispatch-handler.ts").text();
|
|
174
|
+
|
|
175
|
+
// Verify fallback path exists
|
|
176
|
+
expect(handlerCode).toContain("Fallback: root-level detection");
|
|
177
|
+
expect(handlerCode).toContain("const projectType = await detectProjectType(worktreePath");
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
detectOrm,
|
|
20
20
|
detectArchitecturePattern,
|
|
21
21
|
createBunFileChecker,
|
|
22
|
+
isMonorepo,
|
|
23
|
+
detectWorkspaces,
|
|
22
24
|
} from "../../planning";
|
|
23
25
|
import { copyTemplatesToWorktree, harvestMemory, installDependencies } from "../../git-workspace";
|
|
24
26
|
|
|
@@ -81,7 +83,7 @@ export function registerTaskDispatchHandler(ctx: TaskDispatchContext): void {
|
|
|
81
83
|
logger.warn({ templateError }, "Template copy failed (non-critical)");
|
|
82
84
|
}
|
|
83
85
|
|
|
84
|
-
const workspace = await detectWorkspace(worktreePath, logger);
|
|
86
|
+
const workspace = await detectWorkspace(worktreePath, logger, task.filesToModify);
|
|
85
87
|
|
|
86
88
|
// Install dependencies in task worktree
|
|
87
89
|
try {
|
|
@@ -206,10 +208,50 @@ export function registerTaskDispatchHandler(ctx: TaskDispatchContext): void {
|
|
|
206
208
|
*/
|
|
207
209
|
export async function detectWorkspace(
|
|
208
210
|
worktreePath: string,
|
|
209
|
-
logger: import("pino").Logger
|
|
211
|
+
logger: import("pino").Logger,
|
|
212
|
+
filesToModify?: string[]
|
|
210
213
|
): Promise<WorkspaceInfo> {
|
|
211
214
|
try {
|
|
212
215
|
const fileChecker = createBunFileChecker();
|
|
216
|
+
|
|
217
|
+
// Check if this is a monorepo
|
|
218
|
+
const monorepo = await isMonorepo(worktreePath, fileChecker);
|
|
219
|
+
|
|
220
|
+
if (monorepo && filesToModify && filesToModify.length > 0) {
|
|
221
|
+
// Get all sub-workspaces
|
|
222
|
+
const workspaces = await detectWorkspaces(worktreePath, fileChecker);
|
|
223
|
+
|
|
224
|
+
// Resolve the most appropriate sub-workspace based on filesToModify
|
|
225
|
+
const subWorkspace = resolveSubWorkspace(worktreePath, filesToModify, workspaces);
|
|
226
|
+
|
|
227
|
+
if (subWorkspace && subWorkspace !== worktreePath) {
|
|
228
|
+
logger.info({ subWorkspace, monorepo: true, filesToModify }, "Resolved sub-workspace for task");
|
|
229
|
+
|
|
230
|
+
// Detect workspace info from the sub-workspace directory
|
|
231
|
+
const projectType = await detectProjectType(subWorkspace, fileChecker);
|
|
232
|
+
const packageManager = await detectPackageManager(subWorkspace, fileChecker);
|
|
233
|
+
const testFramework = await detectTestFramework(subWorkspace, projectType, fileChecker);
|
|
234
|
+
const framework = await detectFramework(subWorkspace, projectType, fileChecker);
|
|
235
|
+
const orm = await detectOrm(subWorkspace, projectType, fileChecker);
|
|
236
|
+
const architecture = await detectArchitecturePattern(subWorkspace, fileChecker);
|
|
237
|
+
const language = await detectLanguage(subWorkspace, projectType);
|
|
238
|
+
|
|
239
|
+
const workspace: WorkspaceInfo = {
|
|
240
|
+
path: subWorkspace,
|
|
241
|
+
language,
|
|
242
|
+
packageManager,
|
|
243
|
+
framework: framework !== "unknown" ? framework : "none",
|
|
244
|
+
testFramework: mapTestFramework(testFramework),
|
|
245
|
+
orm: orm !== "unknown" ? orm : undefined,
|
|
246
|
+
architecturePattern: architecture !== "custom" && architecture !== "unknown" ? architecture : undefined,
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
logger.debug({ workspace, monorepo: true }, "Sub-workspace detected");
|
|
250
|
+
return workspace;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Fallback: root-level detection (original behavior)
|
|
213
255
|
const projectType = await detectProjectType(worktreePath, fileChecker);
|
|
214
256
|
const packageManager = await detectPackageManager(worktreePath, fileChecker);
|
|
215
257
|
const testFramework = await detectTestFramework(worktreePath, projectType, fileChecker);
|
|
@@ -243,6 +285,69 @@ export async function detectWorkspace(
|
|
|
243
285
|
}
|
|
244
286
|
}
|
|
245
287
|
|
|
288
|
+
/**
|
|
289
|
+
* Resolve the most appropriate sub-workspace for the given filesToModify
|
|
290
|
+
* Uses longest path prefix matching to determine the best sub-workspace
|
|
291
|
+
*/
|
|
292
|
+
function resolveSubWorkspace(
|
|
293
|
+
worktreePath: string,
|
|
294
|
+
filesToModify: string[],
|
|
295
|
+
workspaces: string[]
|
|
296
|
+
): string | null {
|
|
297
|
+
if (workspaces.length === 0) return null;
|
|
298
|
+
|
|
299
|
+
// Normalize workspace paths (remove worktree prefix if present)
|
|
300
|
+
const normalizedWorkspaces = workspaces.map((ws) => {
|
|
301
|
+
if (ws.startsWith(worktreePath)) {
|
|
302
|
+
return ws.slice(worktreePath.length).replace(/^\/+/, "");
|
|
303
|
+
}
|
|
304
|
+
return ws;
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// Count matches for each workspace
|
|
308
|
+
const matchCounts = new Map<string, number>();
|
|
309
|
+
|
|
310
|
+
for (const file of filesToModify) {
|
|
311
|
+
let bestMatch: string | null = null;
|
|
312
|
+
let bestMatchLength = 0;
|
|
313
|
+
|
|
314
|
+
for (let i = 0; i < normalizedWorkspaces.length; i++) {
|
|
315
|
+
const ws = normalizedWorkspaces[i];
|
|
316
|
+
const fullPath = workspaces[i];
|
|
317
|
+
|
|
318
|
+
// Skip root workspace (empty string) or undefined
|
|
319
|
+
if (!ws || ws === "" || ws === ".") continue;
|
|
320
|
+
|
|
321
|
+
// Check if file path starts with workspace path
|
|
322
|
+
if (file.startsWith(ws + "/") || file === ws) {
|
|
323
|
+
if (ws.length > bestMatchLength) {
|
|
324
|
+
bestMatch = fullPath ?? null;
|
|
325
|
+
bestMatchLength = ws.length;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (bestMatch) {
|
|
331
|
+
matchCounts.set(bestMatch, (matchCounts.get(bestMatch) ?? 0) + 1);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Return the workspace with most matches
|
|
336
|
+
if (matchCounts.size === 0) return null;
|
|
337
|
+
|
|
338
|
+
let maxCount = 0;
|
|
339
|
+
let bestWorkspace: string | null = null;
|
|
340
|
+
|
|
341
|
+
for (const [ws, count] of matchCounts) {
|
|
342
|
+
if (count > maxCount) {
|
|
343
|
+
maxCount = count;
|
|
344
|
+
bestWorkspace = ws;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return bestWorkspace;
|
|
349
|
+
}
|
|
350
|
+
|
|
246
351
|
async function detectLanguage(worktreePath: string, projectType: import("../../planning").ProjectType): Promise<string> {
|
|
247
352
|
const nodeTypes = ["nodejs", "nextjs", "express", "react"];
|
|
248
353
|
if (nodeTypes.includes(projectType)) {
|
|
@@ -586,14 +586,14 @@ tr:hover td {
|
|
|
586
586
|
<div class="log-entries" id="log-entries"></div>
|
|
587
587
|
</div>
|
|
588
588
|
|
|
589
|
-
<script type="module">class
|
|
589
|
+
<script type="module">class S{baseUrl;constructor(t){this.baseUrl=t}async getProgress(){let t=await fetch(`${this.baseUrl}/api/progress`);if(!t.ok)throw Error(`Failed to fetch progress: ${t.statusText}`);return t.json()}async getWorkers(){let t=await fetch(`${this.baseUrl}/api/workers`);if(!t.ok)throw Error(`Failed to fetch workers: ${t.statusText}`);return t.json()}async getTasks(){let t=await fetch(`${this.baseUrl}/api/tasks`);if(!t.ok)throw Error(`Failed to fetch tasks: ${t.statusText}`);return t.json()}async getGraph(){let t=await fetch(`${this.baseUrl}/api/graph`);if(!t.ok)throw Error(`Failed to fetch graph: ${t.statusText}`);return t.json()}async getTimeline(){let t=await fetch(`${this.baseUrl}/api/timeline`);if(!t.ok)throw Error(`Failed to fetch timeline: ${t.statusText}`);return t.json()}async getLogs(){let t=await fetch(`${this.baseUrl}/api/logs`);if(!t.ok)throw Error(`Failed to fetch logs: ${t.statusText}`);let i=await t.json();return Array.isArray(i)?i:i.entries||[]}async getTaskLogs(t){let i=await fetch(`${this.baseUrl}/api/tasks/${t}/logs`);if(!i.ok)throw Error(`Failed to fetch task logs: ${i.statusText}`);return i.json()}}class Z{url;onEvent;eventSource=null;status="disconnected";reconnectAttempts=0;reconnectTimer=null;connectionChangeListeners=[];constructor(t,i){this.url=t;this.onEvent=i}connect(){if(this.eventSource)try{this.eventSource.close()}catch(t){}this.eventSource=new EventSource(this.url),this.eventSource.addEventListener("open",()=>{this.reconnectAttempts=0,this.setStatus("connected")}),this.eventSource.addEventListener("heartbeat",()=>{this.setStatus("connected")}),this.eventSource.addEventListener("message",(t)=>{try{let i=JSON.parse(t.data);this.onEvent(i)}catch(i){console.error("SSE parse error:",i)}}),this.eventSource.addEventListener("error",()=>{this.reconnectAttempts++;let t=this.reconnectAttempts>3?"disconnected":"reconnecting";if(this.setStatus(t),this.eventSource)this.eventSource.close();let i=Math.min(this.reconnectAttempts*3000,9000);this.reconnectTimer=setTimeout(()=>{this.connect()},i)})}disconnect(){if(this.reconnectTimer)clearTimeout(this.reconnectTimer),this.reconnectTimer=null;if(this.eventSource){try{this.eventSource.close()}catch(t){}this.eventSource=null}this.setStatus("disconnected")}isConnected(){return this.status==="connected"}getStatus(){return this.status}getReconnectAttempts(){return this.reconnectAttempts}onConnectionChange(t){this.connectionChangeListeners.push(t)}setStatus(t){if(this.status!==t)this.status=t,this.connectionChangeListeners.forEach((i)=>{try{i(t)}catch(y){console.error("Connection change listener error:",y)}})}}class K{state={progress:{pending:0,running:0,completed:0,failed:0,total:0},tasks:[],workers:[],graph:{nodes:[],edges:[]},timeline:{tasks:[]},logs:[],taskPhases:{}};listeners=[];getState(){return{...this.state}}updateProgress(t){this.state.progress=t,this.notify()}updateTasks(t){this.state.tasks=t,this.notify()}updateTask(t,i){let y=this.state.tasks.findIndex((d)=>d.taskId===t);if(y>=0)this.state.tasks[y]={...this.state.tasks[y],...i},this.notify()}updateWorkers(t){this.state.workers=t,this.notify()}updateGraph(t){this.state.graph=t,this.notify()}updateTimeline(t){this.state.timeline=t,this.notify()}addLog(t){if(this.state.logs.unshift(t),this.state.logs.length>200)this.state.logs=this.state.logs.slice(0,200);this.notify()}updateTaskPhase(t,i,y){this.state.taskPhases[t]={phase:i,status:y},this.notify()}subscribe(t){return this.listeners.push(t),()=>{let i=this.listeners.indexOf(t);if(i>=0)this.listeners.splice(i,1)}}notify(){let t=this.getState();this.listeners.forEach((i)=>{try{i(t)}catch(y){console.error("State listener error:",y)}})}}function Y(t){let i=t.total||1,y=i>0?Math.round(t.completed/i*100):0,d=document.getElementById("pct-display"),p=document.getElementById("progress-fill"),f=document.getElementById("s-pending"),l=document.getElementById("s-running"),c=document.getElementById("s-completed"),h=document.getElementById("s-failed");if(d)d.textContent=`${y}%`;if(p)p.style.width=`${y}%`;if(f)f.textContent=String(t.pending||0);if(l)l.textContent=String(t.running||0);if(c)c.textContent=String(t.completed||0);if(h)h.textContent=String(t.failed||0)}function _(t){let i=document.createElement("div");return i.textContent=t,i.innerHTML}function U(t){let i=document.getElementById("worker-list"),y=document.getElementById("workers-count");if(!i)return;if(y)y.textContent=`(${t.length})`;if(!t.length){i.innerHTML='<div class="empty-msg">No workers</div>';return}i.innerHTML=t.map((d)=>{let p=d.currentTask?`<span class="worker-task">Task: ${_(String(d.currentTask))}</span>`:"";return`
|
|
590
590
|
<div class="worker-item">
|
|
591
|
-
<span class="worker-dot ${
|
|
592
|
-
<span class="worker-name">${
|
|
593
|
-
${
|
|
594
|
-
<span class="badge badge-${
|
|
591
|
+
<span class="worker-dot ${d.status}"></span>
|
|
592
|
+
<span class="worker-name">${_(String(d.id))}</span>
|
|
593
|
+
${p}
|
|
594
|
+
<span class="badge badge-${d.status}" style="margin-left:auto">${d.status}</span>
|
|
595
595
|
</div>
|
|
596
|
-
`}).join("")}function
|
|
596
|
+
`}).join("")}function e(t){let i=document.createElement("div");return i.textContent=t,i.innerHTML}class V{panelEl=null;overlayEl=null;constructor(){this.createPanel(),this.setupEventListeners()}createPanel(){this.overlayEl=document.createElement("div"),this.overlayEl.className="task-detail-overlay",this.overlayEl.style.cssText=`
|
|
597
597
|
display: none;
|
|
598
598
|
position: fixed;
|
|
599
599
|
top: 0;
|
|
@@ -618,13 +618,13 @@ tr:hover td {
|
|
|
618
618
|
overflow-y: auto;
|
|
619
619
|
z-index: 1001;
|
|
620
620
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
|
|
621
|
-
`,document.body.appendChild(this.overlayEl),document.body.appendChild(this.panelEl)}setupEventListeners(){this.overlayEl?.addEventListener("click",()=>this.close()),document.addEventListener("keydown",(t)=>{if(t.key==="Escape"&&this.isOpen())this.close()})}show(t,i){if(!this.panelEl||!this.overlayEl)return;let
|
|
621
|
+
`,document.body.appendChild(this.overlayEl),document.body.appendChild(this.panelEl)}setupEventListeners(){this.overlayEl?.addEventListener("click",()=>this.close()),document.addEventListener("keydown",(t)=>{if(t.key==="Escape"&&this.isOpen())this.close()})}show(t,i){if(!this.panelEl||!this.overlayEl)return;let y={red:"Red (Tests)",green:"Green (Implementation)",verify:"Verify",review:"Review",merge:"Merge"},d=i?`<div class="phase-info">
|
|
622
622
|
<strong>Current Phase:</strong>
|
|
623
623
|
<span class="activity-phase-${i.phase}">
|
|
624
|
-
${y
|
|
624
|
+
${e(y[i.phase]||i.phase)}
|
|
625
625
|
(${i.status})
|
|
626
626
|
</span>
|
|
627
|
-
</div>`:"",
|
|
627
|
+
</div>`:"",p=t.startTime?new Date(t.startTime).toLocaleString():"—",f=t.endTime?new Date(t.endTime).toLocaleString():"—",l=this.calculateDuration(t);this.panelEl.innerHTML=`
|
|
628
628
|
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 20px;">
|
|
629
629
|
<h2 style="color: #4fc3f7; margin: 0; font-size: 18px;">Task Details</h2>
|
|
630
630
|
<button class="close-btn" style="background: #ef5350; color: white; border: none; border-radius: 4px; padding: 6px 12px; cursor: pointer; font-size: 14px; font-weight: 600;">Close</button>
|
|
@@ -633,12 +633,12 @@ tr:hover td {
|
|
|
633
633
|
<div style="display: flex; flex-direction: column; gap: 16px;">
|
|
634
634
|
<div>
|
|
635
635
|
<div style="color: #9e9e9e; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px;">Task ID</div>
|
|
636
|
-
<div style="font-family: monospace; font-size: 13px; color: #e0e0e0;">${
|
|
636
|
+
<div style="font-family: monospace; font-size: 13px; color: #e0e0e0;">${e(String(t.taskId))}</div>
|
|
637
637
|
</div>
|
|
638
638
|
|
|
639
639
|
<div>
|
|
640
640
|
<div style="color: #9e9e9e; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px;">Title</div>
|
|
641
|
-
<div style="font-size: 15px; color: #e0e0e0; font-weight: 500;">${
|
|
641
|
+
<div style="font-size: 15px; color: #e0e0e0; font-weight: 500;">${e(t.title||"—")}</div>
|
|
642
642
|
</div>
|
|
643
643
|
|
|
644
644
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px;">
|
|
@@ -652,45 +652,46 @@ tr:hover td {
|
|
|
652
652
|
</div>
|
|
653
653
|
</div>
|
|
654
654
|
|
|
655
|
-
${
|
|
655
|
+
${d}
|
|
656
656
|
|
|
657
657
|
<div>
|
|
658
658
|
<div style="color: #9e9e9e; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px;">Dependencies</div>
|
|
659
659
|
<div style="font-size: 13px; color: #e0e0e0;">
|
|
660
|
-
${t.dependsOn&&t.dependsOn.length>0?t.dependsOn.map((
|
|
660
|
+
${t.dependsOn&&t.dependsOn.length>0?t.dependsOn.map((h)=>`<code style="background: #3a3a3a; padding: 2px 6px; border-radius: 3px; font-family: monospace; font-size: 11px;">${e(String(h))}</code>`).join(" "):"—"}
|
|
661
661
|
</div>
|
|
662
662
|
</div>
|
|
663
663
|
|
|
664
664
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px;">
|
|
665
665
|
<div>
|
|
666
666
|
<div style="color: #9e9e9e; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px;">Start Time</div>
|
|
667
|
-
<div style="font-size: 12px; color: #e0e0e0;">${
|
|
667
|
+
<div style="font-size: 12px; color: #e0e0e0;">${p}</div>
|
|
668
668
|
</div>
|
|
669
669
|
<div>
|
|
670
670
|
<div style="color: #9e9e9e; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px;">End Time</div>
|
|
671
|
-
<div style="font-size: 12px; color: #e0e0e0;">${
|
|
671
|
+
<div style="font-size: 12px; color: #e0e0e0;">${f}</div>
|
|
672
672
|
</div>
|
|
673
673
|
</div>
|
|
674
674
|
|
|
675
|
-
${
|
|
675
|
+
${l?`<div>
|
|
676
676
|
<div style="color: #9e9e9e; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px;">Duration</div>
|
|
677
|
-
<div style="font-size: 13px; color: #66bb6a; font-weight: 600;">${
|
|
677
|
+
<div style="font-size: 13px; color: #66bb6a; font-weight: 600;">${l}</div>
|
|
678
678
|
</div>`:""}
|
|
679
679
|
</div>
|
|
680
|
-
`;let
|
|
681
|
-
<tr data-task-id="${
|
|
682
|
-
<td style="font-family:monospace;font-size:11px">${
|
|
683
|
-
<td>${
|
|
684
|
-
<td><span class="badge badge-${
|
|
685
|
-
<td class="activity-cell">${
|
|
686
|
-
<td class="priority">${
|
|
687
|
-
<td class="deps">${
|
|
680
|
+
`;let c=this.panelEl.querySelector(".close-btn");if(c)c.addEventListener("click",()=>this.close());this.overlayEl.style.display="block",this.panelEl.style.display="block"}close(){if(this.overlayEl)this.overlayEl.style.display="none";if(this.panelEl)this.panelEl.style.display="none"}isOpen(){return this.panelEl?.style.display==="block"}calculateDuration(t){if(!t.startTime)return null;let i=new Date(t.startTime).getTime(),d=(t.endTime?new Date(t.endTime).getTime():Date.now())-i,p=Math.floor(d/1000),f=Math.floor(p/60),l=Math.floor(f/60);if(l>0)return`${l}h ${f%60}m`;else if(f>0)return`${f}m ${p%60}s`;else return`${p}s`}}function j(t){let i=document.createElement("div");return i.textContent=t,i.innerHTML}function r(t,i,y){if(t.status==="running"){let d=i[t.taskId];if(d){let f={red:"Red (Tests)",green:"Green (Impl)",verify:"Verify",review:"Review",merge:"Merge"}[d.phase]||d.phase,l=d.status==="completed"?"✓ ":d.status==="failed"?"✗ ":"";return`<span class="activity-phase-${d.phase}">${l}${j(f)}</span>`}return'<span class="activity-phase-green">Running</span>'}else if(y==="connected")return'<span class="activity-connected">Connected</span>';else if(y==="reconnecting")return'<span class="activity-reconnecting">Reconnecting...</span>';else return'<span class="activity-disconnected">Disconnected</span>'}var a=new V;function W(t,i,y){let d=document.getElementById("task-tbody"),p=document.getElementById("tasks-count");if(!d)return;if(p)p.textContent=`(${t.length})`;if(!t.length){d.innerHTML='<tr><td colspan="6" class="empty-msg">No tasks</td></tr>';return}d.innerHTML=t.map((f)=>{let l=(f.dependsOn||[]).map((h)=>j(String(h))).join(", ")||"—",c=r(f,i,y);return`
|
|
681
|
+
<tr data-task-id="${j(String(f.taskId))}" style="cursor: pointer;">
|
|
682
|
+
<td style="font-family:monospace;font-size:11px">${j(String(f.taskId))}</td>
|
|
683
|
+
<td>${j(f.title||"—")}</td>
|
|
684
|
+
<td><span class="badge badge-${f.status}">${f.status}</span></td>
|
|
685
|
+
<td class="activity-cell">${c}</td>
|
|
686
|
+
<td class="priority">${f.priority||0}</td>
|
|
687
|
+
<td class="deps">${l}</td>
|
|
688
688
|
</tr>
|
|
689
|
-
`}).join(""),
|
|
690
|
-
|
|
691
|
-
<span class="log-
|
|
692
|
-
<span>${
|
|
693
|
-
|
|
689
|
+
`}).join(""),d.querySelectorAll("tr[data-task-id]").forEach((f)=>{f.addEventListener("click",()=>{let l=f.dataset.taskId,c=t.find((h)=>String(h.taskId)===l);if(c){let h=i[c.taskId];a.show(c,h)}})})}var k={pending:"#9e9e9e",running:"#4fc3f7",completed:"#66bb6a",failed:"#ef5350"};function C(t){let i=document.getElementById("graph-container");if(!i)return;if(i.innerHTML="",!t.nodes.length){i.innerHTML='<div class="empty-msg">No graph data</div>';return}let y=i.clientWidth||600,d=400,p=20,f=Math.ceil(Math.sqrt(t.nodes.length)),l=Math.ceil(t.nodes.length/f),c=y/(f+1),h=d/(l+1),B=new Map;t.nodes.forEach((E,$)=>{let n=$%f,u=Math.floor($/f);B.set(E.id,{x:(n+1)*c,y:(u+1)*h})});let o=document.createElementNS("http://www.w3.org/2000/svg","svg");o.setAttribute("width",String(y)),o.setAttribute("height",String(d)),o.style.background="#1a1a1a";let D=document.createElementNS("http://www.w3.org/2000/svg","g");D.setAttribute("class","graph-edges"),t.edges.forEach((E)=>{let $=B.get(E.from),n=B.get(E.to);if(!$||!n)return;let u=document.createElementNS("http://www.w3.org/2000/svg","line");u.setAttribute("x1",String($.x)),u.setAttribute("y1",String($.y)),u.setAttribute("x2",String(n.x)),u.setAttribute("y2",String(n.y)),u.setAttribute("stroke","#424242"),u.setAttribute("stroke-width","2"),u.setAttribute("marker-end","url(#arrowhead)"),D.appendChild(u)});let g=document.createElementNS("http://www.w3.org/2000/svg","defs"),T=document.createElementNS("http://www.w3.org/2000/svg","marker");T.setAttribute("id","arrowhead"),T.setAttribute("markerWidth","10"),T.setAttribute("markerHeight","10"),T.setAttribute("refX","8"),T.setAttribute("refY","3"),T.setAttribute("orient","auto"),T.setAttribute("markerUnits","strokeWidth");let s=document.createElementNS("http://www.w3.org/2000/svg","polygon");s.setAttribute("points","0 0, 10 3, 0 6"),s.setAttribute("fill","#424242"),T.appendChild(s),g.appendChild(T),o.appendChild(g),o.appendChild(D);let v=document.createElementNS("http://www.w3.org/2000/svg","g");v.setAttribute("class","graph-nodes"),t.nodes.forEach((E)=>{let $=B.get(E.id);if(!$)return;let n=k[E.status],u=document.createElementNS("http://www.w3.org/2000/svg","circle");u.setAttribute("cx",String($.x)),u.setAttribute("cy",String($.y)),u.setAttribute("r",String(p)),u.setAttribute("fill",n),u.setAttribute("stroke","#2d2d2d"),u.setAttribute("stroke-width","2");let z=document.createElementNS("http://www.w3.org/2000/svg","text");z.setAttribute("x",String($.x)),z.setAttribute("y",String($.y+4)),z.setAttribute("text-anchor","middle"),z.setAttribute("fill","#1a1a1a"),z.setAttribute("font-size","10"),z.setAttribute("font-weight","600"),z.textContent=E.id.replace(/^task-0*/,"#");let O=document.createElementNS("http://www.w3.org/2000/svg","title");O.textContent=`${E.id} (${E.status})`,u.appendChild(O),v.appendChild(u),v.appendChild(z)}),o.appendChild(v),i.appendChild(o)}var tt={pending:"#9e9e9e",running:"#4fc3f7",completed:"#66bb6a",failed:"#ef5350"};function I(t){let i=document.getElementById("timeline-container");if(!i)return;if(i.innerHTML="",!t.tasks||t.tasks.length===0){i.innerHTML='<div class="empty-msg">No timeline data</div>';return}let y=new Date,d=[];for(let n of t.tasks){if(n.startTime)d.push(new Date(n.startTime).getTime());if(n.endTime)d.push(new Date(n.endTime).getTime());if(n.status==="running"&&n.startTime)d.push(y.getTime())}if(d.length===0){i.innerHTML='<div class="empty-msg">No timeline data</div>';return}let p=Math.min(...d),f=Math.max(...d),l=f-p||1,c=i.clientWidth||800,h=Math.max(300,t.tasks.length*30+60),B=20,o={top:40,right:20,bottom:20,left:100},D=document.createElementNS("http://www.w3.org/2000/svg","svg");D.setAttribute("width",String(c)),D.setAttribute("height",String(h)),D.style.background="#1a1a1a";let g=o.top-10,T=document.createElementNS("http://www.w3.org/2000/svg","g"),s=document.createElementNS("http://www.w3.org/2000/svg","line");s.setAttribute("x1",String(o.left)),s.setAttribute("y1",String(g)),s.setAttribute("x2",String(c-o.right)),s.setAttribute("y2",String(g)),s.setAttribute("stroke","#616161"),s.setAttribute("stroke-width","1"),T.appendChild(s);let v=document.createElementNS("http://www.w3.org/2000/svg","text");v.setAttribute("x",String(o.left)),v.setAttribute("y",String(g-5)),v.setAttribute("fill","#9e9e9e"),v.setAttribute("font-size","10"),v.textContent=J(new Date(p)),T.appendChild(v);let E=document.createElementNS("http://www.w3.org/2000/svg","text");E.setAttribute("x",String(c-o.right)),E.setAttribute("y",String(g-5)),E.setAttribute("text-anchor","end"),E.setAttribute("fill","#9e9e9e"),E.setAttribute("font-size","10"),E.textContent=J(new Date(f)),T.appendChild(E),D.appendChild(T);let $=document.createElementNS("http://www.w3.org/2000/svg","g");t.tasks.forEach((n,u)=>{let z=o.top+u*30,O=tt[n.status],N=document.createElementNS("http://www.w3.org/2000/svg","text");N.setAttribute("x",String(o.left-10)),N.setAttribute("y",String(z+B/2+4)),N.setAttribute("text-anchor","end"),N.setAttribute("fill","#e0e0e0"),N.setAttribute("font-size","11"),N.textContent=n.id.replace(/^task-0*/,"#"),$.appendChild(N);let q=o.left,P=0;if(n.status==="pending")return;if(n.startTime){let w=new Date(n.startTime).getTime();if(q=o.left+(w-p)/l*(c-o.left-o.right),n.status==="running"){let Q=y.getTime();P=o.left+(Q-p)/l*(c-o.left-o.right)-q}else if(n.endTime){let Q=new Date(n.endTime).getTime();P=o.left+(Q-p)/l*(c-o.left-o.right)-q}else P=5}let m=document.createElementNS("http://www.w3.org/2000/svg","rect");m.setAttribute("x",String(q)),m.setAttribute("y",String(z)),m.setAttribute("width",String(Math.max(P,2))),m.setAttribute("height",String(B)),m.setAttribute("fill",O),m.setAttribute("stroke","#2d2d2d"),m.setAttribute("stroke-width","1"),m.setAttribute("rx","2");let X=document.createElementNS("http://www.w3.org/2000/svg","title"),G=[`${n.id} (${n.status})`,n.startTime?`Start: ${J(new Date(n.startTime))}`:"",n.endTime?`End: ${J(new Date(n.endTime))}`:""].filter(Boolean).join(`
|
|
690
|
+
`);X.textContent=G,m.appendChild(X),$.appendChild(m)}),D.appendChild($),i.appendChild(D)}function J(t){let i=String(t.getHours()).padStart(2,"0"),y=String(t.getMinutes()).padStart(2,"0"),d=String(t.getSeconds()).padStart(2,"0");return`${i}:${y}:${d}`}function L(t){let i=document.createElement("div");return i.textContent=t,i.innerHTML}class H{filters={info:!0,warn:!0,error:!0,service:"",search:""};autoScroll=!0;constructor(){this.setupEventListeners(),this.loadFiltersFromStorage()}setupEventListeners(){document.querySelectorAll(".log-filters input[data-level]").forEach((p)=>{p.addEventListener("change",()=>{let f=p,l=f.dataset.level;this.filters[l]=f.checked,this.saveFiltersToStorage()})});let t=document.getElementById("svc-filter");if(t)t.addEventListener("change",()=>{this.filters.service=t.value,this.saveFiltersToStorage()});let i=document.getElementById("log-search");if(i)i.addEventListener("input",()=>{this.filters.search=i.value,this.saveFiltersToStorage()});let y=document.getElementById("filter-reset");if(y)y.addEventListener("click",()=>{this.resetFilters()});let d=document.getElementById("log-entries");if(d)d.addEventListener("scroll",()=>{let p=d.scrollHeight-d.scrollTop<=d.clientHeight+10;this.autoScroll=p})}renderLog(t,i=!0){let y=document.getElementById("log-entries");if(!y)return;let d=document.createElement("div"),p=t.level||"info";d.className=`log-entry log-${p}`,d.dataset.level=p,d.dataset.service=t.service||"";let f=t.timestamp?new Date(t.timestamp).toLocaleTimeString():"";if(d.innerHTML=`
|
|
691
|
+
<span class="log-time">${f}</span>
|
|
692
|
+
<span class="log-svc">${L(t.service||"")}</span>
|
|
693
|
+
<span>${L(t.message||"")}</span>
|
|
694
|
+
`,i)y.insertBefore(d,y.firstChild);else y.appendChild(d);if(this.applyFilterToEntry(d),this.autoScroll&&i)y.scrollTop=0;this.updateLogCount()}renderAllLogs(t){let i=document.getElementById("log-entries");if(!i)return;i.innerHTML="",t.forEach((y)=>this.renderLog(y,!1)),this.updateLogCount(),this.updateServiceFilter(t)}applyFilterToEntry(t){let i=t.dataset.level,y=i?this.filters[i]:!1;if(y&&this.filters.service)y=t.dataset.service===this.filters.service;if(y&&this.filters.search){let d=this.filters.search.toLowerCase();y=(t.textContent?.toLowerCase()||"").includes(d)}t.classList.toggle("log-hidden",!y)}updateLogCount(){let t=document.getElementById("log-entries"),i=document.getElementById("log-count");if(!t||!i)return;let y=t.querySelectorAll(".log-entry").length,d=t.querySelectorAll(".log-entry:not(.log-hidden)").length;i.textContent=`(Showing ${d}/${y})`}updateServiceFilter(t){let i=document.getElementById("svc-filter");if(!i)return;let y=new Set(t.map((p)=>p.service).filter(Boolean)),d=i.value;if(i.innerHTML='<option value="">All Services</option>',y.forEach((p)=>{let f=document.createElement("option");f.value=p,f.textContent=p,i.appendChild(f)}),d)i.value=d}resetFilters(){this.filters={info:!0,warn:!0,error:!0,service:"",search:""},document.querySelectorAll(".log-filters input[data-level]").forEach((y)=>{y.checked=!0});let t=document.getElementById("svc-filter");if(t)t.value="";let i=document.getElementById("log-search");if(i)i.value="";this.saveFiltersToStorage(),this.reapplyFilters()}reapplyFilters(){let t=document.getElementById("log-entries");if(!t)return;t.querySelectorAll(".log-entry").forEach((i)=>{this.applyFilterToEntry(i)}),this.updateLogCount()}loadFiltersFromStorage(){try{let t=localStorage.getItem("aad-log-filters");if(t){let i=JSON.parse(t);this.filters={...this.filters,...i},document.querySelectorAll(".log-filters input[data-level]").forEach((p)=>{let f=p,l=f.dataset.level;f.checked=this.filters[l]!==!1});let y=document.getElementById("svc-filter");if(y&&this.filters.service)y.value=this.filters.service;let d=document.getElementById("log-search");if(d&&this.filters.search)d.value=this.filters.search}}catch(t){console.error("Failed to load filters:",t)}}saveFiltersToStorage(){try{localStorage.setItem("aad-log-filters",JSON.stringify(this.filters))}catch(t){console.error("Failed to save filters:",t)}this.reapplyFilters()}}var A="http://localhost:7333",M=new S(A),x=new K,R=new Z(`${A}/events/all`,it),F=new H;x.subscribe((t)=>{Y(t.progress),U(t.workers),W(t.tasks,t.taskPhases,R.getStatus()),C(t.graph),I(t.timeline)});function it(t){switch(t.type){case"log:entry":x.addLog(t.entry),F.renderLog(t.entry,!0);break;case"progress:updated":x.updateProgress(t.state);break;case"execution:phase:started":x.updateTaskPhase(t.taskId,t.phase,"running");break;case"execution:phase:completed":x.updateTaskPhase(t.taskId,t.phase,"completed");break;case"execution:phase:failed":x.updateTaskPhase(t.taskId,t.phase,"failed");break;case"task:dispatched":case"task:completed":case"task:failed":if(t.task)x.updateTask(t.task.taskId,t.task);yt(),pt(),nt();break;case"worker:idle":case"worker:busy":ft();break;case"heartbeat":break}}async function dt(){try{let[t,i,y,d,p,f]=await Promise.all([M.getProgress(),M.getWorkers(),M.getTasks(),M.getGraph(),M.getTimeline(),M.getLogs()]);x.updateProgress(t.progress),x.updateWorkers(i.workers),x.updateTasks(y.tasks),x.updateGraph(d),x.updateTimeline(p),f.forEach((l)=>x.addLog(l)),F.renderAllLogs(f)}catch(t){console.error("Failed to load initial data:",t)}}async function yt(){try{let t=await M.getProgress();x.updateProgress(t.progress)}catch(t){console.error("Failed to refresh progress:",t)}}async function ft(){try{let t=await M.getWorkers();x.updateWorkers(t.workers)}catch(t){console.error("Failed to refresh workers:",t)}}async function pt(){try{let t=await M.getTimeline();x.updateTimeline(t)}catch(t){console.error("Failed to refresh timeline:",t)}}async function nt(){try{let t=await M.getGraph();x.updateGraph(t)}catch(t){console.error("Failed to refresh graph:",t)}}dt();R.connect();R.onConnectionChange((t)=>{console.log("SSE connection status:",t),W(x.getState().tasks,x.getState().taskPhases,t)});export{x as store,R as sseClient,M as apiClient};
|
|
694
695
|
</script>
|
|
695
696
|
</body>
|
|
696
697
|
</html>
|
|
@@ -115,4 +115,21 @@ describe("installDependencies integration", () => {
|
|
|
115
115
|
// The actual fallback is applied at runtime in installDependencies
|
|
116
116
|
// when yarn is not available in PATH, resolveAvailableCommand returns "npx"
|
|
117
117
|
});
|
|
118
|
+
|
|
119
|
+
test("constructs correct npx command when fallback is triggered", async () => {
|
|
120
|
+
// Test that when resolveAvailableCommand returns "npx",
|
|
121
|
+
// the spawn args preserve the original command name
|
|
122
|
+
const { resolveAvailableCommand } = await import("../dependency-installer");
|
|
123
|
+
|
|
124
|
+
// Simulate fallback scenario
|
|
125
|
+
const rawCmd = "yarn";
|
|
126
|
+
const args = ["install", "--frozen-lockfile"];
|
|
127
|
+
const resolved = await resolveAvailableCommand("nonexistent-binary-xyz123"); // Will return "npx"
|
|
128
|
+
|
|
129
|
+
// Construct spawn args as done in installDependencies
|
|
130
|
+
const spawnArgs = resolved === "npx" ? ["npx", rawCmd, ...args] : [resolved, ...args];
|
|
131
|
+
|
|
132
|
+
// Should be: ["npx", "yarn", "install", "--frozen-lockfile"]
|
|
133
|
+
expect(spawnArgs).toEqual(["npx", "yarn", "install", "--frozen-lockfile"]);
|
|
134
|
+
});
|
|
118
135
|
});
|
|
@@ -80,10 +80,14 @@ export async function installDependencies(
|
|
|
80
80
|
|
|
81
81
|
// Resolve command with fallback (e.g., yarn → npx if yarn not found)
|
|
82
82
|
const cmd = await resolveAvailableCommand(rawCmd!);
|
|
83
|
-
|
|
83
|
+
|
|
84
|
+
// If resolved to npx, preserve original command name as first argument
|
|
85
|
+
const spawnArgs = cmd === "npx" ? ["npx", rawCmd!, ...args] : [cmd, ...args];
|
|
86
|
+
|
|
87
|
+
logger.info({ cmd: spawnArgs[0], args: spawnArgs.slice(1), cwd: workspace.path }, "Installing dependencies");
|
|
84
88
|
|
|
85
89
|
// Bun.spawn で実行(default-spawner.ts と同パターン)
|
|
86
|
-
const proc = Bun.spawn(
|
|
90
|
+
const proc = Bun.spawn(spawnArgs, {
|
|
87
91
|
cwd: workspace.path,
|
|
88
92
|
stdout: "pipe",
|
|
89
93
|
stderr: "pipe",
|
|
@@ -200,7 +200,7 @@ describe("buildTestCommandWithFallback", () => {
|
|
|
200
200
|
expect(await buildTestCommandWithFallback(workspace)).toEqual(["./gradlew", "test"]);
|
|
201
201
|
});
|
|
202
202
|
|
|
203
|
-
test("returns fallback for unknown test framework", async () => {
|
|
203
|
+
test("returns fallback for unknown test framework with npm", async () => {
|
|
204
204
|
const workspace: WorkspaceInfo = {
|
|
205
205
|
path: "/path/to/workspace",
|
|
206
206
|
language: "unknown",
|
|
@@ -209,10 +209,91 @@ describe("buildTestCommandWithFallback", () => {
|
|
|
209
209
|
testFramework: "unknown",
|
|
210
210
|
};
|
|
211
211
|
|
|
212
|
-
// After fallback implementation, unknown should return npm test
|
|
213
212
|
expect(await buildTestCommandWithFallback(workspace)).toEqual(["npm", "test"]);
|
|
214
213
|
});
|
|
215
214
|
|
|
215
|
+
test("returns fallback for unknown test framework with bun", async () => {
|
|
216
|
+
const workspace: WorkspaceInfo = {
|
|
217
|
+
path: "/path/to/workspace",
|
|
218
|
+
language: "unknown",
|
|
219
|
+
packageManager: "bun",
|
|
220
|
+
framework: "unknown",
|
|
221
|
+
testFramework: "unknown",
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
expect(await buildTestCommandWithFallback(workspace)).toEqual(["bun", "test"]);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("returns fallback for unknown test framework with yarn", async () => {
|
|
228
|
+
const workspace: WorkspaceInfo = {
|
|
229
|
+
path: "/path/to/workspace",
|
|
230
|
+
language: "unknown",
|
|
231
|
+
packageManager: "yarn",
|
|
232
|
+
framework: "unknown",
|
|
233
|
+
testFramework: "unknown",
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
expect(await buildTestCommandWithFallback(workspace)).toEqual(["yarn", "test"]);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("returns fallback for unknown test framework with pnpm", async () => {
|
|
240
|
+
const workspace: WorkspaceInfo = {
|
|
241
|
+
path: "/path/to/workspace",
|
|
242
|
+
language: "unknown",
|
|
243
|
+
packageManager: "pnpm",
|
|
244
|
+
framework: "unknown",
|
|
245
|
+
testFramework: "unknown",
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
expect(await buildTestCommandWithFallback(workspace)).toEqual(["pnpm", "test"]);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("builds unknown test with yarn fallback when yarn is unavailable", async () => {
|
|
252
|
+
const workspace: WorkspaceInfo = {
|
|
253
|
+
path: "/path",
|
|
254
|
+
language: "unknown",
|
|
255
|
+
packageManager: "yarn",
|
|
256
|
+
framework: "unknown",
|
|
257
|
+
testFramework: "unknown",
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const originalWhich = Bun.which;
|
|
261
|
+
Bun.which = (cmd: string) => {
|
|
262
|
+
if (cmd === "yarn") return null;
|
|
263
|
+
return originalWhich(cmd);
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
const result = await buildTestCommandWithFallback(workspace);
|
|
268
|
+
expect(result).toEqual(["npx", "yarn", "test"]);
|
|
269
|
+
} finally {
|
|
270
|
+
Bun.which = originalWhich;
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("builds unknown test with pnpm fallback when pnpm is unavailable", async () => {
|
|
275
|
+
const workspace: WorkspaceInfo = {
|
|
276
|
+
path: "/path",
|
|
277
|
+
language: "unknown",
|
|
278
|
+
packageManager: "pnpm",
|
|
279
|
+
framework: "unknown",
|
|
280
|
+
testFramework: "unknown",
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const originalWhich = Bun.which;
|
|
284
|
+
Bun.which = (cmd: string) => {
|
|
285
|
+
if (cmd === "pnpm") return null;
|
|
286
|
+
return originalWhich(cmd);
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
const result = await buildTestCommandWithFallback(workspace);
|
|
291
|
+
expect(result).toEqual(["npx", "pnpm", "test"]);
|
|
292
|
+
} finally {
|
|
293
|
+
Bun.which = originalWhich;
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
216
297
|
test("builds vitest with fallback when yarn is unavailable", async () => {
|
|
217
298
|
const workspace: WorkspaceInfo = {
|
|
218
299
|
path: "/path",
|
|
@@ -94,8 +94,14 @@ export async function buildTestCommandWithFallback(workspace: WorkspaceInfo): Pr
|
|
|
94
94
|
case "unknown": {
|
|
95
95
|
if (packageManager === "bun") return ["bun", "test"];
|
|
96
96
|
if (packageManager === "npm") return ["npm", "test"];
|
|
97
|
-
if (packageManager === "yarn")
|
|
98
|
-
|
|
97
|
+
if (packageManager === "yarn") {
|
|
98
|
+
const cmd = await resolveAvailableCommand("yarn");
|
|
99
|
+
return cmd === "npx" ? ["npx", "yarn", "test"] : ["yarn", "test"];
|
|
100
|
+
}
|
|
101
|
+
if (packageManager === "pnpm") {
|
|
102
|
+
const cmd = await resolveAvailableCommand("pnpm");
|
|
103
|
+
return cmd === "npx" ? ["npx", "pnpm", "test"] : ["pnpm", "test"];
|
|
104
|
+
}
|
|
99
105
|
if (packageManager === "uv") return ["uv", "run", "pytest", "-v"];
|
|
100
106
|
if (packageManager === "poetry") return ["poetry", "run", "pytest", "-v"];
|
|
101
107
|
return ["npm", "test"];
|