@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.
- package/dist/commands/llm-context.d.ts +2 -0
- package/dist/commands/llm-context.js +105 -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 +7 -2
- 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
|
@@ -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
|
+
}
|
package/dist/lib/worktree.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/lib/worktree.js
CHANGED
|
@@ -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
|
}
|