@simonfestl/husky-cli 0.3.0
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/README.md +144 -0
- package/dist/commands/agent.d.ts +2 -0
- package/dist/commands/agent.js +279 -0
- package/dist/commands/config.d.ts +8 -0
- package/dist/commands/config.js +73 -0
- package/dist/commands/roadmap.d.ts +2 -0
- package/dist/commands/roadmap.js +325 -0
- package/dist/commands/task.d.ts +2 -0
- package/dist/commands/task.js +635 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +16 -0
- package/dist/lib/streaming.d.ts +44 -0
- package/dist/lib/streaming.js +157 -0
- package/package.json +30 -0
package/README.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Husky CLI
|
|
2
|
+
|
|
3
|
+
CLI for Huskyv0 Task Orchestration with Claude Agent SDK integration.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# From GitHub (recommended for VMs)
|
|
9
|
+
npm install -g github:simon-sfxecom/husky-cli
|
|
10
|
+
|
|
11
|
+
# Local development
|
|
12
|
+
cd packages/cli
|
|
13
|
+
npm install
|
|
14
|
+
npm run build
|
|
15
|
+
npm link
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Commands
|
|
19
|
+
|
|
20
|
+
### Task Management
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# List tasks
|
|
24
|
+
husky task list
|
|
25
|
+
husky task list --status in_progress
|
|
26
|
+
|
|
27
|
+
# Create task
|
|
28
|
+
husky task create "Fix login bug" --priority high --project abc123
|
|
29
|
+
|
|
30
|
+
# Start/complete tasks
|
|
31
|
+
husky task start <task-id>
|
|
32
|
+
husky task done <task-id> --pr https://github.com/...
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Configuration
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# Set API URL and key
|
|
39
|
+
husky config set api-url https://your-husky-dashboard.run.app
|
|
40
|
+
husky config set api-key your-api-key
|
|
41
|
+
|
|
42
|
+
# View config
|
|
43
|
+
husky config list
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Agent Commands (VM Execution)
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# Generate execution plan
|
|
50
|
+
husky agent plan \
|
|
51
|
+
--session-id=<session-id> \
|
|
52
|
+
--prompt="Fix all TypeScript errors" \
|
|
53
|
+
--api-url=https://husky.example.com \
|
|
54
|
+
--api-key=<api-key> \
|
|
55
|
+
--anthropic-key=<anthropic-key> \
|
|
56
|
+
--workdir=/workspace
|
|
57
|
+
|
|
58
|
+
# Wait for user approval
|
|
59
|
+
husky agent wait-approval \
|
|
60
|
+
--session-id=<session-id> \
|
|
61
|
+
--api-url=https://husky.example.com \
|
|
62
|
+
--api-key=<api-key> \
|
|
63
|
+
--timeout=1800
|
|
64
|
+
|
|
65
|
+
# Execute approved plan
|
|
66
|
+
husky agent execute \
|
|
67
|
+
--session-id=<session-id> \
|
|
68
|
+
--api-url=https://husky.example.com \
|
|
69
|
+
--api-key=<api-key> \
|
|
70
|
+
--anthropic-key=<anthropic-key> \
|
|
71
|
+
--github-token=<github-token>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Environment Variables
|
|
75
|
+
|
|
76
|
+
| Variable | Description |
|
|
77
|
+
|----------|-------------|
|
|
78
|
+
| `ANTHROPIC_API_KEY` | Anthropic API key for Claude |
|
|
79
|
+
| `HUSKY_API_URL` | Husky Dashboard URL |
|
|
80
|
+
| `HUSKY_API_KEY` | Husky API key |
|
|
81
|
+
| `GITHUB_TOKEN` | GitHub token for commits (optional) |
|
|
82
|
+
|
|
83
|
+
## Development
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# Install dependencies
|
|
87
|
+
npm install
|
|
88
|
+
|
|
89
|
+
# Build
|
|
90
|
+
npm run build
|
|
91
|
+
|
|
92
|
+
# Watch mode
|
|
93
|
+
npm run dev
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Publishing / Release
|
|
97
|
+
|
|
98
|
+
Die CLI wird automatisch via GitHub Actions auf npm veroeffentlicht.
|
|
99
|
+
|
|
100
|
+
### Neue Version veroeffentlichen
|
|
101
|
+
|
|
102
|
+
1. **Version in `package.json` aktualisieren:**
|
|
103
|
+
```bash
|
|
104
|
+
npm version patch # oder minor/major
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
2. **GitHub Release erstellen:**
|
|
108
|
+
- Gehe zu GitHub > Releases > "Create a new release"
|
|
109
|
+
- Tag muss zur Version passen (z.B. `v0.3.0` fuer Version `0.3.0`)
|
|
110
|
+
- Release Notes hinzufuegen
|
|
111
|
+
- "Publish release" klicken
|
|
112
|
+
|
|
113
|
+
3. **Automatische Veroeffentlichung:**
|
|
114
|
+
- Der Workflow (`.github/workflows/publish.yml`) wird getriggert
|
|
115
|
+
- Package wird mit npm provenance auf npm veroeffentlicht
|
|
116
|
+
|
|
117
|
+
### Manueller Trigger
|
|
118
|
+
|
|
119
|
+
Falls noetig, kann der Workflow auch manuell ausgeloest werden:
|
|
120
|
+
- GitHub > Actions > "Publish CLI to npm" > "Run workflow"
|
|
121
|
+
|
|
122
|
+
### Voraussetzungen
|
|
123
|
+
|
|
124
|
+
| Secret | Beschreibung |
|
|
125
|
+
|--------|--------------|
|
|
126
|
+
| `NPM_TOKEN` | npm Access Token mit publish Berechtigung. Muss in GitHub Repo Settings > Secrets > Actions hinterlegt sein. |
|
|
127
|
+
|
|
128
|
+
## Changelog
|
|
129
|
+
|
|
130
|
+
### v0.2.0 (2025-01-05)
|
|
131
|
+
- Added `agent` commands (plan, execute, wait-approval)
|
|
132
|
+
- Added StreamClient with batching for efficient API calls
|
|
133
|
+
- Integration with Claude Agent SDK
|
|
134
|
+
- Support for planning phase before execution
|
|
135
|
+
|
|
136
|
+
### v0.1.0 (2025-01-04)
|
|
137
|
+
- Initial release
|
|
138
|
+
- Task management commands (list, create, start, done)
|
|
139
|
+
- Configuration management
|
|
140
|
+
- API key authentication
|
|
141
|
+
|
|
142
|
+
## License
|
|
143
|
+
|
|
144
|
+
MIT
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
import { StreamClient, updateSessionStatus, submitPlan, waitForApproval, } from "../lib/streaming.js";
|
|
4
|
+
// ============================================
|
|
5
|
+
// DEPRECATION NOTICE
|
|
6
|
+
// ============================================
|
|
7
|
+
// The 'husky agent' commands are DEPRECATED and will be removed in a future version.
|
|
8
|
+
//
|
|
9
|
+
// The new architecture has Claude Code (or any AI agent) as the main process,
|
|
10
|
+
// and uses 'husky task' commands as Bash tools for communication.
|
|
11
|
+
//
|
|
12
|
+
// Migration Guide:
|
|
13
|
+
// ----------------
|
|
14
|
+
// OLD (deprecated):
|
|
15
|
+
// husky agent plan --session-id xyz --prompt "..."
|
|
16
|
+
// husky agent wait-approval --session-id xyz
|
|
17
|
+
// husky agent execute --session-id xyz
|
|
18
|
+
//
|
|
19
|
+
// NEW (recommended):
|
|
20
|
+
// export HUSKY_TASK_ID="xyz"
|
|
21
|
+
// husky task status "Working on task..."
|
|
22
|
+
// husky task plan --summary "Plan description" --steps "step1,step2"
|
|
23
|
+
// husky task wait-approval --timeout 1800
|
|
24
|
+
// husky task complete --output "Done" --pr "https://..."
|
|
25
|
+
//
|
|
26
|
+
// The new approach is agent-agnostic - works with Claude Code, Gemini, Codex, etc.
|
|
27
|
+
// ============================================
|
|
28
|
+
function showDeprecationWarning(command) {
|
|
29
|
+
console.warn("\n" + "=".repeat(60));
|
|
30
|
+
console.warn("DEPRECATION WARNING");
|
|
31
|
+
console.warn("=".repeat(60));
|
|
32
|
+
console.warn(`The 'husky agent ${command}' command is deprecated.`);
|
|
33
|
+
console.warn("");
|
|
34
|
+
console.warn("Please migrate to the new 'husky task' commands:");
|
|
35
|
+
console.warn(" husky task status <message> - Report progress");
|
|
36
|
+
console.warn(" husky task plan --summary ... - Submit plan");
|
|
37
|
+
console.warn(" husky task wait-approval - Wait for approval");
|
|
38
|
+
console.warn(" husky task complete --output - Mark complete");
|
|
39
|
+
console.warn("");
|
|
40
|
+
console.warn("Set HUSKY_TASK_ID environment variable instead of --session-id");
|
|
41
|
+
console.warn("=".repeat(60) + "\n");
|
|
42
|
+
}
|
|
43
|
+
export const agentCommand = new Command("agent").description("[DEPRECATED] Run Claude Agent for automated code tasks. Use 'husky task' commands instead.");
|
|
44
|
+
// husky agent plan
|
|
45
|
+
agentCommand
|
|
46
|
+
.command("plan")
|
|
47
|
+
.description("Generate an execution plan using Claude")
|
|
48
|
+
.requiredOption("--session-id <id>", "VM Session ID")
|
|
49
|
+
.requiredOption("--prompt <prompt>", "Task prompt")
|
|
50
|
+
.requiredOption("--api-url <url>", "Husky API URL")
|
|
51
|
+
.requiredOption("--api-key <key>", "Husky API Key")
|
|
52
|
+
.requiredOption("--anthropic-key <key>", "Anthropic API Key")
|
|
53
|
+
.option("--workdir <path>", "Working directory", process.cwd())
|
|
54
|
+
.option("--max-budget <usd>", "Max budget in USD", "2.0")
|
|
55
|
+
.action(async (options) => {
|
|
56
|
+
showDeprecationWarning("plan");
|
|
57
|
+
const streamClient = new StreamClient(options.apiUrl, options.sessionId, options.apiKey);
|
|
58
|
+
try {
|
|
59
|
+
await streamClient.system("Starting plan generation...");
|
|
60
|
+
await updateSessionStatus(options.apiUrl, options.sessionId, options.apiKey, "planning");
|
|
61
|
+
// Use Claude Code CLI in plan mode
|
|
62
|
+
const planPrompt = `You are in PLAN MODE. Analyze the following task and create a detailed execution plan. Do NOT execute any changes yet.
|
|
63
|
+
|
|
64
|
+
TASK: ${options.prompt}
|
|
65
|
+
|
|
66
|
+
Create a structured plan with:
|
|
67
|
+
1. Step-by-step actions needed
|
|
68
|
+
2. Files that will be modified
|
|
69
|
+
3. Risk assessment (low/medium/high) for each step
|
|
70
|
+
4. Estimated time for execution
|
|
71
|
+
|
|
72
|
+
Output your plan in a clear, numbered format. After planning, use the ExitPlanMode tool to indicate completion.`;
|
|
73
|
+
await streamClient.system("Invoking Claude for planning...");
|
|
74
|
+
// Run Claude Code in print mode for planning
|
|
75
|
+
const result = await runClaudeCode(planPrompt, options.workdir, options.anthropicKey, streamClient, parseFloat(options.maxBudget));
|
|
76
|
+
// Parse the plan from Claude's output
|
|
77
|
+
const plan = parsePlanFromOutput(result.output);
|
|
78
|
+
// Submit plan to Husky
|
|
79
|
+
await submitPlan(options.apiUrl, options.sessionId, options.apiKey, plan);
|
|
80
|
+
await updateSessionStatus(options.apiUrl, options.sessionId, options.apiKey, "awaiting_approval");
|
|
81
|
+
await streamClient.system("Plan submitted. Waiting for approval...");
|
|
82
|
+
console.log("Plan generated successfully");
|
|
83
|
+
process.exit(0);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
await streamClient.stderr(`Plan generation failed: ${error}`);
|
|
87
|
+
await updateSessionStatus(options.apiUrl, options.sessionId, options.apiKey, "failed", { lastError: String(error) });
|
|
88
|
+
console.error("Plan generation failed:", error);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
// husky agent execute
|
|
93
|
+
agentCommand
|
|
94
|
+
.command("execute")
|
|
95
|
+
.description("Execute the approved plan")
|
|
96
|
+
.requiredOption("--session-id <id>", "VM Session ID")
|
|
97
|
+
.requiredOption("--api-url <url>", "Husky API URL")
|
|
98
|
+
.requiredOption("--api-key <key>", "Husky API Key")
|
|
99
|
+
.requiredOption("--anthropic-key <key>", "Anthropic API Key")
|
|
100
|
+
.option("--workdir <path>", "Working directory", process.cwd())
|
|
101
|
+
.option("--github-token <token>", "GitHub token for commits")
|
|
102
|
+
.option("--max-budget <usd>", "Max budget in USD", "5.0")
|
|
103
|
+
.action(async (options) => {
|
|
104
|
+
showDeprecationWarning("execute");
|
|
105
|
+
const streamClient = new StreamClient(options.apiUrl, options.sessionId, options.apiKey);
|
|
106
|
+
try {
|
|
107
|
+
await streamClient.system("Starting execution...");
|
|
108
|
+
await updateSessionStatus(options.apiUrl, options.sessionId, options.apiKey, "running");
|
|
109
|
+
// Set GitHub token if provided
|
|
110
|
+
if (options.githubToken) {
|
|
111
|
+
process.env.GITHUB_TOKEN = options.githubToken;
|
|
112
|
+
}
|
|
113
|
+
// Fetch the original prompt from the session
|
|
114
|
+
const sessionResponse = await fetch(`${options.apiUrl}/api/vm-sessions/${options.sessionId}`, {
|
|
115
|
+
headers: { "X-API-Key": options.apiKey },
|
|
116
|
+
});
|
|
117
|
+
if (!sessionResponse.ok) {
|
|
118
|
+
throw new Error("Failed to fetch session details");
|
|
119
|
+
}
|
|
120
|
+
const session = await sessionResponse.json();
|
|
121
|
+
const prompt = session.prompt;
|
|
122
|
+
await streamClient.system(`Executing task: ${prompt}`);
|
|
123
|
+
// Run Claude Code to execute the task
|
|
124
|
+
const result = await runClaudeCode(prompt, options.workdir, options.anthropicKey, streamClient, parseFloat(options.maxBudget));
|
|
125
|
+
await streamClient.system(`Execution completed with exit code: ${result.exitCode}`);
|
|
126
|
+
// Report completion
|
|
127
|
+
await fetch(`${options.apiUrl}/api/webhooks/vm/completion`, {
|
|
128
|
+
method: "POST",
|
|
129
|
+
headers: {
|
|
130
|
+
"Content-Type": "application/json",
|
|
131
|
+
"X-API-Key": options.apiKey,
|
|
132
|
+
},
|
|
133
|
+
body: JSON.stringify({
|
|
134
|
+
sessionId: options.sessionId,
|
|
135
|
+
exitCode: result.exitCode,
|
|
136
|
+
output: result.output,
|
|
137
|
+
}),
|
|
138
|
+
});
|
|
139
|
+
console.log("Execution completed");
|
|
140
|
+
process.exit(result.exitCode);
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
await streamClient.stderr(`Execution failed: ${error}`);
|
|
144
|
+
await updateSessionStatus(options.apiUrl, options.sessionId, options.apiKey, "failed", { lastError: String(error) });
|
|
145
|
+
console.error("Execution failed:", error);
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
// husky agent wait-approval
|
|
150
|
+
agentCommand
|
|
151
|
+
.command("wait-approval")
|
|
152
|
+
.description("Wait for plan approval")
|
|
153
|
+
.requiredOption("--session-id <id>", "VM Session ID")
|
|
154
|
+
.requiredOption("--api-url <url>", "Husky API URL")
|
|
155
|
+
.requiredOption("--api-key <key>", "Husky API Key")
|
|
156
|
+
.option("--timeout <seconds>", "Timeout in seconds", "1800")
|
|
157
|
+
.action(async (options) => {
|
|
158
|
+
showDeprecationWarning("wait-approval");
|
|
159
|
+
const timeoutMs = parseInt(options.timeout) * 1000;
|
|
160
|
+
console.error(`Waiting for approval (timeout: ${options.timeout}s)...`);
|
|
161
|
+
const result = await waitForApproval(options.apiUrl, options.sessionId, options.apiKey, timeoutMs);
|
|
162
|
+
// Output result to stdout for shell script to capture
|
|
163
|
+
console.log(result);
|
|
164
|
+
process.exit(result === "approved" ? 0 : 1);
|
|
165
|
+
});
|
|
166
|
+
/**
|
|
167
|
+
* Run Claude Code CLI and stream output
|
|
168
|
+
*/
|
|
169
|
+
async function runClaudeCode(prompt, workdir, anthropicKey, streamClient, maxBudgetUsd) {
|
|
170
|
+
return new Promise((resolve, reject) => {
|
|
171
|
+
const outputLines = [];
|
|
172
|
+
// Spawn Claude Code process
|
|
173
|
+
const claude = spawn("claude", [
|
|
174
|
+
"-p",
|
|
175
|
+
prompt,
|
|
176
|
+
"--output-format",
|
|
177
|
+
"stream-json",
|
|
178
|
+
"--max-turns",
|
|
179
|
+
"50",
|
|
180
|
+
], {
|
|
181
|
+
cwd: workdir,
|
|
182
|
+
env: {
|
|
183
|
+
...process.env,
|
|
184
|
+
ANTHROPIC_API_KEY: anthropicKey,
|
|
185
|
+
},
|
|
186
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
187
|
+
});
|
|
188
|
+
claude.stdout.on("data", async (data) => {
|
|
189
|
+
const text = data.toString();
|
|
190
|
+
outputLines.push(text);
|
|
191
|
+
// Try to parse JSON messages
|
|
192
|
+
const lines = text.split("\n").filter(Boolean);
|
|
193
|
+
for (const line of lines) {
|
|
194
|
+
try {
|
|
195
|
+
const msg = JSON.parse(line);
|
|
196
|
+
if (msg.type === "assistant" && msg.message?.content) {
|
|
197
|
+
for (const block of msg.message.content) {
|
|
198
|
+
if (block.type === "text") {
|
|
199
|
+
await streamClient.stdout(block.text);
|
|
200
|
+
}
|
|
201
|
+
else if (block.type === "tool_use") {
|
|
202
|
+
await streamClient.system(`Using tool: ${block.name}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
else if (msg.type === "result") {
|
|
207
|
+
await streamClient.system(`Cost: $${msg.cost_usd?.toFixed(4) || "unknown"}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
// Not JSON, send as plain text
|
|
212
|
+
await streamClient.stdout(line);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
claude.stderr.on("data", async (data) => {
|
|
217
|
+
const text = data.toString();
|
|
218
|
+
outputLines.push(text);
|
|
219
|
+
await streamClient.stderr(text);
|
|
220
|
+
});
|
|
221
|
+
claude.on("error", (error) => {
|
|
222
|
+
reject(error);
|
|
223
|
+
});
|
|
224
|
+
claude.on("close", (code) => {
|
|
225
|
+
resolve({
|
|
226
|
+
exitCode: code ?? 1,
|
|
227
|
+
output: outputLines.join("\n").slice(0, 500000), // Limit to 500KB
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Parse plan from Claude's output
|
|
234
|
+
*/
|
|
235
|
+
function parsePlanFromOutput(output) {
|
|
236
|
+
// Simple parsing - extract numbered steps
|
|
237
|
+
const steps = [];
|
|
238
|
+
const lines = output.split("\n");
|
|
239
|
+
let currentStep = 0;
|
|
240
|
+
for (const line of lines) {
|
|
241
|
+
// Match numbered steps like "1." or "Step 1:"
|
|
242
|
+
const stepMatch = line.match(/^(?:Step\s+)?(\d+)[.):]\s*(.+)/i);
|
|
243
|
+
if (stepMatch) {
|
|
244
|
+
currentStep = parseInt(stepMatch[1]);
|
|
245
|
+
const description = stepMatch[2].trim();
|
|
246
|
+
// Extract file paths mentioned
|
|
247
|
+
const fileMatches = description.match(/`([^`]+\.[a-z]+)`/g) || [];
|
|
248
|
+
const files = fileMatches.map((f) => f.replace(/`/g, ""));
|
|
249
|
+
// Determine risk level based on keywords
|
|
250
|
+
let risk = "low";
|
|
251
|
+
if (/delete|remove|drop|danger/i.test(description)) {
|
|
252
|
+
risk = "high";
|
|
253
|
+
}
|
|
254
|
+
else if (/modify|change|update|refactor/i.test(description)) {
|
|
255
|
+
risk = "medium";
|
|
256
|
+
}
|
|
257
|
+
steps.push({
|
|
258
|
+
order: currentStep,
|
|
259
|
+
description,
|
|
260
|
+
files,
|
|
261
|
+
risk,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// If no steps found, create a generic one
|
|
266
|
+
if (steps.length === 0) {
|
|
267
|
+
steps.push({
|
|
268
|
+
order: 1,
|
|
269
|
+
description: "Execute task as specified",
|
|
270
|
+
files: [],
|
|
271
|
+
risk: "medium",
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
return {
|
|
275
|
+
steps,
|
|
276
|
+
estimatedCost: 0.5, // Placeholder
|
|
277
|
+
estimatedRuntime: 5, // 5 minutes placeholder
|
|
278
|
+
};
|
|
279
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
const CONFIG_DIR = join(homedir(), ".husky");
|
|
6
|
+
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
7
|
+
export function getConfig() {
|
|
8
|
+
try {
|
|
9
|
+
if (!existsSync(CONFIG_FILE)) {
|
|
10
|
+
return {};
|
|
11
|
+
}
|
|
12
|
+
const content = readFileSync(CONFIG_FILE, "utf-8");
|
|
13
|
+
return JSON.parse(content);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function saveConfig(config) {
|
|
20
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
21
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
24
|
+
}
|
|
25
|
+
export const configCommand = new Command("config")
|
|
26
|
+
.description("Manage CLI configuration");
|
|
27
|
+
// husky config set <key> <value>
|
|
28
|
+
configCommand
|
|
29
|
+
.command("set <key> <value>")
|
|
30
|
+
.description("Set a configuration value")
|
|
31
|
+
.action((key, value) => {
|
|
32
|
+
const config = getConfig();
|
|
33
|
+
if (key === "api-url") {
|
|
34
|
+
config.apiUrl = value;
|
|
35
|
+
}
|
|
36
|
+
else if (key === "api-key") {
|
|
37
|
+
config.apiKey = value;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
console.error(`Unknown config key: ${key}`);
|
|
41
|
+
console.log("Available keys: api-url, api-key");
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
saveConfig(config);
|
|
45
|
+
console.log(`✓ Set ${key} = ${key === "api-key" ? "***" : value}`);
|
|
46
|
+
});
|
|
47
|
+
// husky config get <key>
|
|
48
|
+
configCommand
|
|
49
|
+
.command("get <key>")
|
|
50
|
+
.description("Get a configuration value")
|
|
51
|
+
.action((key) => {
|
|
52
|
+
const config = getConfig();
|
|
53
|
+
if (key === "api-url") {
|
|
54
|
+
console.log(config.apiUrl || "(not set)");
|
|
55
|
+
}
|
|
56
|
+
else if (key === "api-key") {
|
|
57
|
+
console.log(config.apiKey ? "***" : "(not set)");
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
console.error(`Unknown config key: ${key}`);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
// husky config list
|
|
65
|
+
configCommand
|
|
66
|
+
.command("list")
|
|
67
|
+
.description("List all configuration")
|
|
68
|
+
.action(() => {
|
|
69
|
+
const config = getConfig();
|
|
70
|
+
console.log("Configuration:");
|
|
71
|
+
console.log(` api-url: ${config.apiUrl || "(not set)"}`);
|
|
72
|
+
console.log(` api-key: ${config.apiKey ? "***" : "(not set)"}`);
|
|
73
|
+
});
|