@simonfestl/husky-cli 0.9.5 → 0.9.6
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/dist/commands/llm-context.js +7 -3
- package/dist/commands/service-account.d.ts +2 -0
- package/dist/commands/service-account.js +180 -0
- package/dist/commands/services.d.ts +2 -0
- package/dist/commands/services.js +381 -0
- package/dist/commands/vm.js +230 -6
- package/dist/commands/worktree.js +481 -1
- package/dist/index.js +4 -0
- package/dist/lib/agent-templates.d.ts +20 -0
- package/dist/lib/agent-templates.js +142 -0
- package/dist/lib/worktree.d.ts +26 -0
- package/dist/lib/worktree.js +127 -0
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@ import { Command } from "commander";
|
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import { WorktreeManager } from "../lib/worktree.js";
|
|
4
4
|
import { MergeLock, withMergeLock } from "../lib/merge-lock.js";
|
|
5
|
+
import { getConfig } from "./config.js";
|
|
5
6
|
export const worktreeCommand = new Command("worktree")
|
|
6
7
|
.description("Manage Git worktrees for isolated agent workspaces");
|
|
7
8
|
/**
|
|
@@ -23,13 +24,46 @@ worktreeCommand
|
|
|
23
24
|
.description("Create a new worktree for a session")
|
|
24
25
|
.option("-b, --base-branch <branch>", "Base branch to create from (default: main/master)")
|
|
25
26
|
.option("-p, --project <path>", "Project directory (default: current directory)")
|
|
27
|
+
.option("--task-id <id>", "Link worktree to task and register with dashboard API")
|
|
26
28
|
.option("--json", "Output as JSON")
|
|
27
29
|
.action(async (sessionName, options) => {
|
|
28
30
|
try {
|
|
29
31
|
const manager = getManager(options);
|
|
30
32
|
const info = manager.createWorktree(sessionName);
|
|
33
|
+
// Register with dashboard API if task-id provided
|
|
34
|
+
if (options.taskId) {
|
|
35
|
+
const config = getConfig();
|
|
36
|
+
if (!config.apiUrl) {
|
|
37
|
+
console.warn("Warning: API URL not configured. Worktree created but not registered.");
|
|
38
|
+
console.warn("Run: husky config set api-url <url>");
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
try {
|
|
42
|
+
const res = await fetch(`${config.apiUrl}/api/tasks/${options.taskId}`, {
|
|
43
|
+
method: "PATCH",
|
|
44
|
+
headers: {
|
|
45
|
+
"Content-Type": "application/json",
|
|
46
|
+
...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
|
|
47
|
+
},
|
|
48
|
+
body: JSON.stringify({
|
|
49
|
+
worktreePath: info.path,
|
|
50
|
+
worktreeBranch: info.branch,
|
|
51
|
+
}),
|
|
52
|
+
});
|
|
53
|
+
if (!res.ok) {
|
|
54
|
+
console.warn(`Warning: Failed to register worktree with task (API ${res.status})`);
|
|
55
|
+
}
|
|
56
|
+
else if (!options.json) {
|
|
57
|
+
console.log(`✓ Registered worktree with task ${options.taskId}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
console.warn(`Warning: Failed to register worktree: ${err instanceof Error ? err.message : err}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
31
65
|
if (options.json) {
|
|
32
|
-
console.log(JSON.stringify(info, null, 2));
|
|
66
|
+
console.log(JSON.stringify({ ...info, taskId: options.taskId }, null, 2));
|
|
33
67
|
}
|
|
34
68
|
else {
|
|
35
69
|
console.log(`\nWorktree created successfully!`);
|
|
@@ -403,3 +437,449 @@ function printWorktreeStatus(info, changedFiles, hasUncommitted) {
|
|
|
403
437
|
}
|
|
404
438
|
}
|
|
405
439
|
}
|
|
440
|
+
// ============================================
|
|
441
|
+
// DASHBOARD SYNC COMMANDS
|
|
442
|
+
// ============================================
|
|
443
|
+
// husky worktree sync-stats <session-name> --task-id <id>
|
|
444
|
+
worktreeCommand
|
|
445
|
+
.command("sync-stats <session-name>")
|
|
446
|
+
.description("Sync worktree stats to dashboard for a task")
|
|
447
|
+
.requiredOption("--task-id <id>", "Task ID to update")
|
|
448
|
+
.option("-p, --project <path>", "Project directory (default: current directory)")
|
|
449
|
+
.option("--json", "Output as JSON")
|
|
450
|
+
.action(async (sessionName, options) => {
|
|
451
|
+
const config = getConfig();
|
|
452
|
+
if (!config.apiUrl) {
|
|
453
|
+
console.error("Error: API URL not configured. Run: husky config set api-url <url>");
|
|
454
|
+
process.exit(1);
|
|
455
|
+
}
|
|
456
|
+
try {
|
|
457
|
+
const manager = getManager(options);
|
|
458
|
+
const info = manager.getWorktree(sessionName);
|
|
459
|
+
if (!info) {
|
|
460
|
+
console.error(`Error: No worktree found for session: ${sessionName}`);
|
|
461
|
+
process.exit(1);
|
|
462
|
+
}
|
|
463
|
+
// Prepare stats payload
|
|
464
|
+
const worktreeStats = {
|
|
465
|
+
commitCount: info.stats.commitCount,
|
|
466
|
+
filesChanged: info.stats.filesChanged,
|
|
467
|
+
additions: info.stats.additions,
|
|
468
|
+
deletions: info.stats.deletions,
|
|
469
|
+
lastUpdated: new Date().toISOString(),
|
|
470
|
+
};
|
|
471
|
+
// Update task via API
|
|
472
|
+
const res = await fetch(`${config.apiUrl}/api/tasks/${options.taskId}`, {
|
|
473
|
+
method: "PATCH",
|
|
474
|
+
headers: {
|
|
475
|
+
"Content-Type": "application/json",
|
|
476
|
+
...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
|
|
477
|
+
},
|
|
478
|
+
body: JSON.stringify({
|
|
479
|
+
worktreePath: info.path,
|
|
480
|
+
worktreeBranch: info.branch,
|
|
481
|
+
worktreeStats,
|
|
482
|
+
}),
|
|
483
|
+
});
|
|
484
|
+
if (!res.ok) {
|
|
485
|
+
throw new Error(`API error: ${res.status}`);
|
|
486
|
+
}
|
|
487
|
+
if (options.json) {
|
|
488
|
+
console.log(JSON.stringify({ success: true, taskId: options.taskId, worktreeStats }, null, 2));
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
console.log(`✓ Synced stats for ${sessionName} to task ${options.taskId}`);
|
|
492
|
+
console.log(` Commits: ${worktreeStats.commitCount}`);
|
|
493
|
+
console.log(` Files: ${worktreeStats.filesChanged}`);
|
|
494
|
+
console.log(` Changes: +${worktreeStats.additions}/-${worktreeStats.deletions}`);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
catch (error) {
|
|
498
|
+
console.error("Error syncing stats:", error instanceof Error ? error.message : error);
|
|
499
|
+
process.exit(1);
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
// husky worktree check-conflicts <session-name> --task-id <id>
|
|
503
|
+
worktreeCommand
|
|
504
|
+
.command("check-conflicts <session-name>")
|
|
505
|
+
.description("Check for merge conflicts and sync to dashboard")
|
|
506
|
+
.requiredOption("--task-id <id>", "Task ID to update")
|
|
507
|
+
.option("-p, --project <path>", "Project directory (default: current directory)")
|
|
508
|
+
.option("--json", "Output as JSON")
|
|
509
|
+
.action(async (sessionName, options) => {
|
|
510
|
+
const config = getConfig();
|
|
511
|
+
if (!config.apiUrl) {
|
|
512
|
+
console.error("Error: API URL not configured. Run: husky config set api-url <url>");
|
|
513
|
+
process.exit(1);
|
|
514
|
+
}
|
|
515
|
+
try {
|
|
516
|
+
const manager = getManager(options);
|
|
517
|
+
const info = manager.getWorktree(sessionName);
|
|
518
|
+
if (!info) {
|
|
519
|
+
console.error(`Error: No worktree found for session: ${sessionName}`);
|
|
520
|
+
process.exit(1);
|
|
521
|
+
}
|
|
522
|
+
// Check for conflicts
|
|
523
|
+
const conflictResult = manager.checkMergeConflicts(sessionName);
|
|
524
|
+
// Update task via API
|
|
525
|
+
const res = await fetch(`${config.apiUrl}/api/tasks/${options.taskId}`, {
|
|
526
|
+
method: "PATCH",
|
|
527
|
+
headers: {
|
|
528
|
+
"Content-Type": "application/json",
|
|
529
|
+
...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
|
|
530
|
+
},
|
|
531
|
+
body: JSON.stringify({
|
|
532
|
+
worktreeHasConflicts: conflictResult.hasConflicts,
|
|
533
|
+
worktreeConflictFiles: conflictResult.conflictFiles,
|
|
534
|
+
worktreeConflictCheckedAt: conflictResult.checkedAt.toISOString(),
|
|
535
|
+
}),
|
|
536
|
+
});
|
|
537
|
+
if (!res.ok) {
|
|
538
|
+
throw new Error(`API error: ${res.status}`);
|
|
539
|
+
}
|
|
540
|
+
if (options.json) {
|
|
541
|
+
console.log(JSON.stringify({
|
|
542
|
+
success: true,
|
|
543
|
+
taskId: options.taskId,
|
|
544
|
+
hasConflicts: conflictResult.hasConflicts,
|
|
545
|
+
conflictFiles: conflictResult.conflictFiles,
|
|
546
|
+
checkedAt: conflictResult.checkedAt.toISOString(),
|
|
547
|
+
}, null, 2));
|
|
548
|
+
}
|
|
549
|
+
else {
|
|
550
|
+
if (conflictResult.hasConflicts) {
|
|
551
|
+
console.log(`⚠ Conflicts detected in ${sessionName}`);
|
|
552
|
+
console.log(` Files with conflicts:`);
|
|
553
|
+
for (const file of conflictResult.conflictFiles.slice(0, 5)) {
|
|
554
|
+
console.log(` - ${file}`);
|
|
555
|
+
}
|
|
556
|
+
if (conflictResult.conflictFiles.length > 5) {
|
|
557
|
+
console.log(` ... and ${conflictResult.conflictFiles.length - 5} more`);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
else {
|
|
561
|
+
console.log(`✓ No conflicts in ${sessionName}`);
|
|
562
|
+
}
|
|
563
|
+
console.log(` Updated task ${options.taskId}`);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
catch (error) {
|
|
567
|
+
console.error("Error checking conflicts:", error instanceof Error ? error.message : error);
|
|
568
|
+
process.exit(1);
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
// husky worktree poll-actions
|
|
572
|
+
worktreeCommand
|
|
573
|
+
.command("poll-actions")
|
|
574
|
+
.description("Poll for pending worktree actions from dashboard")
|
|
575
|
+
.option("-p, --project <path>", "Project directory (default: current directory)")
|
|
576
|
+
.option("--execute", "Execute pending actions immediately")
|
|
577
|
+
.option("--json", "Output as JSON")
|
|
578
|
+
.action(async (options) => {
|
|
579
|
+
const config = getConfig();
|
|
580
|
+
if (!config.apiUrl) {
|
|
581
|
+
console.error("Error: API URL not configured. Run: husky config set api-url <url>");
|
|
582
|
+
process.exit(1);
|
|
583
|
+
}
|
|
584
|
+
try {
|
|
585
|
+
// Fetch pending actions
|
|
586
|
+
const res = await fetch(`${config.apiUrl}/api/worktrees/pending`, {
|
|
587
|
+
headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
|
|
588
|
+
});
|
|
589
|
+
if (!res.ok) {
|
|
590
|
+
throw new Error(`API error: ${res.status}`);
|
|
591
|
+
}
|
|
592
|
+
const data = await res.json();
|
|
593
|
+
const pendingActions = data.pendingActions || [];
|
|
594
|
+
if (options.json && !options.execute) {
|
|
595
|
+
console.log(JSON.stringify({ pendingActions }, null, 2));
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
if (pendingActions.length === 0) {
|
|
599
|
+
if (!options.json) {
|
|
600
|
+
console.log("No pending actions.");
|
|
601
|
+
}
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
if (!options.execute) {
|
|
605
|
+
console.log(`\n PENDING WORKTREE ACTIONS`);
|
|
606
|
+
console.log(" " + "-".repeat(60));
|
|
607
|
+
for (const action of pendingActions) {
|
|
608
|
+
const icon = action.action === "merge" ? "↑" : "✕";
|
|
609
|
+
console.log(` ${icon} ${action.action.toUpperCase().padEnd(8)} ${action.taskId}`);
|
|
610
|
+
console.log(` Branch: ${action.worktreeBranch}`);
|
|
611
|
+
console.log(` Path: ${action.worktreePath}`);
|
|
612
|
+
}
|
|
613
|
+
console.log("");
|
|
614
|
+
console.log(" Use --execute to process these actions");
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
// Execute pending actions
|
|
618
|
+
const projectDir = getProjectDir(options);
|
|
619
|
+
const results = [];
|
|
620
|
+
for (const action of pendingActions) {
|
|
621
|
+
console.log(`\nExecuting ${action.action} for task ${action.taskId}...`);
|
|
622
|
+
// Extract session name from branch (e.g., "husky/task-abc123" -> "task-abc123")
|
|
623
|
+
const sessionName = action.worktreeBranch.replace("husky/", "");
|
|
624
|
+
const manager = new WorktreeManager(projectDir);
|
|
625
|
+
let success = false;
|
|
626
|
+
let error;
|
|
627
|
+
try {
|
|
628
|
+
if (action.action === "merge") {
|
|
629
|
+
// Execute merge with lock
|
|
630
|
+
success = await withMergeLock(projectDir, sessionName, async () => {
|
|
631
|
+
return manager.mergeWorktree(sessionName, {
|
|
632
|
+
deleteAfter: false,
|
|
633
|
+
message: `husky: Merge ${action.worktreeBranch} (via dashboard)`,
|
|
634
|
+
});
|
|
635
|
+
});
|
|
636
|
+
if (!success) {
|
|
637
|
+
error = "Merge failed - check for conflicts";
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
else if (action.action === "cleanup") {
|
|
641
|
+
// Execute cleanup
|
|
642
|
+
manager.removeWorktree(sessionName, true);
|
|
643
|
+
success = true;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
catch (err) {
|
|
647
|
+
success = false;
|
|
648
|
+
error = err instanceof Error ? err.message : "Unknown error";
|
|
649
|
+
}
|
|
650
|
+
// Report result to dashboard
|
|
651
|
+
const completeRes = await fetch(`${config.apiUrl}/api/worktrees/${action.taskId}/action`, {
|
|
652
|
+
method: "PATCH",
|
|
653
|
+
headers: {
|
|
654
|
+
"Content-Type": "application/json",
|
|
655
|
+
...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
|
|
656
|
+
},
|
|
657
|
+
body: JSON.stringify({
|
|
658
|
+
status: success ? "completed" : "failed",
|
|
659
|
+
error,
|
|
660
|
+
}),
|
|
661
|
+
});
|
|
662
|
+
if (!completeRes.ok) {
|
|
663
|
+
console.error(` Warning: Failed to report status to dashboard`);
|
|
664
|
+
}
|
|
665
|
+
results.push({ taskId: action.taskId, action: action.action, success, error });
|
|
666
|
+
if (success) {
|
|
667
|
+
console.log(` ✓ ${action.action} completed`);
|
|
668
|
+
}
|
|
669
|
+
else {
|
|
670
|
+
console.log(` ✗ ${action.action} failed: ${error}`);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
if (options.json) {
|
|
674
|
+
console.log(JSON.stringify({ results }, null, 2));
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
catch (error) {
|
|
678
|
+
console.error("Error polling actions:", error instanceof Error ? error.message : error);
|
|
679
|
+
process.exit(1);
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
// husky worktree push <session-name>
|
|
683
|
+
worktreeCommand
|
|
684
|
+
.command("push <session-name>")
|
|
685
|
+
.description("Push worktree branch to remote")
|
|
686
|
+
.option("-p, --project <path>", "Project directory (default: current directory)")
|
|
687
|
+
.option("-f, --force", "Force push")
|
|
688
|
+
.option("--json", "Output as JSON")
|
|
689
|
+
.action(async (sessionName, options) => {
|
|
690
|
+
try {
|
|
691
|
+
const manager = getManager(options);
|
|
692
|
+
const info = manager.getWorktree(sessionName);
|
|
693
|
+
if (!info) {
|
|
694
|
+
console.error(`Error: No worktree found for session: ${sessionName}`);
|
|
695
|
+
process.exit(1);
|
|
696
|
+
}
|
|
697
|
+
const success = manager.pushWorktreeBranch(sessionName, options.force);
|
|
698
|
+
if (options.json) {
|
|
699
|
+
console.log(JSON.stringify({ success, sessionName, branch: info.branch }, null, 2));
|
|
700
|
+
}
|
|
701
|
+
if (!success) {
|
|
702
|
+
process.exit(1);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
catch (error) {
|
|
706
|
+
console.error("Error pushing branch:", error instanceof Error ? error.message : error);
|
|
707
|
+
process.exit(1);
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
// husky worktree pr <session-name>
|
|
711
|
+
worktreeCommand
|
|
712
|
+
.command("pr <session-name>")
|
|
713
|
+
.description("Create a pull request for worktree branch")
|
|
714
|
+
.option("-p, --project <path>", "Project directory (default: current directory)")
|
|
715
|
+
.requiredOption("-t, --title <title>", "PR title")
|
|
716
|
+
.option("-b, --body <body>", "PR body/description")
|
|
717
|
+
.option("--draft", "Create as draft PR")
|
|
718
|
+
.option("--push", "Push branch before creating PR")
|
|
719
|
+
.option("--task-id <id>", "Task ID to update with PR URL")
|
|
720
|
+
.option("--json", "Output as JSON")
|
|
721
|
+
.action(async (sessionName, options) => {
|
|
722
|
+
const config = getConfig();
|
|
723
|
+
try {
|
|
724
|
+
const manager = getManager(options);
|
|
725
|
+
const info = manager.getWorktree(sessionName);
|
|
726
|
+
if (!info) {
|
|
727
|
+
console.error(`Error: No worktree found for session: ${sessionName}`);
|
|
728
|
+
process.exit(1);
|
|
729
|
+
}
|
|
730
|
+
// Push first if requested
|
|
731
|
+
if (options.push) {
|
|
732
|
+
const pushSuccess = manager.pushWorktreeBranch(sessionName);
|
|
733
|
+
if (!pushSuccess) {
|
|
734
|
+
console.error("Failed to push branch");
|
|
735
|
+
process.exit(1);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
// Create PR
|
|
739
|
+
const result = manager.createPullRequest(sessionName, {
|
|
740
|
+
title: options.title,
|
|
741
|
+
body: options.body,
|
|
742
|
+
draft: options.draft,
|
|
743
|
+
});
|
|
744
|
+
if (!result.success) {
|
|
745
|
+
console.error(`Error creating PR: ${result.error}`);
|
|
746
|
+
process.exit(1);
|
|
747
|
+
}
|
|
748
|
+
// Update task with PR URL if specified
|
|
749
|
+
if (options.taskId && config.apiUrl && result.prUrl) {
|
|
750
|
+
try {
|
|
751
|
+
await fetch(`${config.apiUrl}/api/tasks/${options.taskId}`, {
|
|
752
|
+
method: "PATCH",
|
|
753
|
+
headers: {
|
|
754
|
+
"Content-Type": "application/json",
|
|
755
|
+
...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
|
|
756
|
+
},
|
|
757
|
+
body: JSON.stringify({
|
|
758
|
+
result: {
|
|
759
|
+
prUrl: result.prUrl,
|
|
760
|
+
completedAt: new Date().toISOString(),
|
|
761
|
+
},
|
|
762
|
+
}),
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
catch {
|
|
766
|
+
console.warn("Warning: Could not update task with PR URL");
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
if (options.json) {
|
|
770
|
+
console.log(JSON.stringify({
|
|
771
|
+
success: true,
|
|
772
|
+
sessionName,
|
|
773
|
+
branch: info.branch,
|
|
774
|
+
prUrl: result.prUrl,
|
|
775
|
+
}, null, 2));
|
|
776
|
+
}
|
|
777
|
+
else {
|
|
778
|
+
console.log(`✓ Pull request created: ${result.prUrl}`);
|
|
779
|
+
if (options.taskId) {
|
|
780
|
+
console.log(` Updated task ${options.taskId}`);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
catch (error) {
|
|
785
|
+
console.error("Error creating PR:", error instanceof Error ? error.message : error);
|
|
786
|
+
process.exit(1);
|
|
787
|
+
}
|
|
788
|
+
});
|
|
789
|
+
// husky worktree sync-all
|
|
790
|
+
worktreeCommand
|
|
791
|
+
.command("sync-all")
|
|
792
|
+
.description("Sync stats and check conflicts for all worktrees with linked tasks")
|
|
793
|
+
.option("-p, --project <path>", "Project directory (default: current directory)")
|
|
794
|
+
.option("--json", "Output as JSON")
|
|
795
|
+
.action(async (options) => {
|
|
796
|
+
const config = getConfig();
|
|
797
|
+
if (!config.apiUrl) {
|
|
798
|
+
console.error("Error: API URL not configured. Run: husky config set api-url <url>");
|
|
799
|
+
process.exit(1);
|
|
800
|
+
}
|
|
801
|
+
try {
|
|
802
|
+
const manager = getManager(options);
|
|
803
|
+
const worktrees = manager.listWorktrees();
|
|
804
|
+
if (worktrees.length === 0) {
|
|
805
|
+
console.log("No worktrees found.");
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
// Fetch tasks with worktrees to find linked tasks
|
|
809
|
+
const tasksRes = await fetch(`${config.apiUrl}/api/worktrees`, {
|
|
810
|
+
headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
|
|
811
|
+
});
|
|
812
|
+
if (!tasksRes.ok) {
|
|
813
|
+
throw new Error(`API error: ${tasksRes.status}`);
|
|
814
|
+
}
|
|
815
|
+
const tasksData = await tasksRes.json();
|
|
816
|
+
const tasksByBranch = new Map();
|
|
817
|
+
for (const wt of tasksData.worktrees || []) {
|
|
818
|
+
tasksByBranch.set(wt.worktreeBranch, wt.taskId);
|
|
819
|
+
}
|
|
820
|
+
const results = [];
|
|
821
|
+
for (const wt of worktrees) {
|
|
822
|
+
const taskId = tasksByBranch.get(wt.branch);
|
|
823
|
+
if (!taskId) {
|
|
824
|
+
results.push({ sessionName: wt.sessionName, synced: false });
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
// Check conflicts
|
|
828
|
+
const conflictResult = manager.checkMergeConflicts(wt.sessionName);
|
|
829
|
+
// Prepare update
|
|
830
|
+
const updatePayload = {
|
|
831
|
+
worktreePath: wt.path,
|
|
832
|
+
worktreeBranch: wt.branch,
|
|
833
|
+
worktreeStats: {
|
|
834
|
+
commitCount: wt.stats.commitCount,
|
|
835
|
+
filesChanged: wt.stats.filesChanged,
|
|
836
|
+
additions: wt.stats.additions,
|
|
837
|
+
deletions: wt.stats.deletions,
|
|
838
|
+
lastUpdated: new Date().toISOString(),
|
|
839
|
+
},
|
|
840
|
+
worktreeHasConflicts: conflictResult.hasConflicts,
|
|
841
|
+
worktreeConflictFiles: conflictResult.conflictFiles,
|
|
842
|
+
worktreeConflictCheckedAt: conflictResult.checkedAt.toISOString(),
|
|
843
|
+
};
|
|
844
|
+
// Update task
|
|
845
|
+
const updateRes = await fetch(`${config.apiUrl}/api/tasks/${taskId}`, {
|
|
846
|
+
method: "PATCH",
|
|
847
|
+
headers: {
|
|
848
|
+
"Content-Type": "application/json",
|
|
849
|
+
...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
|
|
850
|
+
},
|
|
851
|
+
body: JSON.stringify(updatePayload),
|
|
852
|
+
});
|
|
853
|
+
results.push({
|
|
854
|
+
sessionName: wt.sessionName,
|
|
855
|
+
taskId,
|
|
856
|
+
synced: updateRes.ok,
|
|
857
|
+
hasConflicts: conflictResult.hasConflicts,
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
if (options.json) {
|
|
861
|
+
console.log(JSON.stringify({ results }, null, 2));
|
|
862
|
+
}
|
|
863
|
+
else {
|
|
864
|
+
console.log(`\n WORKTREE SYNC RESULTS`);
|
|
865
|
+
console.log(" " + "-".repeat(60));
|
|
866
|
+
for (const r of results) {
|
|
867
|
+
const icon = r.synced ? "✓" : r.taskId ? "✗" : "○";
|
|
868
|
+
const conflictIcon = r.hasConflicts ? " ⚠" : "";
|
|
869
|
+
const taskInfo = r.taskId ? ` → ${r.taskId}` : " (no linked task)";
|
|
870
|
+
console.log(` ${icon} ${r.sessionName}${taskInfo}${conflictIcon}`);
|
|
871
|
+
}
|
|
872
|
+
const synced = results.filter(r => r.synced).length;
|
|
873
|
+
const conflicts = results.filter(r => r.hasConflicts).length;
|
|
874
|
+
console.log("");
|
|
875
|
+
console.log(` Synced: ${synced}/${results.length}`);
|
|
876
|
+
if (conflicts > 0) {
|
|
877
|
+
console.log(` With conflicts: ${conflicts}`);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
catch (error) {
|
|
882
|
+
console.error("Error syncing worktrees:", error instanceof Error ? error.message : error);
|
|
883
|
+
process.exit(1);
|
|
884
|
+
}
|
|
885
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -20,8 +20,10 @@ import { completionCommand } from "./commands/completion.js";
|
|
|
20
20
|
import { worktreeCommand } from "./commands/worktree.js";
|
|
21
21
|
import { workerCommand } from "./commands/worker.js";
|
|
22
22
|
import { bizCommand } from "./commands/biz.js";
|
|
23
|
+
import { servicesCommand } from "./commands/services.js";
|
|
23
24
|
import { printLLMContext, llmCommand } from "./commands/llm-context.js";
|
|
24
25
|
import { runInteractiveMode } from "./commands/interactive.js";
|
|
26
|
+
import { serviceAccountCommand } from "./commands/service-account.js";
|
|
25
27
|
const program = new Command();
|
|
26
28
|
program
|
|
27
29
|
.name("husky")
|
|
@@ -48,6 +50,8 @@ program.addCommand(completionCommand);
|
|
|
48
50
|
program.addCommand(worktreeCommand);
|
|
49
51
|
program.addCommand(workerCommand);
|
|
50
52
|
program.addCommand(bizCommand);
|
|
53
|
+
program.addCommand(servicesCommand);
|
|
54
|
+
program.addCommand(serviceAccountCommand);
|
|
51
55
|
program.addCommand(llmCommand);
|
|
52
56
|
// Handle --llm flag specially
|
|
53
57
|
if (process.argv.includes("--llm")) {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface AgentTypeConfig {
|
|
2
|
+
directories: string[];
|
|
3
|
+
dependencies: string[];
|
|
4
|
+
defaultPrompt: string;
|
|
5
|
+
geminiMdTemplate?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface AgentType {
|
|
8
|
+
id: string;
|
|
9
|
+
departmentId: string;
|
|
10
|
+
name: string;
|
|
11
|
+
slug: string;
|
|
12
|
+
description: string;
|
|
13
|
+
agentConfig: AgentTypeConfig;
|
|
14
|
+
createdAt: string;
|
|
15
|
+
updatedAt: string;
|
|
16
|
+
}
|
|
17
|
+
export declare const DEFAULT_AGENT_CONFIGS: Record<string, AgentTypeConfig>;
|
|
18
|
+
export declare function generateStartupScript(agentType: AgentType | string, huskyApiUrl?: string, huskyApiKey?: string, gcpProject?: string): string;
|
|
19
|
+
export declare function getDefaultAgentConfig(slug: string): AgentTypeConfig | undefined;
|
|
20
|
+
export declare function listDefaultAgentTypes(): string[];
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
export const DEFAULT_AGENT_CONFIGS = {
|
|
2
|
+
support: {
|
|
3
|
+
directories: [
|
|
4
|
+
"sops",
|
|
5
|
+
"tickets",
|
|
6
|
+
"customers",
|
|
7
|
+
"templates",
|
|
8
|
+
"knowledge/auto",
|
|
9
|
+
"learning",
|
|
10
|
+
"logs",
|
|
11
|
+
],
|
|
12
|
+
dependencies: ["google-cloud-firestore"],
|
|
13
|
+
defaultPrompt: "Du bist ein Support Agent. Starte mit /shift-start um die Schicht zu beginnen.",
|
|
14
|
+
},
|
|
15
|
+
accounting: {
|
|
16
|
+
directories: [
|
|
17
|
+
"inbox/unprocessed",
|
|
18
|
+
"inbox/processed",
|
|
19
|
+
"inbox/failed",
|
|
20
|
+
"documents/invoices",
|
|
21
|
+
"documents/receipts",
|
|
22
|
+
"exports",
|
|
23
|
+
"logs",
|
|
24
|
+
],
|
|
25
|
+
dependencies: ["google-cloud-firestore", "google-cloud-storage"],
|
|
26
|
+
defaultPrompt: "Du bist ein Accounting Agent. Pruefe /inbox fuer neue Belege.",
|
|
27
|
+
},
|
|
28
|
+
marketing: {
|
|
29
|
+
directories: [
|
|
30
|
+
"campaigns/active",
|
|
31
|
+
"campaigns/drafts",
|
|
32
|
+
"newsletters/templates",
|
|
33
|
+
"newsletters/sent",
|
|
34
|
+
"assets/images",
|
|
35
|
+
"analytics",
|
|
36
|
+
"logs",
|
|
37
|
+
],
|
|
38
|
+
dependencies: ["google-cloud-firestore"],
|
|
39
|
+
defaultPrompt: "Du bist ein Marketing Agent. Pruefe /campaigns fuer aktuelle Kampagnen.",
|
|
40
|
+
},
|
|
41
|
+
research: {
|
|
42
|
+
directories: [
|
|
43
|
+
"sources/youtube",
|
|
44
|
+
"sources/competitors",
|
|
45
|
+
"products/images/raw",
|
|
46
|
+
"products/images/processed",
|
|
47
|
+
"products/data",
|
|
48
|
+
"reports",
|
|
49
|
+
"logs",
|
|
50
|
+
],
|
|
51
|
+
dependencies: ["google-cloud-firestore", "yt-dlp"],
|
|
52
|
+
defaultPrompt: "Du bist ein Research Agent. Pruefe /youtube fuer neue Videos zu analysieren.",
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
export function generateStartupScript(agentType, huskyApiUrl, huskyApiKey, gcpProject) {
|
|
56
|
+
let config;
|
|
57
|
+
let typeName;
|
|
58
|
+
let typeSlug;
|
|
59
|
+
if (typeof agentType === "string") {
|
|
60
|
+
config = DEFAULT_AGENT_CONFIGS[agentType] || DEFAULT_AGENT_CONFIGS.support;
|
|
61
|
+
typeName =
|
|
62
|
+
agentType.charAt(0).toUpperCase() + agentType.slice(1) + " Agent";
|
|
63
|
+
typeSlug = agentType;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
config = agentType.agentConfig;
|
|
67
|
+
typeName = agentType.name;
|
|
68
|
+
typeSlug = agentType.slug;
|
|
69
|
+
}
|
|
70
|
+
const dirsToCreate = config.directories
|
|
71
|
+
.map((d) => `"$WORKSPACE/${d}"`)
|
|
72
|
+
.join(" ");
|
|
73
|
+
const pipDeps = config.dependencies.join(" ");
|
|
74
|
+
return `#!/bin/bash
|
|
75
|
+
exec > /var/log/agent-init.log 2>&1
|
|
76
|
+
set -e
|
|
77
|
+
|
|
78
|
+
echo "=== AGENT VM STARTUP ==="
|
|
79
|
+
echo "Type: ${typeSlug}"
|
|
80
|
+
echo "Name: ${typeName}"
|
|
81
|
+
echo "Time: $(date)"
|
|
82
|
+
|
|
83
|
+
AGENT_TYPE="${typeSlug}"
|
|
84
|
+
AGENT_NAME="${typeName}"
|
|
85
|
+
HUSKY_API_URL="${huskyApiUrl || ""}"
|
|
86
|
+
HUSKY_API_KEY="${huskyApiKey || ""}"
|
|
87
|
+
GCP_PROJECT="${gcpProject || "$(curl -s http://metadata.google.internal/computeMetadata/v1/project/project-id -H Metadata-Flavor:Google)"}"
|
|
88
|
+
|
|
89
|
+
AGENT_USER="agent"
|
|
90
|
+
WORKSPACE="/home/$AGENT_USER/workspace"
|
|
91
|
+
|
|
92
|
+
if ! id "$AGENT_USER" &>/dev/null; then
|
|
93
|
+
useradd -m -s /bin/bash "$AGENT_USER"
|
|
94
|
+
usermod -aG sudo "$AGENT_USER"
|
|
95
|
+
echo "$AGENT_USER ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
echo "[1/5] Installing system packages..."
|
|
99
|
+
apt-get update -qq
|
|
100
|
+
apt-get install -y -qq nodejs npm jq python3-pip imagemagick curl
|
|
101
|
+
|
|
102
|
+
echo "[2/5] Installing CLI tools..."
|
|
103
|
+
npm install -g @google/gemini-cli @huskyv0/cli 2>/dev/null || true
|
|
104
|
+
|
|
105
|
+
echo "[3/5] Installing Python dependencies..."
|
|
106
|
+
pip3 install ${pipDeps} 2>/dev/null || true
|
|
107
|
+
|
|
108
|
+
echo "[4/5] Creating workspace..."
|
|
109
|
+
sudo -u "$AGENT_USER" mkdir -p ${dirsToCreate}
|
|
110
|
+
sudo -u "$AGENT_USER" mkdir -p "$WORKSPACE/.gemini/commands" "$WORKSPACE/scripts"
|
|
111
|
+
|
|
112
|
+
echo "[5/5] Configuring environment..."
|
|
113
|
+
cat >> "/home/$AGENT_USER/.bashrc" << 'BASHRC'
|
|
114
|
+
export WORKSPACE="$HOME/workspace"
|
|
115
|
+
export AGENT_TYPE="${typeSlug}"
|
|
116
|
+
export AGENT_NAME="${typeName}"
|
|
117
|
+
export GOOGLE_CLOUD_PROJECT="$GCP_PROJECT"
|
|
118
|
+
export PATH="$WORKSPACE/scripts:$PATH"
|
|
119
|
+
alias ws="cd $WORKSPACE"
|
|
120
|
+
alias logs="tail -f $WORKSPACE/logs/$(date +%Y-%m-%d).log"
|
|
121
|
+
BASHRC
|
|
122
|
+
|
|
123
|
+
if [ -n "$HUSKY_API_URL" ] && [ -n "$HUSKY_API_KEY" ]; then
|
|
124
|
+
sudo -u "$AGENT_USER" bash -c "husky config set api-url '$HUSKY_API_URL'"
|
|
125
|
+
sudo -u "$AGENT_USER" bash -c "husky config set api-key '$HUSKY_API_KEY'"
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
chown -R "$AGENT_USER:$AGENT_USER" "/home/$AGENT_USER"
|
|
129
|
+
|
|
130
|
+
echo "=== AGENT READY ==="
|
|
131
|
+
echo "Type: $AGENT_TYPE"
|
|
132
|
+
echo "Name: $AGENT_NAME"
|
|
133
|
+
echo "User: $AGENT_USER"
|
|
134
|
+
echo "Workspace: $WORKSPACE"
|
|
135
|
+
`;
|
|
136
|
+
}
|
|
137
|
+
export function getDefaultAgentConfig(slug) {
|
|
138
|
+
return DEFAULT_AGENT_CONFIGS[slug];
|
|
139
|
+
}
|
|
140
|
+
export function listDefaultAgentTypes() {
|
|
141
|
+
return Object.keys(DEFAULT_AGENT_CONFIGS);
|
|
142
|
+
}
|