@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.
@@ -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
- const url = new URL("/api/tasks", config.apiUrl);
159
- const res = await fetch(url.toString(), {
160
- headers: {
161
- "x-api-key": config.apiKey,
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 (res.ok) {
165
- console.log(`API connection successful (API URL: ${config.apiUrl})`);
166
- }
167
- else if (res.status === 401) {
168
- console.error(`API connection failed: Unauthorized (HTTP 401)`);
169
- console.error(" Check your API key with: husky config set api-key <key>");
170
- process.exit(1);
171
- }
172
- else if (res.status === 403) {
173
- console.error(`API connection failed: Forbidden (HTTP 403)`);
174
- console.error(" Your API key may not have the required permissions");
175
- process.exit(1);
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
- else {
178
- console.error(`API connection failed: HTTP ${res.status}`);
179
- try {
180
- const body = await res.json();
181
- if (body.error) {
182
- console.error(` Error: ${body.error}`);
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
- catch {
186
- // Ignore JSON parse errors
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,2 @@
1
+ import { Command } from "commander";
2
+ export declare const initCommand: Command;
@@ -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,2 @@
1
+ import { Command } from "commander";
2
+ export declare const previewCommand: Command;
@@ -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
+ });
@@ -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.");
@@ -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, aider, custom)")
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, aider, custom)", "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();