@simonfestl/husky-cli 0.3.0 → 0.5.1

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,400 @@
1
+ import { Command } from "commander";
2
+ export const completionCommand = new Command("completion")
3
+ .description("Generate shell completion scripts");
4
+ // Command definitions for completion
5
+ const COMMANDS = {
6
+ task: [
7
+ "list", "create", "get", "update", "delete", "start", "done", "status", "plan",
8
+ "wait-approval", "complete", "qa-start", "qa-status", "qa-approve", "qa-reject",
9
+ "qa-iteration", "qa-escalate", "merge-conflict"
10
+ ],
11
+ project: ["list", "create", "get", "update", "delete", "add-knowledge", "list-knowledge", "delete-knowledge"],
12
+ roadmap: [
13
+ "list", "create", "get", "update", "delete", "add-phase", "add-feature", "generate",
14
+ "list-features", "update-feature", "delete-feature", "convert-feature"
15
+ ],
16
+ workflow: [
17
+ "list", "create", "get", "update", "delete", "add-step", "update-step", "delete-step",
18
+ "generate-steps", "list-steps"
19
+ ],
20
+ idea: ["list", "create", "get", "update", "delete", "convert"],
21
+ department: ["list", "create", "get", "update", "delete"],
22
+ vm: ["list", "create", "get", "update", "delete", "start", "stop", "logs", "approve", "reject"],
23
+ jules: ["list", "create", "get", "update", "delete", "message", "approve", "activities", "sources"],
24
+ process: ["list", "create", "get", "update", "delete"],
25
+ strategy: [
26
+ "show", "set-vision", "set-mission", "add-value", "update-value", "delete-value",
27
+ "add-goal", "update-goal", "delete-goal", "add-persona", "update-persona", "delete-persona"
28
+ ],
29
+ settings: ["get", "set"],
30
+ "vm-config": ["list", "create", "get", "update", "delete"],
31
+ config: ["set", "get", "list", "test"],
32
+ changelog: ["generate", "list", "show", "publish", "delete"],
33
+ explain: [],
34
+ completion: ["bash", "zsh", "fish", "install"],
35
+ agent: [],
36
+ };
37
+ const MAIN_COMMANDS = Object.keys(COMMANDS).join(" ");
38
+ // Common options across commands
39
+ const COMMON_OPTIONS = "--json --force --status";
40
+ // husky completion bash
41
+ completionCommand
42
+ .command("bash")
43
+ .description("Output bash completion script")
44
+ .action(() => {
45
+ console.log(generateBashCompletion());
46
+ });
47
+ // husky completion zsh
48
+ completionCommand
49
+ .command("zsh")
50
+ .description("Output zsh completion script")
51
+ .action(() => {
52
+ console.log(generateZshCompletion());
53
+ });
54
+ // husky completion fish
55
+ completionCommand
56
+ .command("fish")
57
+ .description("Output fish completion script")
58
+ .action(() => {
59
+ console.log(generateFishCompletion());
60
+ });
61
+ // husky completion install
62
+ completionCommand
63
+ .command("install")
64
+ .description("Print installation instructions for shell completions")
65
+ .action(() => {
66
+ console.log(`
67
+ SHELL COMPLETION INSTALLATION
68
+
69
+ Bash:
70
+ Add to your ~/.bashrc:
71
+ eval "$(husky completion bash)"
72
+
73
+ Or append to file:
74
+ husky completion bash >> ~/.bashrc
75
+ source ~/.bashrc
76
+
77
+ Zsh:
78
+ Add to your ~/.zshrc:
79
+ eval "$(husky completion zsh)"
80
+
81
+ Or append to file:
82
+ husky completion zsh >> ~/.zshrc
83
+ source ~/.zshrc
84
+
85
+ Fish:
86
+ Save to fish completions directory:
87
+ husky completion fish > ~/.config/fish/completions/husky.fish
88
+
89
+ Or create if directory doesn't exist:
90
+ mkdir -p ~/.config/fish/completions
91
+ husky completion fish > ~/.config/fish/completions/husky.fish
92
+
93
+ After installation, restart your shell or source the config file.
94
+ `);
95
+ });
96
+ function generateBashCompletion() {
97
+ const subcommandCases = Object.entries(COMMANDS)
98
+ .filter(([, subs]) => subs.length > 0)
99
+ .map(([cmd, subs]) => ` ${cmd})
100
+ COMPREPLY=( $(compgen -W "${subs.join(" ")}" -- \${cur}) )
101
+ return 0
102
+ ;;`)
103
+ .join("\n");
104
+ return `# Bash completion for husky CLI
105
+ # Generated by husky completion bash
106
+
107
+ _husky_completions() {
108
+ local cur prev words cword
109
+ COMPREPLY=()
110
+ cur="\${COMP_WORDS[COMP_CWORD]}"
111
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
112
+
113
+ # Main commands
114
+ local commands="${MAIN_COMMANDS}"
115
+
116
+ # Common options
117
+ local common_opts="${COMMON_OPTIONS}"
118
+
119
+ # Handle completion based on previous word
120
+ case "\${prev}" in
121
+ husky)
122
+ COMPREPLY=( $(compgen -W "\${commands}" -- \${cur}) )
123
+ return 0
124
+ ;;
125
+ ${subcommandCases}
126
+ --status)
127
+ COMPREPLY=( $(compgen -W "backlog in_progress review done pending running completed failed" -- \${cur}) )
128
+ return 0
129
+ ;;
130
+ --priority)
131
+ COMPREPLY=( $(compgen -W "low medium high urgent must should could wont" -- \${cur}) )
132
+ return 0
133
+ ;;
134
+ --assignee)
135
+ COMPREPLY=( $(compgen -W "human llm unassigned" -- \${cur}) )
136
+ return 0
137
+ ;;
138
+ --agent)
139
+ COMPREPLY=( $(compgen -W "claude-code gemini-cli aider custom" -- \${cur}) )
140
+ return 0
141
+ ;;
142
+ --type)
143
+ COMPREPLY=( $(compgen -W "global project architecture patterns decisions learnings" -- \${cur}) )
144
+ return 0
145
+ ;;
146
+ --value-stream)
147
+ COMPREPLY=( $(compgen -W "order_to_delivery procure_to_pay returns_management product_lifecycle customer_service marketing_sales finance_accounting hr_operations it_operations general" -- \${cur}) )
148
+ return 0
149
+ ;;
150
+ --action)
151
+ COMPREPLY=( $(compgen -W "manual semi_automated fully_automated" -- \${cur}) )
152
+ return 0
153
+ ;;
154
+ --work-status)
155
+ COMPREPLY=( $(compgen -W "planning in_progress review completed on_hold" -- \${cur}) )
156
+ return 0
157
+ ;;
158
+ esac
159
+
160
+ # If current word starts with -, complete options
161
+ if [[ \${cur} == -* ]]; then
162
+ COMPREPLY=( $(compgen -W "\${common_opts} --id --project --description --name --help" -- \${cur}) )
163
+ return 0
164
+ fi
165
+
166
+ # Default: complete with main commands
167
+ if [[ \${COMP_CWORD} -eq 1 ]]; then
168
+ COMPREPLY=( $(compgen -W "\${commands}" -- \${cur}) )
169
+ fi
170
+
171
+ return 0
172
+ }
173
+
174
+ complete -F _husky_completions husky
175
+ `;
176
+ }
177
+ function generateZshCompletion() {
178
+ const subcommandCases = Object.entries(COMMANDS)
179
+ .filter(([, subs]) => subs.length > 0)
180
+ .map(([cmd, subs]) => ` ${cmd})
181
+ _values 'subcommand' ${subs.map(s => `'${s}'`).join(" ")}
182
+ ;;`)
183
+ .join("\n");
184
+ return `#compdef husky
185
+ # Zsh completion for husky CLI
186
+ # Generated by husky completion zsh
187
+
188
+ _husky() {
189
+ local -a commands
190
+ commands=(
191
+ 'task:Manage tasks'
192
+ 'project:Manage projects and knowledge'
193
+ 'roadmap:Manage roadmaps'
194
+ 'workflow:Manage workflows and workflow steps'
195
+ 'idea:Manage ideas'
196
+ 'department:Manage departments'
197
+ 'vm:Manage VM sessions'
198
+ 'jules:Manage Jules AI coding sessions'
199
+ 'process:Manage processes'
200
+ 'strategy:Manage business strategy'
201
+ 'settings:Manage application settings'
202
+ 'vm-config:Manage VM configurations'
203
+ 'config:Manage CLI configuration'
204
+ 'changelog:Generate and manage changelogs'
205
+ 'explain:Explain CLI commands for AI agents'
206
+ 'completion:Generate shell completion scripts'
207
+ 'agent:Interactive agent mode'
208
+ )
209
+
210
+ _arguments -C \\
211
+ '1: :->command' \\
212
+ '*: :->args'
213
+
214
+ case $state in
215
+ command)
216
+ _describe -t commands 'husky commands' commands
217
+ ;;
218
+ args)
219
+ case $words[2] in
220
+ ${subcommandCases}
221
+ esac
222
+ ;;
223
+ esac
224
+ }
225
+
226
+ # Common options completion
227
+ _husky_common_options() {
228
+ _arguments \\
229
+ '--json[Output as JSON]' \\
230
+ '--force[Skip confirmation]' \\
231
+ '--status[Filter by status]:status:(backlog in_progress review done pending running completed failed)' \\
232
+ '--priority[Priority level]:priority:(low medium high urgent must should could wont)' \\
233
+ '--id[Task/Resource ID]:id:' \\
234
+ '--project[Project ID]:project:' \\
235
+ '--help[Show help]'
236
+ }
237
+
238
+ compdef _husky husky
239
+ `;
240
+ }
241
+ function generateFishCompletion() {
242
+ const mainCommandCompletions = Object.entries(COMMANDS)
243
+ .map(([cmd, _]) => {
244
+ const desc = getCommandDescription(cmd);
245
+ return `complete -c husky -n "__fish_use_subcommand" -a "${cmd}" -d "${desc}"`;
246
+ })
247
+ .join("\n");
248
+ const subcommandCompletions = Object.entries(COMMANDS)
249
+ .filter(([, subs]) => subs.length > 0)
250
+ .flatMap(([cmd, subs]) => subs.map(sub => `complete -c husky -n "__fish_seen_subcommand_from ${cmd}" -a "${sub}" -d "${getSubcommandDescription(cmd, sub)}"`))
251
+ .join("\n");
252
+ return `# Fish completion for husky CLI
253
+ # Generated by husky completion fish
254
+
255
+ # Disable file completion by default
256
+ complete -c husky -f
257
+
258
+ # Main commands
259
+ ${mainCommandCompletions}
260
+
261
+ # Subcommands
262
+ ${subcommandCompletions}
263
+
264
+ # Common options
265
+ complete -c husky -l json -d "Output as JSON"
266
+ complete -c husky -l force -d "Skip confirmation"
267
+ complete -c husky -l help -d "Show help"
268
+ complete -c husky -l id -d "Task/Resource ID" -r
269
+ complete -c husky -l project -d "Project ID" -r
270
+ complete -c husky -l status -d "Filter by status" -r -a "backlog in_progress review done pending running completed failed"
271
+ complete -c husky -l priority -d "Priority level" -r -a "low medium high urgent must should could wont"
272
+ complete -c husky -l assignee -d "Assignee type" -r -a "human llm unassigned"
273
+ complete -c husky -l agent -d "Agent type" -r -a "claude-code gemini-cli aider custom"
274
+ complete -c husky -l type -d "Type filter" -r -a "global project architecture patterns decisions learnings"
275
+ complete -c husky -l value-stream -d "Value stream" -r -a "order_to_delivery procure_to_pay returns_management product_lifecycle customer_service marketing_sales finance_accounting hr_operations it_operations general"
276
+ complete -c husky -l action -d "Action type" -r -a "manual semi_automated fully_automated"
277
+ complete -c husky -l work-status -d "Work status" -r -a "planning in_progress review completed on_hold"
278
+ `;
279
+ }
280
+ function getCommandDescription(cmd) {
281
+ const descriptions = {
282
+ task: "Manage tasks",
283
+ project: "Manage projects and knowledge",
284
+ roadmap: "Manage roadmaps",
285
+ workflow: "Manage workflows and workflow steps",
286
+ idea: "Manage ideas",
287
+ department: "Manage departments",
288
+ vm: "Manage VM sessions",
289
+ jules: "Manage Jules AI coding sessions",
290
+ process: "Manage processes",
291
+ strategy: "Manage business strategy",
292
+ settings: "Manage application settings",
293
+ "vm-config": "Manage VM configurations",
294
+ config: "Manage CLI configuration",
295
+ changelog: "Generate and manage changelogs",
296
+ explain: "Explain CLI commands for AI agents",
297
+ completion: "Generate shell completion scripts",
298
+ agent: "Interactive agent mode",
299
+ };
300
+ return descriptions[cmd] || cmd;
301
+ }
302
+ function getSubcommandDescription(cmd, sub) {
303
+ // Common subcommand descriptions
304
+ const common = {
305
+ list: "List all",
306
+ create: "Create new",
307
+ get: "Get details",
308
+ update: "Update",
309
+ delete: "Delete",
310
+ start: "Start",
311
+ stop: "Stop",
312
+ show: "Show",
313
+ };
314
+ // Command-specific descriptions
315
+ const specific = {
316
+ task: {
317
+ done: "Mark task as done",
318
+ status: "Report task progress status",
319
+ plan: "Submit execution plan for approval",
320
+ "wait-approval": "Wait for plan approval",
321
+ complete: "Mark task as complete with result",
322
+ "qa-start": "Start QA validation",
323
+ "qa-status": "Get QA validation status",
324
+ "qa-approve": "Manually approve QA",
325
+ "qa-reject": "Manually reject QA",
326
+ "qa-iteration": "Add a QA iteration result",
327
+ "qa-escalate": "Escalate QA to human review",
328
+ "merge-conflict": "Resolve a Git merge conflict using AI",
329
+ },
330
+ project: {
331
+ "add-knowledge": "Add a knowledge entry",
332
+ "list-knowledge": "List knowledge entries",
333
+ "delete-knowledge": "Delete a knowledge entry",
334
+ },
335
+ roadmap: {
336
+ "add-phase": "Add a phase to a roadmap",
337
+ "add-feature": "Add a feature to a roadmap phase",
338
+ generate: "Generate roadmap with AI",
339
+ "list-features": "List all features in a roadmap",
340
+ "update-feature": "Update a roadmap feature",
341
+ "delete-feature": "Delete a feature from a roadmap",
342
+ "convert-feature": "Convert a roadmap feature to a task",
343
+ },
344
+ workflow: {
345
+ "add-step": "Add a step to a workflow",
346
+ "update-step": "Update a workflow step",
347
+ "delete-step": "Delete a workflow step",
348
+ "generate-steps": "AI-generate steps from an SOP document",
349
+ "list-steps": "List all steps in a workflow",
350
+ },
351
+ idea: {
352
+ convert: "Convert an idea to a task",
353
+ },
354
+ vm: {
355
+ logs: "Get VM logs",
356
+ approve: "Approve VM session plan",
357
+ reject: "Reject VM session plan",
358
+ },
359
+ jules: {
360
+ message: "Send a message to a Jules session",
361
+ approve: "Approve a Jules session plan",
362
+ activities: "Get session activities",
363
+ sources: "List available Jules sources",
364
+ },
365
+ strategy: {
366
+ "set-vision": "Set/update the company vision",
367
+ "set-mission": "Set/update the company mission",
368
+ "add-value": "Add a company value",
369
+ "update-value": "Update a company value",
370
+ "delete-value": "Delete a company value",
371
+ "add-goal": "Add a strategic goal",
372
+ "update-goal": "Update a strategic goal",
373
+ "delete-goal": "Delete a strategic goal",
374
+ "add-persona": "Add a target audience persona",
375
+ "update-persona": "Update a target audience persona",
376
+ "delete-persona": "Delete a target audience persona",
377
+ },
378
+ config: {
379
+ set: "Set a configuration value",
380
+ test: "Test API connection",
381
+ },
382
+ changelog: {
383
+ generate: "Generate a changelog from git commits",
384
+ publish: "Publish a changelog",
385
+ },
386
+ completion: {
387
+ bash: "Output bash completion script",
388
+ zsh: "Output zsh completion script",
389
+ fish: "Output fish completion script",
390
+ install: "Print installation instructions",
391
+ },
392
+ };
393
+ if (specific[cmd]?.[sub]) {
394
+ return specific[cmd][sub];
395
+ }
396
+ if (common[sub]) {
397
+ return `${common[sub]} ${cmd}`;
398
+ }
399
+ return sub;
400
+ }
@@ -4,5 +4,6 @@ interface Config {
4
4
  apiKey?: string;
5
5
  }
