@simonfestl/husky-cli 0.9.3 → 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.
@@ -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
+ }
@@ -33,6 +33,11 @@ export interface MergeOptions {
33
33
  deleteAfter?: boolean;
34
34
  message?: string;
35
35
  }
36
+ export interface ConflictCheckResult {
37
+ hasConflicts: boolean;
38
+ conflictFiles: string[];
39
+ checkedAt: Date;
40
+ }
36
41
  export declare class WorktreeManager {
37
42
  private projectDir;
38
43
  private baseBranch;
@@ -130,4 +135,25 @@ export declare class WorktreeManager {
130
135
  * Get the worktrees directory.
131
136
  */
132
137
  getWorktreesDir(): string;
138
+ /**
139
+ * Check if a worktree branch would have merge conflicts with base branch.
140
+ * Uses git merge-tree to simulate the merge without actually doing it.
141
+ */
142
+ checkMergeConflicts(sessionName: string): ConflictCheckResult;
143
+ /**
144
+ * Push the worktree branch to remote.
145
+ */
146
+ pushWorktreeBranch(sessionName: string, force?: boolean): boolean;
147
+ /**
148
+ * Create a PR for a worktree branch using gh CLI.
149
+ */
150
+ createPullRequest(sessionName: string, options: {
151
+ title: string;
152
+ body?: string;
153
+ draft?: boolean;
154
+ }): {
155
+ success: boolean;
156
+ prUrl?: string;
157
+ error?: string;
158
+ };
133
159
  }
@@ -470,4 +470,131 @@ export class WorktreeManager {
470
470
  getWorktreesDir() {
471
471
  return this.worktreesDir;
472
472
  }
473
+ /**
474
+ * Check if a worktree branch would have merge conflicts with base branch.
475
+ * Uses git merge-tree to simulate the merge without actually doing it.
476
+ */
477
+ checkMergeConflicts(sessionName) {
478
+ const worktreePath = this.getWorktreePath(sessionName);
479
+ const branchName = this.getBranchName(sessionName);
480
+ if (!fs.existsSync(worktreePath)) {
481
+ return {
482
+ hasConflicts: false,
483
+ conflictFiles: [],
484
+ checkedAt: new Date(),
485
+ };
486
+ }
487
+ // Get the merge base
488
+ const mergeBaseResult = this.runGit([
489
+ "merge-base",
490
+ this.baseBranch,
491
+ branchName,
492
+ ]);
493
+ if (mergeBaseResult.status !== 0) {
494
+ // No common ancestor, can't check conflicts
495
+ return {
496
+ hasConflicts: false,
497
+ conflictFiles: [],
498
+ checkedAt: new Date(),
499
+ };
500
+ }
501
+ const mergeBase = mergeBaseResult.stdout.trim();
502
+ // Use git merge-tree to simulate the merge
503
+ const mergeTreeResult = this.runGit([
504
+ "merge-tree",
505
+ mergeBase,
506
+ this.baseBranch,
507
+ branchName,
508
+ ]);
509
+ // Parse the output for conflicts
510
+ const output = mergeTreeResult.stdout;
511
+ const conflictFiles = [];
512
+ // Look for conflict markers in merge-tree output
513
+ // Format: "changed in both" or conflict sections
514
+ const lines = output.split("\n");
515
+ let inConflict = false;
516
+ for (const line of lines) {
517
+ if (line.includes("changed in both") || line.includes("CONFLICT")) {
518
+ inConflict = true;
519
+ }
520
+ // Extract file paths from conflict sections
521
+ if (inConflict && line.match(/^\+\+\+|^---|^@@/)) {
522
+ const fileMatch = line.match(/^\+\+\+ b\/(.+)$/) || line.match(/^--- a\/(.+)$/);
523
+ if (fileMatch && fileMatch[1] && !conflictFiles.includes(fileMatch[1])) {
524
+ conflictFiles.push(fileMatch[1]);
525
+ }
526
+ }
527
+ }
528
+ // Alternative: use diff to find files changed in both branches
529
+ if (conflictFiles.length === 0 && output.includes("<<<<<<<")) {
530
+ // Parse conflict markers directly
531
+ const conflictMatches = output.matchAll(/\+\+\+ b\/([^\n]+)/g);
532
+ for (const match of conflictMatches) {
533
+ if (match[1] && !conflictFiles.includes(match[1])) {
534
+ conflictFiles.push(match[1]);
535
+ }
536
+ }
537
+ }
538
+ return {
539
+ hasConflicts: conflictFiles.length > 0 || output.includes("<<<<<<<") || output.includes("CONFLICT"),
540
+ conflictFiles,
541
+ checkedAt: new Date(),
542
+ };
543
+ }
544
+ /**
545
+ * Push the worktree branch to remote.
546
+ */
547
+ pushWorktreeBranch(sessionName, force = false) {
548
+ const branchName = this.getBranchName(sessionName);
549
+ const pushArgs = ["push", "-u", "origin", branchName];
550
+ if (force) {
551
+ pushArgs.splice(1, 0, "--force");
552
+ }
553
+ const result = this.runGit(pushArgs);
554
+ if (result.status !== 0) {
555
+ console.error(`Failed to push branch: ${result.stderr}`);
556
+ return false;
557
+ }
558
+ console.log(`Pushed ${branchName} to origin`);
559
+ return true;
560
+ }
561
+ /**
562
+ * Create a PR for a worktree branch using gh CLI.
563
+ */
564
+ createPullRequest(sessionName, options) {
565
+ const branchName = this.getBranchName(sessionName);
566
+ // Check if gh CLI is available
567
+ const ghCheck = spawnSync("which", ["gh"], { encoding: "utf-8" });
568
+ if (ghCheck.status !== 0) {
569
+ return { success: false, error: "GitHub CLI (gh) not installed" };
570
+ }
571
+ // Create PR
572
+ const prArgs = [
573
+ "pr",
574
+ "create",
575
+ "--base",
576
+ this.baseBranch,
577
+ "--head",
578
+ branchName,
579
+ "--title",
580
+ options.title,
581
+ ];
582
+ if (options.body) {
583
+ prArgs.push("--body", options.body);
584
+ }
585
+ if (options.draft) {
586
+ prArgs.push("--draft");
587
+ }
588
+ const result = spawnSync("gh", prArgs, {
589
+ cwd: this.projectDir,
590
+ encoding: "utf-8",
591
+ timeout: 60000,
592
+ });
593
+ if (result.status !== 0) {
594
+ return { success: false, error: result.stderr || "Failed to create PR" };
595
+ }
596
+ // Extract PR URL from output
597
+ const prUrl = result.stdout.trim();
598
+ return { success: true, prUrl };
599
+ }
473
600
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simonfestl/husky-cli",
3
- "version": "0.9.3",
3
+ "version": "0.9.6",
4
4
  "description": "CLI for Huskyv0 Task Orchestration with Claude Agent SDK",
5
5
  "type": "module",
6
6
  "bin": {