@simonfestl/husky-cli 1.0.0 → 1.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 +37 -0
- package/dist/commands/agent-msg.d.ts +2 -0
- package/dist/commands/agent-msg.js +252 -0
- package/dist/commands/agent.js +270 -0
- package/dist/commands/biz/gotess.d.ts +3 -0
- package/dist/commands/biz/gotess.js +320 -0
- package/dist/commands/biz.js +5 -1
- package/dist/commands/chat.js +507 -0
- package/dist/commands/completion.js +2 -2
- package/dist/commands/config.d.ts +27 -0
- package/dist/commands/config.js +117 -28
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +91 -0
- package/dist/commands/interactive/vm-sessions.js +0 -1
- package/dist/commands/llm-context.js +27 -0
- package/dist/commands/preview.d.ts +2 -0
- package/dist/commands/preview.js +161 -0
- package/dist/commands/task.js +3 -0
- package/dist/commands/vm.js +7 -2
- package/dist/index.js +6 -0
- package/dist/lib/biz/gotess.d.ts +97 -0
- package/dist/lib/biz/gotess.js +202 -0
- package/dist/lib/permissions.d.ts +78 -0
- package/dist/lib/permissions.js +139 -0
- package/package.json +1 -1
package/dist/commands/config.js
CHANGED
|
@@ -46,12 +46,86 @@ function saveConfig(config) {
|
|
|
46
46
|
}
|
|
47
47
|
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
48
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Fetch role and permissions from /api/auth/whoami
|
|
51
|
+
* Caches the result in config for 1 hour
|
|
52
|
+
*/
|
|
53
|
+
export async function fetchAndCacheRole() {
|
|
54
|
+
const config = getConfig();
|
|
55
|
+
// Check if we have cached role that's less than 1 hour old
|
|
56
|
+
if (config.role && config.roleLastChecked) {
|
|
57
|
+
const lastChecked = new Date(config.roleLastChecked);
|
|
58
|
+
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
|
|
59
|
+
if (lastChecked > oneHourAgo) {
|
|
60
|
+
return { role: config.role, permissions: config.permissions };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Fetch fresh role/permissions
|
|
64
|
+
if (!config.apiUrl || !config.apiKey) {
|
|
65
|
+
return {};
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const url = new URL("/api/auth/whoami", config.apiUrl);
|
|
69
|
+
const res = await fetch(url.toString(), {
|
|
70
|
+
headers: { "x-api-key": config.apiKey },
|
|
71
|
+
});
|
|
72
|
+
if (res.ok) {
|
|
73
|
+
const data = await res.json();
|
|
74
|
+
// Update config cache
|
|
75
|
+
config.role = data.role;
|
|
76
|
+
config.permissions = data.permissions;
|
|
77
|
+
config.roleLastChecked = new Date().toISOString();
|
|
78
|
+
saveConfig(config);
|
|
79
|
+
return { role: data.role, permissions: data.permissions };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Ignore fetch errors, return cached or empty
|
|
84
|
+
}
|
|
85
|
+
return { role: config.role, permissions: config.permissions };
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Check if current config has a specific permission
|
|
89
|
+
*/
|
|
90
|
+
export function hasPermission(permission) {
|
|
91
|
+
const config = getConfig();
|
|
92
|
+
if (!config.permissions)
|
|
93
|
+
return false;
|
|
94
|
+
// Direct match
|
|
95
|
+
if (config.permissions.includes(permission))
|
|
96
|
+
return true;
|
|
97
|
+
// Wildcard match (e.g., "task:*" matches "task:read")
|
|
98
|
+
const [resource] = permission.split(":");
|
|
99
|
+
if (config.permissions.includes(`${resource}:*`))
|
|
100
|
+
return true;
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Get current role from config (may be undefined if not fetched)
|
|
105
|
+
*/
|
|
106
|
+
export function getRole() {
|
|
107
|
+
return getConfig().role;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Clear the role cache to force a refresh on next fetchAndCacheRole call
|
|
111
|
+
*/
|
|
112
|
+
export function clearRoleCache() {
|
|
113
|
+
const config = getConfig();
|
|
114
|
+
delete config.roleLastChecked;
|
|
115
|
+
saveConfig(config);
|
|
116
|
+
}
|
|
49
117
|
// Helper to set a single config value (used by interactive mode and worker identity)
|
|
50
118
|
export function setConfig(key, value) {
|
|
51
119
|
const config = getConfig();
|
|
52
120
|
config[key] = value;
|
|
53
121
|
saveConfig(config);
|
|
54
122
|
}
|
|
123
|
+
export function setGotessConfig(token, bookId) {
|
|
124
|
+
const config = getConfig();
|
|
125
|
+
config.gotessToken = token;
|
|
126
|
+
config.gotessBookId = bookId;
|
|
127
|
+
saveConfig(config);
|
|
128
|
+
}
|
|
55
129
|
export const configCommand = new Command("config")
|
|
56
130
|
.description("Manage CLI configuration");
|
|
57
131
|
// husky config set <key> <value>
|
|
@@ -82,6 +156,8 @@ configCommand
|
|
|
82
156
|
// GCP
|
|
83
157
|
"gcp-project-id": "gcpProjectId",
|
|
84
158
|
"gcp-location": "gcpLocation",
|
|
159
|
+
"gotess-token": "gotessToken",
|
|
160
|
+
"gotess-book-id": "gotessBookId",
|
|
85
161
|
};
|
|
86
162
|
const configKey = keyMappings[key];
|
|
87
163
|
if (!configKey) {
|
|
@@ -93,6 +169,7 @@ configCommand
|
|
|
93
169
|
console.log(" SeaTable: seatable-api-token, seatable-server-url");
|
|
94
170
|
console.log(" Qdrant: qdrant-url, qdrant-api-key");
|
|
95
171
|
console.log(" GCP: gcp-project-id, gcp-location");
|
|
172
|
+
console.log(" Gotess: gotess-token, gotess-book-id");
|
|
96
173
|
process.exit(1);
|
|
97
174
|
}
|
|
98
175
|
// Validation for specific keys
|
|
@@ -107,7 +184,7 @@ configCommand
|
|
|
107
184
|
config[configKey] = value;
|
|
108
185
|
saveConfig(config);
|
|
109
186
|
// Mask sensitive values in output
|
|
110
|
-
const sensitiveKeys = ["api-key", "billbee-api-key", "billbee-password", "zendesk-api-token", "seatable-api-token"];
|
|
187
|
+
const sensitiveKeys = ["api-key", "billbee-api-key", "billbee-password", "zendesk-api-token", "seatable-api-token", "gotess-token"];
|
|
111
188
|
const displayValue = sensitiveKeys.includes(key) ? "***" : value;
|
|
112
189
|
console.log(`✓ Set ${key} = ${displayValue}`);
|
|
113
190
|
});
|
|
@@ -155,37 +232,49 @@ configCommand
|
|
|
155
232
|
}
|
|
156
233
|
console.log("Testing API connection...");
|
|
157
234
|
try {
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
},
|
|
235
|
+
// First test basic connectivity with /api/tasks
|
|
236
|
+
const tasksUrl = new URL("/api/tasks", config.apiUrl);
|
|
237
|
+
const tasksRes = await fetch(tasksUrl.toString(), {
|
|
238
|
+
headers: { "x-api-key": config.apiKey },
|
|
163
239
|
});
|
|
164
|
-
if (
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
240
|
+
if (!tasksRes.ok) {
|
|
241
|
+
if (tasksRes.status === 401) {
|
|
242
|
+
console.error(`API connection failed: Unauthorized (HTTP 401)`);
|
|
243
|
+
console.error(" Check your API key with: husky config set api-key <key>");
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
246
|
+
else if (tasksRes.status === 403) {
|
|
247
|
+
console.error(`API connection failed: Forbidden (HTTP 403)`);
|
|
248
|
+
console.error(" Your API key may not have the required permissions");
|
|
249
|
+
process.exit(1);
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
console.error(`API connection failed: HTTP ${tasksRes.status}`);
|
|
253
|
+
process.exit(1);
|
|
254
|
+
}
|
|
176
255
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
256
|
+
console.log(`API connection successful (API URL: ${config.apiUrl})`);
|
|
257
|
+
// Now fetch role/permissions from whoami
|
|
258
|
+
const whoamiUrl = new URL("/api/auth/whoami", config.apiUrl);
|
|
259
|
+
const whoamiRes = await fetch(whoamiUrl.toString(), {
|
|
260
|
+
headers: { "x-api-key": config.apiKey },
|
|
261
|
+
});
|
|
262
|
+
if (whoamiRes.ok) {
|
|
263
|
+
const data = await whoamiRes.json();
|
|
264
|
+
// Cache the role/permissions
|
|
265
|
+
const updatedConfig = getConfig();
|
|
266
|
+
updatedConfig.role = data.role;
|
|
267
|
+
updatedConfig.permissions = data.permissions;
|
|
268
|
+
updatedConfig.roleLastChecked = new Date().toISOString();
|
|
269
|
+
saveConfig(updatedConfig);
|
|
270
|
+
console.log(`\nRBAC Info:`);
|
|
271
|
+
console.log(` Role: ${data.role || "(not assigned)"}`);
|
|
272
|
+
if (data.permissions && data.permissions.length > 0) {
|
|
273
|
+
console.log(` Permissions: ${data.permissions.join(", ")}`);
|
|
184
274
|
}
|
|
185
|
-
|
|
186
|
-
|
|
275
|
+
if (data.agentId) {
|
|
276
|
+
console.log(` Agent ID: ${data.agentId}`);
|
|
187
277
|
}
|
|
188
|
-
process.exit(1);
|
|
189
278
|
}
|
|
190
279
|
}
|
|
191
280
|
catch (error) {
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { existsSync, writeFileSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { createRequire } from "module";
|
|
5
|
+
import { generateLLMContext } from "./llm-context.js";
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
const packageJson = require("../../package.json");
|
|
8
|
+
const HUSKY_MD_FILENAME = "HUSKY.md";
|
|
9
|
+
function generateHuskyMdContent() {
|
|
10
|
+
const timestamp = new Date().toISOString();
|
|
11
|
+
const cliRef = generateLLMContext();
|
|
12
|
+
return `<!--
|
|
13
|
+
Auto-generated by husky init (v${packageJson.version})
|
|
14
|
+
Generated: ${timestamp}
|
|
15
|
+
|
|
16
|
+
This file instructs AI coding agents to use the Husky CLI.
|
|
17
|
+
Update with: husky init --force
|
|
18
|
+
-->
|
|
19
|
+
|
|
20
|
+
${cliRef}
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Standard Workflow for AI Agents
|
|
25
|
+
|
|
26
|
+
### On Session Start
|
|
27
|
+
\`\`\`bash
|
|
28
|
+
husky config test # Verify API connection
|
|
29
|
+
husky worker whoami # Confirm worker identity
|
|
30
|
+
\`\`\`
|
|
31
|
+
|
|
32
|
+
### When Working on a Task
|
|
33
|
+
\`\`\`bash
|
|
34
|
+
# 1. Get task details
|
|
35
|
+
husky task get <id>
|
|
36
|
+
|
|
37
|
+
# 2. Start the task (creates isolated worktree)
|
|
38
|
+
husky task start <id>
|
|
39
|
+
|
|
40
|
+
# 3. CD into the worktree directory (MANDATORY)
|
|
41
|
+
cd <worktree-path> # Path shown in task start output
|
|
42
|
+
|
|
43
|
+
# 4. Report progress as you work
|
|
44
|
+
husky task message <id> "Analyzing codebase..."
|
|
45
|
+
husky task message <id> "Implementing feature X..."
|
|
46
|
+
|
|
47
|
+
# 5. When done, create PR and complete
|
|
48
|
+
husky worktree pr <worktree-name> -t "feat: description"
|
|
49
|
+
husky task done <id> --pr <pr-url>
|
|
50
|
+
\`\`\`
|
|
51
|
+
|
|
52
|
+
**IMPORTANT:** After \`husky task start\`, you MUST \`cd\` into the worktree directory before making any code changes.
|
|
53
|
+
|
|
54
|
+
### When Handling Customer Support
|
|
55
|
+
\`\`\`bash
|
|
56
|
+
# 1. Get full customer context
|
|
57
|
+
husky biz customers 360 <email>
|
|
58
|
+
|
|
59
|
+
# 2. Check relevant tickets
|
|
60
|
+
husky biz tickets search "<customer-email>"
|
|
61
|
+
|
|
62
|
+
# 3. Check order history if needed
|
|
63
|
+
husky biz orders search "<order-id-or-email>"
|
|
64
|
+
|
|
65
|
+
# 4. Reply with context
|
|
66
|
+
husky biz tickets reply <ticket-id> "Your response..."
|
|
67
|
+
\`\`\`
|
|
68
|
+
`;
|
|
69
|
+
}
|
|
70
|
+
export const initCommand = new Command("init")
|
|
71
|
+
.description("Initialize Husky in the current directory (creates HUSKY.md)")
|
|
72
|
+
.option("-f, --force", "Overwrite existing HUSKY.md")
|
|
73
|
+
.option("-q, --quiet", "Suppress output")
|
|
74
|
+
.action((options) => {
|
|
75
|
+
const targetPath = join(process.cwd(), HUSKY_MD_FILENAME);
|
|
76
|
+
if (existsSync(targetPath) && !options.force) {
|
|
77
|
+
console.error(`Error: ${HUSKY_MD_FILENAME} already exists.`);
|
|
78
|
+
console.error("Use --force to overwrite.");
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
const content = generateHuskyMdContent();
|
|
82
|
+
writeFileSync(targetPath, content);
|
|
83
|
+
if (!options.quiet) {
|
|
84
|
+
console.log(`✓ Created ${HUSKY_MD_FILENAME}`);
|
|
85
|
+
console.log("");
|
|
86
|
+
console.log(" This file instructs AI agents to use the Husky CLI.");
|
|
87
|
+
console.log(" Commit it to your repository so all agents see it.");
|
|
88
|
+
console.log("");
|
|
89
|
+
console.log(" Update with: husky init --force");
|
|
90
|
+
}
|
|
91
|
+
});
|
|
@@ -140,7 +140,6 @@ async function createVMSession(config) {
|
|
|
140
140
|
choices: [
|
|
141
141
|
{ name: "Claude Code", value: "claude-code" },
|
|
142
142
|
{ name: "Gemini CLI", value: "gemini-cli" },
|
|
143
|
-
{ name: "Aider", value: "aider" },
|
|
144
143
|
{ name: "Custom", value: "custom" },
|
|
145
144
|
],
|
|
146
145
|
default: "claude-code",
|
|
@@ -11,6 +11,7 @@ export function generateLLMContext() {
|
|
|
11
11
|
>
|
|
12
12
|
> **DO NOT:**
|
|
13
13
|
> - Make direct API calls to Billbee, Zendesk, or other services
|
|
14
|
+
> - Make direct API calls to husky-api (use CLI instead)
|
|
14
15
|
> - Bypass Husky CLI for task management
|
|
15
16
|
> - Create custom integrations when Husky commands exist
|
|
16
17
|
>
|
|
@@ -18,6 +19,7 @@ export function generateLLMContext() {
|
|
|
18
19
|
> - Use \`husky biz\` commands for business operations
|
|
19
20
|
> - Use \`husky task\` commands for task lifecycle
|
|
20
21
|
> - Use \`husky worktree\` for Git isolation
|
|
22
|
+
> - Use \`husky chat\` commands for Google Chat communication
|
|
21
23
|
> - Check \`husky config test\` before operations
|
|
22
24
|
|
|
23
25
|
---
|
|
@@ -153,6 +155,31 @@ husky worker sessions # List active sessions
|
|
|
153
155
|
husky worker activity # Who is working on what
|
|
154
156
|
\`\`\`
|
|
155
157
|
|
|
158
|
+
### Chat (Google Chat Integration)
|
|
159
|
+
\`\`\`bash
|
|
160
|
+
husky chat reply-chat --space <space-id> "<message>" # Send message to Google Chat
|
|
161
|
+
husky chat inbox # Get messages from Google Chat
|
|
162
|
+
husky chat inbox --unread # Only unread messages
|
|
163
|
+
husky chat pending # Get pending messages from user
|
|
164
|
+
husky chat send "<message>" # Send message as supervisor
|
|
165
|
+
husky chat reply <messageId> "<response>" # Reply to specific message
|
|
166
|
+
husky chat review "<question>" # Request human review via Google Chat
|
|
167
|
+
husky chat review-status <reviewId> # Check review status
|
|
168
|
+
husky chat watch # Watch for new messages (blocking)
|
|
169
|
+
\`\`\`
|
|
170
|
+
|
|
171
|
+
### Agent Messaging (agent-to-agent communication)
|
|
172
|
+
\`\`\`bash
|
|
173
|
+
husky agent-msg send --type <type> --title "<title>" # Send message (types: approval_request, status_update, error_report, completion, query)
|
|
174
|
+
husky agent-msg list # List messages
|
|
175
|
+
husky agent-msg list --status pending # Filter by status
|
|
176
|
+
husky agent-msg pending # List pending messages (for supervisor)
|
|
177
|
+
husky agent-msg respond <id> --approve # Approve request
|
|
178
|
+
husky agent-msg respond <id> --reject # Reject request
|
|
179
|
+
husky agent-msg get <id> # Get message details
|
|
180
|
+
husky agent-msg wait <id> # Wait for response (blocking)
|
|
181
|
+
\`\`\`
|
|
182
|
+
|
|
156
183
|
### Utility Commands
|
|
157
184
|
\`\`\`bash
|
|
158
185
|
husky explain <command> # Explain CLI commands
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { getConfig } from "./config.js";
|
|
3
|
+
const PREVIEW_DEPLOY_TRIGGER_ID = "80b3ba55-ae74-41cd-b7d0-a477ecc357b1";
|
|
4
|
+
const PREVIEW_CLEANUP_TRIGGER_ID = "965c3e86-677f-4063-b391-43019f621ea2";
|
|
5
|
+
const GCP_PROJECT = process.env.GCP_PROJECT_ID || process.env.GOOGLE_CLOUD_PROJECT || "tigerv0";
|
|
6
|
+
async function apiRequest(method, path, body) {
|
|
7
|
+
const config = getConfig();
|
|
8
|
+
const url = `${config.apiUrl}${path}`;
|
|
9
|
+
const response = await fetch(url, {
|
|
10
|
+
method,
|
|
11
|
+
headers: {
|
|
12
|
+
"Content-Type": "application/json",
|
|
13
|
+
"x-api-key": config.apiKey || "",
|
|
14
|
+
},
|
|
15
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
16
|
+
});
|
|
17
|
+
return response;
|
|
18
|
+
}
|
|
19
|
+
async function triggerCloudBuild(triggerId, substitutions) {
|
|
20
|
+
const { execSync } = await import("child_process");
|
|
21
|
+
const subsArgs = Object.entries(substitutions)
|
|
22
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
23
|
+
.join(",");
|
|
24
|
+
try {
|
|
25
|
+
const cmd = `gcloud builds triggers run ${triggerId} --project=${GCP_PROJECT} --branch=main --substitutions=${subsArgs} --format="value(metadata.build.id)" 2>&1`;
|
|
26
|
+
const output = execSync(cmd, { encoding: "utf-8" }).trim();
|
|
27
|
+
const buildId = output.split("\n").pop() || "";
|
|
28
|
+
return { success: true, buildId };
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
32
|
+
return { success: false, error: message };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export const previewCommand = new Command("preview")
|
|
36
|
+
.description("Manage PR preview deployments");
|
|
37
|
+
previewCommand
|
|
38
|
+
.command("list")
|
|
39
|
+
.description("List active preview deployments")
|
|
40
|
+
.option("--json", "Output as JSON")
|
|
41
|
+
.action(async (options) => {
|
|
42
|
+
try {
|
|
43
|
+
const response = await apiRequest("GET", "/api/previews");
|
|
44
|
+
if (!response.ok) {
|
|
45
|
+
console.error(`Error: ${response.status} ${response.statusText}`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
const previews = await response.json();
|
|
49
|
+
if (options.json) {
|
|
50
|
+
console.log(JSON.stringify(previews, null, 2));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (!previews.length) {
|
|
54
|
+
console.log("No active previews");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
console.log("\nActive Previews:\n");
|
|
58
|
+
for (const p of previews) {
|
|
59
|
+
console.log(` PR #${p.prNumber}${p.prTitle ? `: ${p.prTitle}` : ""}`);
|
|
60
|
+
console.log(` Status: ${p.status}`);
|
|
61
|
+
console.log(` Dashboard: ${p.dashboardUrl}`);
|
|
62
|
+
console.log(` Terminal: ${p.terminalUrl}`);
|
|
63
|
+
console.log(` Commit: ${p.commitSha.slice(0, 7)}`);
|
|
64
|
+
console.log("");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
console.error("Failed to fetch previews:", error);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
previewCommand
|
|
73
|
+
.command("deploy <pr-number>")
|
|
74
|
+
.description("Deploy a preview for a PR")
|
|
75
|
+
.option("--branch <branch>", "Branch to deploy (default: from PR)")
|
|
76
|
+
.action(async (prNumber, options) => {
|
|
77
|
+
const prNum = parseInt(prNumber, 10);
|
|
78
|
+
if (isNaN(prNum) || prNum <= 0) {
|
|
79
|
+
console.error("Error: PR number must be a positive integer");
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
console.log(`Triggering preview deployment for PR #${prNum}...`);
|
|
83
|
+
const result = await triggerCloudBuild(PREVIEW_DEPLOY_TRIGGER_ID, {
|
|
84
|
+
_PR_NUMBER: String(prNum),
|
|
85
|
+
});
|
|
86
|
+
if (result.success) {
|
|
87
|
+
console.log(`Build started: ${result.buildId}`);
|
|
88
|
+
console.log(`\nMonitor at: https://console.cloud.google.com/cloud-build/builds/${result.buildId}?project=${GCP_PROJECT}`);
|
|
89
|
+
console.log("\nPreview URLs will be available once build completes (~5-10 min)");
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
console.error(`Failed to trigger build: ${result.error}`);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
previewCommand
|
|
97
|
+
.command("cleanup [pr-number]")
|
|
98
|
+
.description("Cleanup preview deployments (specific PR or all merged)")
|
|
99
|
+
.action(async (prNumber) => {
|
|
100
|
+
const substitutions = {};
|
|
101
|
+
if (prNumber) {
|
|
102
|
+
const prNum = parseInt(prNumber, 10);
|
|
103
|
+
if (isNaN(prNum) || prNum <= 0) {
|
|
104
|
+
console.error("Error: PR number must be a positive integer");
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
substitutions._PR_NUMBER = String(prNum);
|
|
108
|
+
console.log(`Cleaning up preview for PR #${prNum}...`);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
console.log("Cleaning up all merged/closed PR previews...");
|
|
112
|
+
}
|
|
113
|
+
const result = await triggerCloudBuild(PREVIEW_CLEANUP_TRIGGER_ID, substitutions);
|
|
114
|
+
if (result.success) {
|
|
115
|
+
console.log(`Cleanup started: ${result.buildId}`);
|
|
116
|
+
console.log(`\nMonitor at: https://console.cloud.google.com/cloud-build/builds/${result.buildId}?project=${GCP_PROJECT}`);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
console.error(`Failed to trigger cleanup: ${result.error}`);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
previewCommand
|
|
124
|
+
.command("status <pr-number>")
|
|
125
|
+
.description("Get status of a specific preview")
|
|
126
|
+
.option("--json", "Output as JSON")
|
|
127
|
+
.action(async (prNumber, options) => {
|
|
128
|
+
const prNum = parseInt(prNumber, 10);
|
|
129
|
+
if (isNaN(prNum) || prNum <= 0) {
|
|
130
|
+
console.error("Error: PR number must be a positive integer");
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
const response = await apiRequest("GET", `/api/previews/${prNum}`);
|
|
135
|
+
if (response.status === 404) {
|
|
136
|
+
console.log(`No preview found for PR #${prNum}`);
|
|
137
|
+
process.exit(0);
|
|
138
|
+
}
|
|
139
|
+
if (!response.ok) {
|
|
140
|
+
console.error(`Error: ${response.status} ${response.statusText}`);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
const preview = await response.json();
|
|
144
|
+
if (options.json) {
|
|
145
|
+
console.log(JSON.stringify(preview, null, 2));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
console.log(`\nPreview for PR #${preview.prNumber}`);
|
|
149
|
+
if (preview.prTitle)
|
|
150
|
+
console.log(` Title: ${preview.prTitle}`);
|
|
151
|
+
console.log(` Status: ${preview.status}`);
|
|
152
|
+
console.log(` Dashboard: ${preview.dashboardUrl}`);
|
|
153
|
+
console.log(` Terminal: ${preview.terminalUrl}`);
|
|
154
|
+
console.log(` Commit: ${preview.commitSha}`);
|
|
155
|
+
console.log(` Created: ${new Date(preview.createdAt).toLocaleString()}`);
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
console.error("Failed to fetch preview:", error);
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
});
|
package/dist/commands/task.js
CHANGED
|
@@ -7,6 +7,7 @@ import { ensureWorkerRegistered, generateSessionId, registerSession } from "../l
|
|
|
7
7
|
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
|
+
import { requirePermission } from "../lib/permissions.js";
|
|
10
11
|
export const taskCommand = new Command("task")
|
|
11
12
|
.description("Manage tasks");
|
|
12
13
|
// Helper: Get task ID from --id flag or HUSKY_TASK_ID env var
|
|
@@ -284,6 +285,8 @@ taskCommand
|
|
|
284
285
|
.option("--pr <url>", "Link to PR")
|
|
285
286
|
.option("--skip-qa", "Skip QA review and mark as done directly")
|
|
286
287
|
.action(async (id, options) => {
|
|
288
|
+
// RBAC: Only supervisor and pr_agent can set tasks to done
|
|
289
|
+
requirePermission("task:done");
|
|
287
290
|
const config = getConfig();
|
|
288
291
|
if (!config.apiUrl) {
|
|
289
292
|
console.error("Error: API URL not configured.");
|
package/dist/commands/vm.js
CHANGED
|
@@ -4,6 +4,7 @@ import * as readline from "readline";
|
|
|
4
4
|
import * as fs from "fs";
|
|
5
5
|
import * as path from "path";
|
|
6
6
|
import { DEFAULT_AGENT_CONFIGS, generateStartupScript, listDefaultAgentTypes, getDefaultAgentConfig, } from "../lib/agent-templates.js";
|
|
7
|
+
import { requirePermission } from "../lib/permissions.js";
|
|
7
8
|
export const vmCommand = new Command("vm").description("Manage VM sessions");
|
|
8
9
|
// Helper: Ensure API is configured
|
|
9
10
|
function ensureConfig() {
|
|
@@ -80,7 +81,7 @@ vmCommand
|
|
|
80
81
|
.description("List all VM sessions")
|
|
81
82
|
.option("--json", "Output as JSON")
|
|
82
83
|
.option("--status <status>", "Filter by status (pending, starting, running, completed, failed, terminated)")
|
|
83
|
-
.option("--agent <agent>", "Filter by agent type (claude-code, gemini-cli,
|
|
84
|
+
.option("--agent <agent>", "Filter by agent type (claude-code, gemini-cli, custom)")
|
|
84
85
|
.action(async (options) => {
|
|
85
86
|
const config = ensureConfig();
|
|
86
87
|
try {
|
|
@@ -117,7 +118,7 @@ vmCommand
|
|
|
117
118
|
.command("create <name>")
|
|
118
119
|
.description("Create a new VM session")
|
|
119
120
|
.option("-p, --prompt <prompt>", "Initial prompt for the agent")
|
|
120
|
-
.option("--agent <agent>", "Agent type (claude-code, gemini-cli,
|
|
121
|
+
.option("--agent <agent>", "Agent type (claude-code, gemini-cli, custom)", "gemini-cli")
|
|
121
122
|
.option("-t, --type <type>", "Business agent type (support, accounting, marketing, research)")
|
|
122
123
|
.option("--config <configId>", "VM config to use")
|
|
123
124
|
.option("--project <projectId>", "Link to project")
|
|
@@ -128,6 +129,8 @@ vmCommand
|
|
|
128
129
|
.option("--zone <zone>", "GCP zone", "europe-west1-b")
|
|
129
130
|
.option("--json", "Output as JSON")
|
|
130
131
|
.action(async (name, options) => {
|
|
132
|
+
// RBAC: Only supervisor and devops can create VMs
|
|
133
|
+
requirePermission("vm:create");
|
|
131
134
|
const config = ensureConfig();
|
|
132
135
|
const validBusinessTypes = [
|
|
133
136
|
"support",
|
|
@@ -356,6 +359,8 @@ vmCommand
|
|
|
356
359
|
.description("Start/provision the VM")
|
|
357
360
|
.option("--json", "Output as JSON")
|
|
358
361
|
.action(async (id, options) => {
|
|
362
|
+
// RBAC: Only supervisor and devops can start VMs
|
|
363
|
+
requirePermission("vm:manage");
|
|
359
364
|
const config = ensureConfig();
|
|
360
365
|
console.log("Starting VM provisioning...");
|
|
361
366
|
console.log("This may take a few minutes...\n");
|
package/dist/index.js
CHANGED
|
@@ -21,9 +21,12 @@ import { worktreeCommand } from "./commands/worktree.js";
|
|
|
21
21
|
import { workerCommand } from "./commands/worker.js";
|
|
22
22
|
import { bizCommand } from "./commands/biz.js";
|
|
23
23
|
import { printLLMContext, llmCommand } from "./commands/llm-context.js";
|
|
24
|
+
import { agentMsgCommand } from "./commands/agent-msg.js";
|
|
24
25
|
import { runInteractiveMode } from "./commands/interactive.js";
|
|
25
26
|
import { serviceAccountCommand } from "./commands/service-account.js";
|
|
26
27
|
import { chatCommand } from "./commands/chat.js";
|
|
28
|
+
import { previewCommand } from "./commands/preview.js";
|
|
29
|
+
import { initCommand } from "./commands/init.js";
|
|
27
30
|
// Read version from package.json
|
|
28
31
|
const require = createRequire(import.meta.url);
|
|
29
32
|
const packageJson = require("../package.json");
|
|
@@ -54,7 +57,10 @@ program.addCommand(workerCommand);
|
|
|
54
57
|
program.addCommand(bizCommand);
|
|
55
58
|
program.addCommand(serviceAccountCommand);
|
|
56
59
|
program.addCommand(chatCommand);
|
|
60
|
+
program.addCommand(previewCommand);
|
|
57
61
|
program.addCommand(llmCommand);
|
|
62
|
+
program.addCommand(initCommand);
|
|
63
|
+
program.addCommand(agentMsgCommand);
|
|
58
64
|
// Handle --llm flag specially
|
|
59
65
|
if (process.argv.includes("--llm")) {
|
|
60
66
|
printLLMContext();
|