@simonfestl/husky-cli 1.8.2 ā 1.9.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.
- package/dist/commands/biz/tickets.js +2 -2
- package/dist/commands/config.js +14 -12
- package/dist/commands/task.js +19 -19
- package/dist/lib/biz/qdrant.d.ts +1 -4
- package/dist/lib/biz/qdrant.js +9 -12
- package/dist/lib/error-hints.d.ts +69 -0
- package/dist/lib/error-hints.js +164 -0
- package/dist/lib/permissions.js +3 -0
- package/package.json +1 -1
|
@@ -8,6 +8,7 @@ import { ZendeskClient } from "../../lib/biz/index.js";
|
|
|
8
8
|
import { AgentBrain } from "../../lib/biz/agent-brain.js";
|
|
9
9
|
import * as fs from "fs";
|
|
10
10
|
import * as path from "path";
|
|
11
|
+
import { errorWithAutoHint } from "../../lib/error-hints.js";
|
|
11
12
|
export const ticketsCommand = new Command("tickets")
|
|
12
13
|
.description("Manage support tickets (Zendesk)");
|
|
13
14
|
// husky biz tickets list
|
|
@@ -159,8 +160,7 @@ ticketsCommand
|
|
|
159
160
|
});
|
|
160
161
|
}
|
|
161
162
|
if (Object.keys(updates).length === 0) {
|
|
162
|
-
|
|
163
|
-
process.exit(1);
|
|
163
|
+
errorWithAutoHint("Provide --status, --priority, or --field. Use --help for options.");
|
|
164
164
|
}
|
|
165
165
|
const ticket = await client.updateTicket(parseInt(id, 10), updates);
|
|
166
166
|
if (options.json) {
|
package/dist/commands/config.js
CHANGED
|
@@ -2,6 +2,7 @@ import { Command } from "commander";
|
|
|
2
2
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
import { homedir } from "os";
|
|
5
|
+
import { ErrorHelpers, errorWithHint, ExplainTopic } from "../lib/error-hints.js";
|
|
5
6
|
const CONFIG_DIR = join(homedir(), ".husky");
|
|
6
7
|
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
7
8
|
// API Key validation - must be at least 16 characters, alphanumeric + common key chars (base64, JWT, etc.)
|
|
@@ -180,21 +181,20 @@ configCommand
|
|
|
180
181
|
console.log(" Gemini: gemini-api-key");
|
|
181
182
|
console.log(" NocoDB: nocodb-api-token, nocodb-base-url, nocodb-workspace-id");
|
|
182
183
|
console.log(" Brain: agent-type");
|
|
184
|
+
console.error("\nš” For configuration help: husky explain config");
|
|
183
185
|
process.exit(1);
|
|
184
186
|
}
|
|
185
187
|
// Validation for specific keys
|
|
186
188
|
if (key === "api-url" || key === "billbee-base-url") {
|
|
187
189
|
const validation = validateApiUrl(value);
|
|
188
190
|
if (!validation.valid) {
|
|
189
|
-
|
|
190
|
-
process.exit(1);
|
|
191
|
+
errorWithHint(validation.error || "Invalid URL", ExplainTopic.CONFIG, "Learn about proper URL format");
|
|
191
192
|
}
|
|
192
193
|
}
|
|
193
194
|
if (key === "agent-type") {
|
|
194
195
|
const validTypes = ["support", "claude", "gotess", "supervisor", "worker"];
|
|
195
196
|
if (!validTypes.includes(value)) {
|
|
196
|
-
|
|
197
|
-
process.exit(1);
|
|
197
|
+
errorWithHint(`Invalid agent type. Must be one of: ${validTypes.join(", ")}`, ExplainTopic.CONFIG, "See available configuration options");
|
|
198
198
|
}
|
|
199
199
|
}
|
|
200
200
|
// Set the value
|
|
@@ -240,41 +240,41 @@ configCommand
|
|
|
240
240
|
const config = getConfig();
|
|
241
241
|
// Check if configuration is complete
|
|
242
242
|
if (!config.apiUrl) {
|
|
243
|
-
|
|
244
|
-
process.exit(1);
|
|
243
|
+
ErrorHelpers.missingApiUrl();
|
|
245
244
|
}
|
|
246
245
|
if (!config.apiKey) {
|
|
247
|
-
|
|
248
|
-
process.exit(1);
|
|
246
|
+
ErrorHelpers.missingApiKey();
|
|
249
247
|
}
|
|
250
248
|
console.log("Testing API connection...");
|
|
251
249
|
try {
|
|
252
250
|
// First test basic connectivity with /api/tasks
|
|
253
251
|
const tasksUrl = new URL("/api/tasks", config.apiUrl);
|
|
252
|
+
const apiKey = config.apiKey; // We already checked it's defined above
|
|
254
253
|
const tasksRes = await fetch(tasksUrl.toString(), {
|
|
255
|
-
headers: { "x-api-key":
|
|
254
|
+
headers: { "x-api-key": apiKey },
|
|
256
255
|
});
|
|
257
256
|
if (!tasksRes.ok) {
|
|
258
257
|
if (tasksRes.status === 401) {
|
|
259
258
|
console.error(`API connection failed: Unauthorized (HTTP 401)`);
|
|
260
259
|
console.error(" Check your API key with: husky config set api-key <key>");
|
|
260
|
+
console.error("\nš” For configuration help: husky explain config");
|
|
261
261
|
process.exit(1);
|
|
262
262
|
}
|
|
263
263
|
else if (tasksRes.status === 403) {
|
|
264
264
|
console.error(`API connection failed: Forbidden (HTTP 403)`);
|
|
265
265
|
console.error(" Your API key may not have the required permissions");
|
|
266
|
+
console.error("\nš” For configuration help: husky explain config");
|
|
266
267
|
process.exit(1);
|
|
267
268
|
}
|
|
268
269
|
else {
|
|
269
|
-
|
|
270
|
-
process.exit(1);
|
|
270
|
+
errorWithHint(`API connection failed: HTTP ${tasksRes.status}`, ExplainTopic.CONFIG, "Check your API configuration");
|
|
271
271
|
}
|
|
272
272
|
}
|
|
273
273
|
console.log(`API connection successful (API URL: ${config.apiUrl})`);
|
|
274
274
|
// Now fetch role/permissions from whoami
|
|
275
275
|
const whoamiUrl = new URL("/api/auth/whoami", config.apiUrl);
|
|
276
276
|
const whoamiRes = await fetch(whoamiUrl.toString(), {
|
|
277
|
-
headers: { "x-api-key":
|
|
277
|
+
headers: { "x-api-key": apiKey },
|
|
278
278
|
});
|
|
279
279
|
if (whoamiRes.ok) {
|
|
280
280
|
const data = await whoamiRes.json();
|
|
@@ -298,9 +298,11 @@ configCommand
|
|
|
298
298
|
if (error instanceof TypeError && error.message.includes("fetch")) {
|
|
299
299
|
console.error(`API connection failed: Could not connect to ${config.apiUrl}`);
|
|
300
300
|
console.error(" Check your API URL with: husky config set api-url <url>");
|
|
301
|
+
console.error("\nš” For configuration help: husky explain config");
|
|
301
302
|
}
|
|
302
303
|
else {
|
|
303
304
|
console.error(`API connection failed: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
305
|
+
console.error("\nš” For configuration help: husky explain config");
|
|
304
306
|
}
|
|
305
307
|
process.exit(1);
|
|
306
308
|
}
|
package/dist/commands/task.js
CHANGED
|
@@ -8,14 +8,16 @@ import { WorktreeManager } from "../lib/worktree.js";
|
|
|
8
8
|
import { execSync } from "child_process";
|
|
9
9
|
import { resolveProject, fetchProjects, formatProjectList } from "../lib/project-resolver.js";
|
|
10
10
|
import { requirePermission } from "../lib/permissions.js";
|
|
11
|
+
import { ErrorHelpers, errorWithHint, ExplainTopic } from "../lib/error-hints.js";
|
|
11
12
|
export const taskCommand = new Command("task")
|
|
12
13
|
.description("Manage tasks");
|
|
13
14
|
// Helper: Get task ID from --id flag or HUSKY_TASK_ID env var
|
|
14
15
|
function getTaskId(options) {
|
|
15
16
|
const id = options.id || process.env.HUSKY_TASK_ID;
|
|
16
17
|
if (!id) {
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
ErrorHelpers.missingTaskId();
|
|
19
|
+
// TypeScript doesn't know that ErrorHelpers.missingTaskId() never returns
|
|
20
|
+
throw new Error("unreachable");
|
|
19
21
|
}
|
|
20
22
|
return id;
|
|
21
23
|
}
|
|
@@ -23,8 +25,9 @@ function getTaskId(options) {
|
|
|
23
25
|
function ensureConfig() {
|
|
24
26
|
const config = getConfig();
|
|
25
27
|
if (!config.apiUrl) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
ErrorHelpers.missingApiUrl();
|
|
29
|
+
// TypeScript doesn't know that ErrorHelpers.missingApiUrl() never returns
|
|
30
|
+
throw new Error("unreachable");
|
|
28
31
|
}
|
|
29
32
|
return config;
|
|
30
33
|
}
|
|
@@ -128,8 +131,7 @@ taskCommand
|
|
|
128
131
|
.action(async (options) => {
|
|
129
132
|
const config = getConfig();
|
|
130
133
|
if (!config.apiUrl) {
|
|
131
|
-
|
|
132
|
-
process.exit(1);
|
|
134
|
+
ErrorHelpers.missingApiUrl();
|
|
133
135
|
}
|
|
134
136
|
try {
|
|
135
137
|
const url = new URL("/api/tasks", config.apiUrl);
|
|
@@ -141,7 +143,7 @@ taskCommand
|
|
|
141
143
|
let filterProjectId = null;
|
|
142
144
|
if (!options.all && !options.project) {
|
|
143
145
|
const repoIdentifier = getGitRepoIdentifier();
|
|
144
|
-
if (repoIdentifier) {
|
|
146
|
+
if (repoIdentifier && config.apiUrl) {
|
|
145
147
|
autoDetectedProject = await findProjectByRepo(config.apiUrl, config.apiKey, repoIdentifier);
|
|
146
148
|
if (autoDetectedProject) {
|
|
147
149
|
filterProjectId = autoDetectedProject.id;
|
|
@@ -215,8 +217,7 @@ taskCommand
|
|
|
215
217
|
printTasks(tasks);
|
|
216
218
|
}
|
|
217
219
|
catch (error) {
|
|
218
|
-
|
|
219
|
-
process.exit(1);
|
|
220
|
+
errorWithHint(`Error fetching tasks: ${error instanceof Error ? error.message : error}`, ExplainTopic.TASK, "Learn about task listing and management");
|
|
220
221
|
}
|
|
221
222
|
});
|
|
222
223
|
// husky task start <id>
|
|
@@ -227,14 +228,16 @@ taskCommand
|
|
|
227
228
|
.action(async (id, options) => {
|
|
228
229
|
const config = getConfig();
|
|
229
230
|
if (!config.apiUrl) {
|
|
230
|
-
|
|
231
|
-
|
|
231
|
+
ErrorHelpers.missingApiUrl();
|
|
232
|
+
throw new Error("unreachable");
|
|
232
233
|
}
|
|
234
|
+
const apiUrl = config.apiUrl; // Type narrowing
|
|
235
|
+
const apiKey = config.apiKey || "";
|
|
233
236
|
try {
|
|
234
237
|
// Ensure worker is registered and create a session
|
|
235
|
-
const workerId = await ensureWorkerRegistered(
|
|
238
|
+
const workerId = await ensureWorkerRegistered(apiUrl, apiKey);
|
|
236
239
|
const sessionId = generateSessionId();
|
|
237
|
-
await registerSession(
|
|
240
|
+
await registerSession(apiUrl, apiKey, workerId, sessionId);
|
|
238
241
|
// Create worktree for isolation (unless --no-worktree)
|
|
239
242
|
let worktreeInfo = null;
|
|
240
243
|
if (options.worktree !== false) {
|
|
@@ -515,8 +518,7 @@ taskCommand
|
|
|
515
518
|
updates.projectId = resolved.projectId;
|
|
516
519
|
}
|
|
517
520
|
if (Object.keys(updates).length === 0) {
|
|
518
|
-
|
|
519
|
-
process.exit(1);
|
|
521
|
+
errorWithHint("No update options provided. Use --help for available options.", ExplainTopic.TASK, "See all available update options");
|
|
520
522
|
}
|
|
521
523
|
// Auto-create worktree when starting a task (unless --no-worktree)
|
|
522
524
|
let worktreeInfo = null;
|
|
@@ -715,12 +717,10 @@ taskCommand
|
|
|
715
717
|
const taskId = idArg || options.id || process.env.HUSKY_TASK_ID;
|
|
716
718
|
const message = messageArg || options.message;
|
|
717
719
|
if (!taskId) {
|
|
718
|
-
|
|
719
|
-
process.exit(1);
|
|
720
|
+
errorWithHint("Task ID required. Use positional arg, --id, or set HUSKY_TASK_ID", ExplainTopic.TASK, "Learn about task ID usage");
|
|
720
721
|
}
|
|
721
722
|
if (!message) {
|
|
722
|
-
|
|
723
|
-
process.exit(1);
|
|
723
|
+
errorWithHint("Message required. Use positional arg or -m/--message", ExplainTopic.TASK, "See message command examples");
|
|
724
724
|
}
|
|
725
725
|
try {
|
|
726
726
|
const res = await fetch(`${config.apiUrl}/api/tasks/${taskId}/status`, {
|
package/dist/lib/biz/qdrant.d.ts
CHANGED
|
@@ -48,6 +48,7 @@ export declare class QdrantClient {
|
|
|
48
48
|
upsertOne(collectionName: string, id: string | number, vector: number[], payload?: Record<string, unknown>): Promise<void>;
|
|
49
49
|
getPoint(collectionName: string, id: string | number): Promise<Point | null>;
|
|
50
50
|
deletePoints(collectionName: string, ids: (string | number)[]): Promise<void>;
|
|
51
|
+
setPayload(collectionName: string, pointId: string | number, payload: Record<string, unknown>): Promise<void>;
|
|
51
52
|
count(collectionName: string): Promise<number>;
|
|
52
53
|
scroll(collectionName: string, options?: {
|
|
53
54
|
filter?: Record<string, unknown>;
|
|
@@ -57,9 +58,5 @@ export declare class QdrantClient {
|
|
|
57
58
|
id: string | number;
|
|
58
59
|
payload: Record<string, unknown>;
|
|
59
60
|
}>>;
|
|
60
|
-
/**
|
|
61
|
-
* Update payload for a specific point (for quality/visibility updates)
|
|
62
|
-
*/
|
|
63
|
-
setPayload(collectionName: string, id: string | number, payload: Record<string, unknown>): Promise<void>;
|
|
64
61
|
}
|
|
65
62
|
export default QdrantClient;
|
package/dist/lib/biz/qdrant.js
CHANGED
|
@@ -155,6 +155,15 @@ export class QdrantClient {
|
|
|
155
155
|
body: JSON.stringify({ points: ids }),
|
|
156
156
|
});
|
|
157
157
|
}
|
|
158
|
+
async setPayload(collectionName, pointId, payload) {
|
|
159
|
+
await this.request(`/collections/${collectionName}/points/payload?wait=true`, {
|
|
160
|
+
method: 'POST',
|
|
161
|
+
body: JSON.stringify({
|
|
162
|
+
points: [pointId],
|
|
163
|
+
payload,
|
|
164
|
+
}),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
158
167
|
async count(collectionName) {
|
|
159
168
|
const info = await this.getCollection(collectionName);
|
|
160
169
|
return info.pointsCount;
|
|
@@ -173,17 +182,5 @@ export class QdrantClient {
|
|
|
173
182
|
payload: p.payload || {},
|
|
174
183
|
}));
|
|
175
184
|
}
|
|
176
|
-
/**
|
|
177
|
-
* Update payload for a specific point (for quality/visibility updates)
|
|
178
|
-
*/
|
|
179
|
-
async setPayload(collectionName, id, payload) {
|
|
180
|
-
await this.request(`/collections/${collectionName}/points/payload?wait=true`, {
|
|
181
|
-
method: 'POST',
|
|
182
|
-
body: JSON.stringify({
|
|
183
|
-
points: [id],
|
|
184
|
-
payload,
|
|
185
|
-
}),
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
185
|
}
|
|
189
186
|
export default QdrantClient;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Hints System
|
|
3
|
+
*
|
|
4
|
+
* Provides helpful hints and references to `husky explain` when users encounter errors.
|
|
5
|
+
* This makes the CLI more user-friendly by guiding users to relevant documentation.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Map of error categories to their corresponding `husky explain` topics
|
|
9
|
+
*/
|
|
10
|
+
export declare enum ExplainTopic {
|
|
11
|
+
TASK = "task",
|
|
12
|
+
CONFIG = "config",
|
|
13
|
+
ROADMAP = "roadmap",
|
|
14
|
+
CHANGELOG = "changelog",
|
|
15
|
+
AGENT = "agent"
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Print an error message with an automatic hint based on content
|
|
19
|
+
*/
|
|
20
|
+
export declare function errorWithAutoHint(message: string, exitCode?: number): never;
|
|
21
|
+
/**
|
|
22
|
+
* Print an error message with a specific hint topic
|
|
23
|
+
*/
|
|
24
|
+
export declare function errorWithHint(message: string, topic: ExplainTopic, customHint?: string, exitCode?: number): never;
|
|
25
|
+
/**
|
|
26
|
+
* Print an error message with no hint (for errors where no help is available)
|
|
27
|
+
*/
|
|
28
|
+
export declare function errorWithoutHint(message: string, exitCode?: number): never;
|
|
29
|
+
/**
|
|
30
|
+
* Predefined error helpers for common scenarios
|
|
31
|
+
*/
|
|
32
|
+
export declare const ErrorHelpers: {
|
|
33
|
+
/**
|
|
34
|
+
* Error: Task ID not provided
|
|
35
|
+
*/
|
|
36
|
+
missingTaskId: () => never;
|
|
37
|
+
/**
|
|
38
|
+
* Error: API URL not configured
|
|
39
|
+
*/
|
|
40
|
+
missingApiUrl: () => never;
|
|
41
|
+
/**
|
|
42
|
+
* Error: API key not configured
|
|
43
|
+
*/
|
|
44
|
+
missingApiKey: () => never;
|
|
45
|
+
/**
|
|
46
|
+
* Error: Both API URL and key not configured
|
|
47
|
+
*/
|
|
48
|
+
missingConfig: () => never;
|
|
49
|
+
/**
|
|
50
|
+
* Error: Permission denied
|
|
51
|
+
*/
|
|
52
|
+
permissionDenied: (operation: string) => never;
|
|
53
|
+
/**
|
|
54
|
+
* Error: Invalid task status
|
|
55
|
+
*/
|
|
56
|
+
invalidTaskStatus: (status: string) => never;
|
|
57
|
+
/**
|
|
58
|
+
* Error: Task operation failed
|
|
59
|
+
*/
|
|
60
|
+
taskOperationFailed: (operation: string, reason: string) => never;
|
|
61
|
+
/**
|
|
62
|
+
* Error: API request failed
|
|
63
|
+
*/
|
|
64
|
+
apiRequestFailed: (status: number, message: string) => never;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Format a warning message with a hint (non-fatal)
|
|
68
|
+
*/
|
|
69
|
+
export declare function warningWithHint(message: string, topic: ExplainTopic, customHint?: string): void;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Hints System
|
|
3
|
+
*
|
|
4
|
+
* Provides helpful hints and references to `husky explain` when users encounter errors.
|
|
5
|
+
* This makes the CLI more user-friendly by guiding users to relevant documentation.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Map of error categories to their corresponding `husky explain` topics
|
|
9
|
+
*/
|
|
10
|
+
export var ExplainTopic;
|
|
11
|
+
(function (ExplainTopic) {
|
|
12
|
+
ExplainTopic["TASK"] = "task";
|
|
13
|
+
ExplainTopic["CONFIG"] = "config";
|
|
14
|
+
ExplainTopic["ROADMAP"] = "roadmap";
|
|
15
|
+
ExplainTopic["CHANGELOG"] = "changelog";
|
|
16
|
+
ExplainTopic["AGENT"] = "agent";
|
|
17
|
+
})(ExplainTopic || (ExplainTopic = {}));
|
|
18
|
+
const ERROR_PATTERNS = [
|
|
19
|
+
{
|
|
20
|
+
keywords: ["task id", "HUSKY_TASK_ID", "task status", "task complete", "task start"],
|
|
21
|
+
topic: ExplainTopic.TASK,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
keywords: ["api url", "api key", "not configured", "config set", "authentication"],
|
|
25
|
+
topic: ExplainTopic.CONFIG,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
keywords: ["roadmap", "phase", "feature"],
|
|
29
|
+
topic: ExplainTopic.ROADMAP,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
keywords: ["changelog", "version", "commits"],
|
|
33
|
+
topic: ExplainTopic.CHANGELOG,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
keywords: ["workflow", "agent", "session"],
|
|
37
|
+
topic: ExplainTopic.AGENT,
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
/**
|
|
41
|
+
* Detect which explain topic is most relevant based on error message
|
|
42
|
+
*/
|
|
43
|
+
function detectExplainTopic(message) {
|
|
44
|
+
const lowerMessage = message.toLowerCase();
|
|
45
|
+
for (const pattern of ERROR_PATTERNS) {
|
|
46
|
+
if (pattern.keywords.some(keyword => lowerMessage.includes(keyword))) {
|
|
47
|
+
return pattern.topic;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Format a hint message for a specific explain topic
|
|
54
|
+
*/
|
|
55
|
+
function formatHint(topic, customHint) {
|
|
56
|
+
if (customHint) {
|
|
57
|
+
return `\nš” Hint: ${customHint}\n Run: husky explain ${topic}`;
|
|
58
|
+
}
|
|
59
|
+
const hints = {
|
|
60
|
+
[ExplainTopic.TASK]: "For task workflow help",
|
|
61
|
+
[ExplainTopic.CONFIG]: "For configuration help",
|
|
62
|
+
[ExplainTopic.ROADMAP]: "For roadmap commands help",
|
|
63
|
+
[ExplainTopic.CHANGELOG]: "For changelog commands help",
|
|
64
|
+
[ExplainTopic.AGENT]: "For agent workflow examples",
|
|
65
|
+
};
|
|
66
|
+
return `\nš” ${hints[topic]}: husky explain ${topic}`;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Print an error message with an automatic hint based on content
|
|
70
|
+
*/
|
|
71
|
+
export function errorWithAutoHint(message, exitCode = 1) {
|
|
72
|
+
console.error(`Error: ${message}`);
|
|
73
|
+
const topic = detectExplainTopic(message);
|
|
74
|
+
if (topic) {
|
|
75
|
+
console.error(formatHint(topic));
|
|
76
|
+
}
|
|
77
|
+
process.exit(exitCode);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Print an error message with a specific hint topic
|
|
81
|
+
*/
|
|
82
|
+
export function errorWithHint(message, topic, customHint, exitCode = 1) {
|
|
83
|
+
console.error(`Error: ${message}`);
|
|
84
|
+
console.error(formatHint(topic, customHint));
|
|
85
|
+
process.exit(exitCode);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Print an error message with no hint (for errors where no help is available)
|
|
89
|
+
*/
|
|
90
|
+
export function errorWithoutHint(message, exitCode = 1) {
|
|
91
|
+
console.error(`Error: ${message}`);
|
|
92
|
+
process.exit(exitCode);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Predefined error helpers for common scenarios
|
|
96
|
+
*/
|
|
97
|
+
export const ErrorHelpers = {
|
|
98
|
+
/**
|
|
99
|
+
* Error: Task ID not provided
|
|
100
|
+
*/
|
|
101
|
+
missingTaskId: () => {
|
|
102
|
+
errorWithHint("Task ID required. Use --id or set HUSKY_TASK_ID environment variable.", ExplainTopic.TASK, "Learn about task ID usage and environment variables");
|
|
103
|
+
},
|
|
104
|
+
/**
|
|
105
|
+
* Error: API URL not configured
|
|
106
|
+
*/
|
|
107
|
+
missingApiUrl: () => {
|
|
108
|
+
errorWithHint("API URL not configured. Run: husky config set api-url <url>", ExplainTopic.CONFIG, "Learn how to configure the CLI");
|
|
109
|
+
},
|
|
110
|
+
/**
|
|
111
|
+
* Error: API key not configured
|
|
112
|
+
*/
|
|
113
|
+
missingApiKey: () => {
|
|
114
|
+
errorWithHint("API key not configured. Run: husky config set api-key <key>", ExplainTopic.CONFIG, "Learn how to configure authentication");
|
|
115
|
+
},
|
|
116
|
+
/**
|
|
117
|
+
* Error: Both API URL and key not configured
|
|
118
|
+
*/
|
|
119
|
+
missingConfig: () => {
|
|
120
|
+
errorWithHint("API URL and key required. Run: husky config test", ExplainTopic.CONFIG, "Learn about CLI configuration");
|
|
121
|
+
},
|
|
122
|
+
/**
|
|
123
|
+
* Error: Permission denied
|
|
124
|
+
*/
|
|
125
|
+
permissionDenied: (operation) => {
|
|
126
|
+
errorWithAutoHint(`Permission denied: ${operation}`);
|
|
127
|
+
},
|
|
128
|
+
/**
|
|
129
|
+
* Error: Invalid task status
|
|
130
|
+
*/
|
|
131
|
+
invalidTaskStatus: (status) => {
|
|
132
|
+
errorWithHint(`Invalid status: ${status}. Valid statuses: backlog, in_progress, review, done`, ExplainTopic.TASK, "See all available task statuses");
|
|
133
|
+
},
|
|
134
|
+
/**
|
|
135
|
+
* Error: Task operation failed
|
|
136
|
+
*/
|
|
137
|
+
taskOperationFailed: (operation, reason) => {
|
|
138
|
+
errorWithHint(`Failed to ${operation}: ${reason}`, ExplainTopic.TASK, "Learn about task management");
|
|
139
|
+
},
|
|
140
|
+
/**
|
|
141
|
+
* Error: API request failed
|
|
142
|
+
*/
|
|
143
|
+
apiRequestFailed: (status, message) => {
|
|
144
|
+
if (status === 401) {
|
|
145
|
+
errorWithHint("Authentication failed. Check your API key.", ExplainTopic.CONFIG, "Learn how to configure authentication");
|
|
146
|
+
}
|
|
147
|
+
else if (status === 403) {
|
|
148
|
+
errorWithAutoHint(`Permission denied: ${message}`);
|
|
149
|
+
}
|
|
150
|
+
else if (status === 404) {
|
|
151
|
+
errorWithoutHint(`Resource not found: ${message}`);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
errorWithAutoHint(`API error (${status}): ${message}`);
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
/**
|
|
159
|
+
* Format a warning message with a hint (non-fatal)
|
|
160
|
+
*/
|
|
161
|
+
export function warningWithHint(message, topic, customHint) {
|
|
162
|
+
console.warn(`ā Warning: ${message}`);
|
|
163
|
+
console.warn(formatHint(topic, customHint).replace("š”", "ā¹ļø"));
|
|
164
|
+
}
|
package/dist/lib/permissions.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Permissions are fetched from the API and cached locally.
|
|
6
6
|
*/
|
|
7
7
|
import { getConfig, hasPermission, getRole, fetchAndCacheRole, clearRoleCache } from "../commands/config.js";
|
|
8
|
+
import { ExplainTopic } from "./error-hints.js";
|
|
8
9
|
/**
|
|
9
10
|
* Check if current user has a specific permission.
|
|
10
11
|
* Uses cached permissions from config.
|
|
@@ -27,6 +28,7 @@ export function requirePermission(permission) {
|
|
|
27
28
|
else {
|
|
28
29
|
console.error("Run 'husky config test' to refresh your role and permissions.");
|
|
29
30
|
}
|
|
31
|
+
console.error(`\nš” For configuration help: husky explain ${ExplainTopic.CONFIG}`);
|
|
30
32
|
process.exit(1);
|
|
31
33
|
}
|
|
32
34
|
}
|
|
@@ -43,6 +45,7 @@ export function requireAnyPermission(permissions) {
|
|
|
43
45
|
if (config.role) {
|
|
44
46
|
console.error(`Your role (${config.role}) does not have these permissions.`);
|
|
45
47
|
}
|
|
48
|
+
console.error(`\nš” For configuration help: husky explain ${ExplainTopic.CONFIG}`);
|
|
46
49
|
process.exit(1);
|
|
47
50
|
}
|
|
48
51
|
}
|