6
6
  export declare function getConfig(): Config;
7
+ export declare function setConfig(key: "apiUrl" | "apiKey", value: string): void;
7
8
  export declare const configCommand: Command;
8
9
  export {};
@@ -4,6 +4,29 @@ import { join } from "path";
4
4
  import { homedir } from "os";
5
5
  const CONFIG_DIR = join(homedir(), ".husky");
6
6
  const CONFIG_FILE = join(CONFIG_DIR, "config.json");
7
+ // API Key validation - must be at least 16 characters and alphanumeric with dashes/underscores
8
+ function validateApiKey(key) {
9
+ if (key.length < 16) {
10
+ return { valid: false, error: "API key must be at least 16 characters long" };
11
+ }
12
+ if (!/^[a-zA-Z0-9_-]+$/.test(key)) {
13
+ return { valid: false, error: "API key must only contain letters, numbers, dashes, and underscores" };
14
+ }
15
+ return { valid: true };
16
+ }
17
+ // URL validation
18
+ function validateApiUrl(url) {
19
+ try {
20
+ const parsed = new URL(url);
21
+ if (!["http:", "https:"].includes(parsed.protocol)) {
22
+ return { valid: false, error: "API URL must use http or https protocol" };
23
+ }
24
+ return { valid: true };
25
+ }
26
+ catch {
27
+ return { valid: false, error: "Invalid URL format" };
28
+ }
29
+ }
7
30
  export function getConfig() {
8
31
  try {
9
32
  if (!existsSync(CONFIG_FILE)) {
@@ -22,6 +45,12 @@ function saveConfig(config) {
22
45
  }
23
46
  writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
24
47
  }
48
+ // Helper to set a single config value (used by interactive mode)
49
+ export function setConfig(key, value) {
50
+ const config = getConfig();
51
+ config[key] = value;
52
+ saveConfig(config);
53
+ }
25
54
  export const configCommand = new Command("config")
26
55
  .description("Manage CLI configuration");
27
56
  // husky config set <key> <value>
@@ -31,9 +60,19 @@ configCommand
31
60
  .action((key, value) => {
32
61
  const config = getConfig();
33
62
  if (key === "api-url") {
63
+ const validation = validateApiUrl(value);
64
+ if (!validation.valid) {
65
+ console.error(`Error: ${validation.error}`);
66
+ process.exit(1);
67
+ }
34
68
  config.apiUrl = value;
35
69
  }
36
70
  else if (key === "api-key") {
71
+ const validation = validateApiKey(value);
72
+ if (!validation.valid) {
73
+ console.error(`Error: ${validation.error}`);
74
+ process.exit(1);
75
+ }
37
76
  config.apiKey = value;
38
77
  }
39
78
  else {
@@ -42,7 +81,7 @@ configCommand
42
81
  process.exit(1);
43
82
  }
44
83
  saveConfig(config);
45
- console.log(`✓ Set ${key} = ${key === "api-key" ? "***" : value}`);
84
+ console.log(`Set ${key} = ${key === "api-key" ? "***" : value}`);
46
85
  });
47
86
  // husky config get <key>
48
87
  configCommand
@@ -71,3 +110,64 @@ configCommand
71
110
  console.log(` api-url: ${config.apiUrl || "(not set)"}`);
72
111
  console.log(` api-key: ${config.apiKey ? "***" : "(not set)"}`);
73
112
  });
113
+ // husky config test
114
+ configCommand
115
+ .command("test")
116
+ .description("Test API connection with configured credentials")
117
+ .action(async () => {
118
+ const config = getConfig();
119
+ // Check if configuration is complete
120
+ if (!config.apiUrl) {
121
+ console.error("Error: API URL not configured. Run: husky config set api-url <url>");
122
+ process.exit(1);
123
+ }
124
+ if (!config.apiKey) {
125
+ console.error("Error: API key not configured. Run: husky config set api-key <key>");
126
+ process.exit(1);
127
+ }
128
+ console.log("Testing API connection...");
129
+ try {
130
+ const url = new URL("/api/tasks", config.apiUrl);
131
+ const res = await fetch(url.toString(), {
132
+ headers: {
133
+ "x-api-key": config.apiKey,
134
+ },
135
+ });
136
+ if (res.ok) {
137
+ console.log(`API connection successful (API URL: ${config.apiUrl})`);
138
+ }
139
+ else if (res.status === 401) {
140
+ console.error(`API connection failed: Unauthorized (HTTP 401)`);
141
+ console.error(" Check your API key with: husky config set api-key <key>");
142
+ process.exit(1);
143
+ }
144
+ else if (res.status === 403) {
145
+ console.error(`API connection failed: Forbidden (HTTP 403)`);
146
+ console.error(" Your API key may not have the required permissions");
147
+ process.exit(1);
148
+ }
149
+ else {
150
+ console.error(`API connection failed: HTTP ${res.status}`);
151
+ try {
152
+ const body = await res.json();
153
+ if (body.error) {
154
+ console.error(` Error: ${body.error}`);
155
+ }
156
+ }
157
+ catch {
158
+ // Ignore JSON parse errors
159
+ }
160
+ process.exit(1);
161
+ }
162
+ }
163
+ catch (error) {
164
+ if (error instanceof TypeError && error.message.includes("fetch")) {
165
+ console.error(`API connection failed: Could not connect to ${config.apiUrl}`);
166
+ console.error(" Check your API URL with: husky config set api-url <url>");
167
+ }
168
+ else {
169
+ console.error(`API connection failed: ${error instanceof Error ? error.message : "Unknown error"}`);
170
+ }
171
+ process.exit(1);
172
+ }
173
+ });
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const departmentCommand: Command;