@vibe-assurance/cli 1.0.6 → 1.1.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/bin/vibe.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  // Load .env file if present (for local development)
4
4
  const path = require('path');
@@ -12,6 +12,7 @@ const login = require('../src/commands/login');
12
12
  const logout = require('../src/commands/logout');
13
13
  const mcpServer = require('../src/commands/mcp-server');
14
14
  const setupClaude = require('../src/commands/setup-claude');
15
+ const { registerProjectCommands } = require('../src/commands/projects');
15
16
 
16
17
  program
17
18
  .name('vibe')
@@ -38,4 +39,7 @@ program
38
39
  .description('Configure Claude Code to use the Vibe Assurance MCP server')
39
40
  .action(setupClaude);
40
41
 
42
+ // CR-2026-043: Register project management commands
43
+ registerProjectCommands(program);
44
+
41
45
  program.parse();
package/nul ADDED
@@ -0,0 +1,2 @@
1
+ dir: cannot access '/s': No such file or directory
2
+ dir: cannot access '/b': No such file or directory
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-assurance/cli",
3
- "version": "1.0.6",
3
+ "version": "1.1.0",
4
4
  "description": "Vibe Assurance CLI - Connect AI coding agents to your governance platform via MCP",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -31,6 +31,7 @@
31
31
  "chalk": "^4.1.2",
32
32
  "commander": "^12.0.0",
33
33
  "dotenv": "^16.6.1",
34
+ "inquirer": "^8.2.6",
34
35
  "keytar": "^7.9.0",
35
36
  "open": "^8.4.2",
36
37
  "ora": "^5.4.1"
package/src/api/client.js CHANGED
@@ -1,11 +1,12 @@
1
1
  const axios = require('axios');
2
- const { getCredentials, storeCredentials, deleteCredentials } = require('../config/credentials');
2
+ const { getCredentials, storeCredentials, deleteCredentials, getProjectId } = require('../config/credentials');
3
3
 
4
4
  // Default to dev Azure URL, can be overridden via environment variable
5
5
  const API_BASE_URL = process.env.VIBE_API_URL || 'https://agent-platform-dev.azurewebsites.net';
6
6
 
7
7
  /**
8
8
  * Create an authenticated axios instance with token refresh handling
9
+ * CR-2026-043: Now includes X-Project-Id header for project scoping
9
10
  * @returns {Promise<import('axios').AxiosInstance>}
10
11
  */
11
12
  async function createClient() {
@@ -14,13 +15,23 @@ async function createClient() {
14
15
  throw new Error('Not authenticated. Run: vibe login');
15
16
  }
16
17
 
18
+ // CR-2026-043: Get current project ID for scoping
19
+ const projectId = await getProjectId();
20
+
21
+ const headers = {
22
+ 'Authorization': `Bearer ${creds.accessToken}`,
23
+ 'Content-Type': 'application/json',
24
+ 'User-Agent': 'vibe-cli/1.0.0'
25
+ };
26
+
27
+ // CR-2026-043: Add project header if available
28
+ if (projectId) {
29
+ headers['X-Project-Id'] = projectId;
30
+ }
31
+
17
32
  const client = axios.create({
18
33
  baseURL: API_BASE_URL,
19
- headers: {
20
- 'Authorization': `Bearer ${creds.accessToken}`,
21
- 'Content-Type': 'application/json',
22
- 'User-Agent': 'vibe-cli/1.0.0'
23
- },
34
+ headers,
24
35
  timeout: 30000 // 30 second timeout
25
36
  });
26
37
 
@@ -3,7 +3,9 @@ const http = require('http');
3
3
  const { URL } = require('url');
4
4
  const chalk = require('chalk');
5
5
  const ora = require('ora');
6
- const { storeCredentials, hasValidCredentials } = require('../config/credentials');
6
+ const inquirer = require('inquirer');
7
+ const axios = require('axios');
8
+ const { storeCredentials, hasValidCredentials, setProjectId } = require('../config/credentials');
7
9
  const { API_BASE_URL } = require('../api/client');
8
10
 
9
11
  // Local callback server port (chosen to be unlikely to conflict)
@@ -140,6 +142,10 @@ async function login() {
140
142
  await authPromise;
141
143
  cleanup();
142
144
  spinner.succeed('Successfully authenticated!');
145
+
146
+ // CR-2026-043: Select project after successful login
147
+ await selectProject();
148
+
143
149
  console.log(chalk.green('\nYou are now logged in to Vibe Assurance.'));
144
150
  console.log('\nNext steps:');
145
151
  console.log(' 1. Run `vibe setup-claude` to configure Claude Code');
@@ -153,6 +159,67 @@ async function login() {
153
159
  }
154
160
  }
155
161
 
162
+ /**
163
+ * CR-2026-043: Select project after successful authentication
164
+ * Fetches user's projects and prompts for selection if multiple exist
165
+ */
166
+ async function selectProject() {
167
+ const spinner = ora('Fetching your projects...').start();
168
+
169
+ try {
170
+ // Get credentials to make authenticated request
171
+ const { getCredentials } = require('../config/credentials');
172
+ const creds = await getCredentials();
173
+
174
+ // Fetch user profile which now includes projects (CR-2026-043 backend change)
175
+ const response = await axios.get(`${API_BASE_URL}/api/auth/me`, {
176
+ headers: {
177
+ 'Authorization': `Bearer ${creds.accessToken}`,
178
+ 'Content-Type': 'application/json'
179
+ }
180
+ });
181
+
182
+ const { projects, defaultProjectId } = response.data;
183
+ spinner.stop();
184
+
185
+ if (!projects || projects.length === 0) {
186
+ console.log(chalk.yellow('\nNo projects found. A default project will be created when you first use the API.'));
187
+ return;
188
+ }
189
+
190
+ if (projects.length === 1) {
191
+ // Auto-select single project
192
+ await setProjectId(projects[0]._id, projects[0].name);
193
+ console.log(chalk.green(`\n✓ Selected project: ${projects[0].name}`));
194
+ return;
195
+ }
196
+
197
+ // Multiple projects - prompt for selection
198
+ console.log(''); // Empty line for spacing
199
+ const choices = projects.map(p => ({
200
+ name: `${p.name}${p.isDefault ? chalk.dim(' (default)') : ''}`,
201
+ value: { id: p._id, name: p.name }
202
+ }));
203
+
204
+ const { selected } = await inquirer.prompt([{
205
+ type: 'list',
206
+ name: 'selected',
207
+ message: 'Select a project to work with:',
208
+ choices,
209
+ default: choices.findIndex(c => c.value.id === defaultProjectId)
210
+ }]);
211
+
212
+ await setProjectId(selected.id, selected.name);
213
+ console.log(chalk.green(`✓ Selected project: ${selected.name}`));
214
+
215
+ } catch (err) {
216
+ spinner.stop();
217
+ // Non-fatal error - user can still use CLI with default project
218
+ console.log(chalk.yellow('\nCould not fetch projects. Using default project.'));
219
+ console.log(chalk.dim(` (You can run 'vibe project select' later to switch projects)`));
220
+ }
221
+ }
222
+
156
223
  /**
157
224
  * Generate success HTML page
158
225
  * Matches the main app's branding with Tailwind CSS and emerald colors
@@ -0,0 +1,211 @@
1
+ /**
2
+ * CR-2026-043: Project management commands
3
+ *
4
+ * Provides CLI commands for listing and switching between projects:
5
+ * - vibe projects - List all accessible projects
6
+ * - vibe project current - Show current project
7
+ * - vibe project select - Switch to a different project
8
+ */
9
+
10
+ const chalk = require('chalk');
11
+ const ora = require('ora');
12
+ const inquirer = require('inquirer');
13
+ const api = require('../api/client');
14
+ const { getProjectId, getProjectName, setProjectId, hasValidCredentials } = require('../config/credentials');
15
+
16
+ /**
17
+ * List all accessible projects
18
+ * Command: vibe projects
19
+ */
20
+ async function listProjects() {
21
+ // Check authentication
22
+ if (!await hasValidCredentials()) {
23
+ console.log(chalk.red('Not authenticated. Run: vibe login'));
24
+ process.exit(1);
25
+ }
26
+
27
+ const spinner = ora('Fetching projects...').start();
28
+
29
+ try {
30
+ const projects = await api.get('/api/projects');
31
+ const currentProjectId = await getProjectId();
32
+ spinner.stop();
33
+
34
+ if (!projects || projects.length === 0) {
35
+ console.log(chalk.yellow('\nNo projects found.'));
36
+ console.log(chalk.dim('Create a project in the Vibe Assurance web portal.'));
37
+ return;
38
+ }
39
+
40
+ console.log(chalk.bold('\nYour Projects:\n'));
41
+
42
+ for (const p of projects) {
43
+ const current = p._id === currentProjectId ? chalk.green(' ← current') : '';
44
+ const def = p.isDefault ? chalk.dim(' (default)') : '';
45
+
46
+ console.log(` ${p.name}${def}${current}`);
47
+ console.log(chalk.dim(` ID: ${p._id}`));
48
+ if (p.description) {
49
+ console.log(chalk.dim(` ${p.description}`));
50
+ }
51
+ console.log('');
52
+ }
53
+ } catch (err) {
54
+ spinner.stop();
55
+ console.error(chalk.red('Failed to fetch projects:'), err.message);
56
+ process.exit(1);
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Show current project
62
+ * Command: vibe project current
63
+ */
64
+ async function showCurrentProject() {
65
+ // Check authentication
66
+ if (!await hasValidCredentials()) {
67
+ console.log(chalk.red('Not authenticated. Run: vibe login'));
68
+ process.exit(1);
69
+ }
70
+
71
+ const projectId = await getProjectId();
72
+ const projectName = await getProjectName();
73
+
74
+ if (!projectId) {
75
+ console.log(chalk.yellow('No project selected.'));
76
+ console.log(chalk.dim('Run "vibe login" or "vibe project select" to select a project.'));
77
+ return;
78
+ }
79
+
80
+ const spinner = ora('Fetching project details...').start();
81
+
82
+ try {
83
+ const project = await api.get(`/api/projects/${projectId}`);
84
+ spinner.stop();
85
+
86
+ console.log(chalk.bold('\nCurrent Project:\n'));
87
+ console.log(` Name: ${project.name}`);
88
+ console.log(` ID: ${project._id}`);
89
+ if (project.description) {
90
+ console.log(` Description: ${project.description}`);
91
+ }
92
+ if (project.isDefault) {
93
+ console.log(chalk.dim(' (This is your default project)'));
94
+ }
95
+ } catch (err) {
96
+ spinner.stop();
97
+ // Fallback to cached info if API fails
98
+ if (projectName) {
99
+ console.log(chalk.bold('\nCurrent Project:\n'));
100
+ console.log(` Name: ${projectName}`);
101
+ console.log(` ID: ${projectId}`);
102
+ console.log(chalk.dim(' (Unable to fetch full details)'));
103
+ } else {
104
+ console.log(chalk.yellow(`Project ID: ${projectId}`));
105
+ console.log(chalk.dim('(Unable to fetch project details)'));
106
+ }
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Select/switch project
112
+ * Command: vibe project select [projectId]
113
+ * @param {string} [projectIdArg] - Optional project ID or name to select
114
+ */
115
+ async function selectProject(projectIdArg) {
116
+ // Check authentication
117
+ if (!await hasValidCredentials()) {
118
+ console.log(chalk.red('Not authenticated. Run: vibe login'));
119
+ process.exit(1);
120
+ }
121
+
122
+ const spinner = ora('Fetching projects...').start();
123
+
124
+ try {
125
+ const projects = await api.get('/api/projects');
126
+ spinner.stop();
127
+
128
+ if (!projects || projects.length === 0) {
129
+ console.log(chalk.yellow('No projects found.'));
130
+ return;
131
+ }
132
+
133
+ let selected;
134
+
135
+ if (projectIdArg) {
136
+ // Find by ID or name (case-insensitive)
137
+ selected = projects.find(p =>
138
+ p._id === projectIdArg ||
139
+ p.name.toLowerCase() === projectIdArg.toLowerCase()
140
+ );
141
+
142
+ if (!selected) {
143
+ console.log(chalk.red(`Project not found: ${projectIdArg}`));
144
+ console.log(chalk.dim('\nAvailable projects:'));
145
+ projects.forEach(p => console.log(chalk.dim(` - ${p.name} (${p._id})`)));
146
+ process.exit(1);
147
+ }
148
+ } else {
149
+ // Interactive selection
150
+ const currentProjectId = await getProjectId();
151
+ const choices = projects.map(p => ({
152
+ name: `${p.name}${p.isDefault ? chalk.dim(' (default)') : ''}${p._id === currentProjectId ? chalk.green(' ← current') : ''}`,
153
+ value: p
154
+ }));
155
+
156
+ const result = await inquirer.prompt([{
157
+ type: 'list',
158
+ name: 'selected',
159
+ message: 'Select a project:',
160
+ choices
161
+ }]);
162
+
163
+ selected = result.selected;
164
+ }
165
+
166
+ // Store the selected project
167
+ await setProjectId(selected._id, selected.name);
168
+ console.log(chalk.green(`✓ Switched to project: ${selected.name}`));
169
+
170
+ } catch (err) {
171
+ spinner.stop();
172
+ console.error(chalk.red('Failed to switch project:'), err.message);
173
+ process.exit(1);
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Register project commands with Commander
179
+ * @param {import('commander').Command} program - Commander program instance
180
+ */
181
+ function registerProjectCommands(program) {
182
+ // vibe projects - List all projects
183
+ program
184
+ .command('projects')
185
+ .description('List your accessible projects')
186
+ .action(listProjects);
187
+
188
+ // vibe project - Project subcommands
189
+ const projectCmd = program
190
+ .command('project')
191
+ .description('Manage project context');
192
+
193
+ // vibe project current
194
+ projectCmd
195
+ .command('current')
196
+ .description('Show current project')
197
+ .action(showCurrentProject);
198
+
199
+ // vibe project select [projectId]
200
+ projectCmd
201
+ .command('select [projectId]')
202
+ .description('Switch to a different project (by ID or name)')
203
+ .action(selectProject);
204
+ }
205
+
206
+ module.exports = {
207
+ listProjects,
208
+ showCurrentProject,
209
+ selectProject,
210
+ registerProjectCommands
211
+ };
@@ -121,9 +121,47 @@ async function hasValidCredentials() {
121
121
  return true;
122
122
  }
123
123
 
124
+ /**
125
+ * CR-2026-043: Get stored project ID
126
+ * @returns {Promise<string|null>} Project ID or null if not set
127
+ */
128
+ async function getProjectId() {
129
+ const creds = await getCredentials();
130
+ return creds?.projectId || null;
131
+ }
132
+
133
+ /**
134
+ * CR-2026-043: Get stored project name
135
+ * @returns {Promise<string|null>} Project name or null if not set
136
+ */
137
+ async function getProjectName() {
138
+ const creds = await getCredentials();
139
+ return creds?.projectName || null;
140
+ }
141
+
142
+ /**
143
+ * CR-2026-043: Set project ID (updates existing credentials)
144
+ * @param {string} projectId - Project ID to store
145
+ * @param {string} projectName - Project name for display
146
+ * @returns {Promise<void>}
147
+ */
148
+ async function setProjectId(projectId, projectName) {
149
+ const creds = await getCredentials();
150
+ if (creds) {
151
+ await storeCredentials({
152
+ ...creds,
153
+ projectId,
154
+ projectName
155
+ });
156
+ }
157
+ }
158
+
124
159
  module.exports = {
125
160
  storeCredentials,
126
161
  getCredentials,
127
162
  deleteCredentials,
128
- hasValidCredentials
163
+ hasValidCredentials,
164
+ getProjectId,
165
+ getProjectName,
166
+ setProjectId
129
167
  };
package/src/mcp/tools.js CHANGED
@@ -226,7 +226,7 @@ const tools = [
226
226
 
227
227
  {
228
228
  name: 'vibe_update_artifact',
229
- description: 'Update an existing artifact. You can update the status, content, title, or metadata.',
229
+ description: 'Update an existing artifact. You can update the status, content, title, or metadata. WARNING: If you provide a "files" array, it REPLACES ALL existing files. To safely add a single file without affecting others, use vibe_append_file instead.',
230
230
  inputSchema: {
231
231
  type: 'object',
232
232
  properties: {
@@ -249,7 +249,7 @@ const tools = [
249
249
  },
250
250
  files: {
251
251
  type: 'array',
252
- description: 'Updated files array',
252
+ description: 'WARNING: This REPLACES ALL existing files. Use vibe_append_file to safely add a single file.',
253
253
  items: {
254
254
  type: 'object',
255
255
  properties: {
@@ -270,6 +270,32 @@ const tools = [
270
270
  }
271
271
  },
272
272
 
273
+ {
274
+ name: 'vibe_append_file',
275
+ description: 'Safely append or update a single file in an artifact WITHOUT affecting other files. Use this instead of vibe_update_artifact when you only need to add one file (e.g., adding verification-report.md to a CR). This is the SAFE way to add files - it will not destroy existing documentation.',
276
+ inputSchema: {
277
+ type: 'object',
278
+ properties: {
279
+ artifactId: {
280
+ type: 'string',
281
+ description: 'The artifact ID to add the file to (e.g., "CR-2025-024")'
282
+ },
283
+ name: {
284
+ type: 'string',
285
+ description: 'Filename to add or update (e.g., "verification-report.md")'
286
+ },
287
+ content: {
288
+ type: 'string',
289
+ description: 'Full file content (must be complete, not abbreviated)'
290
+ }
291
+ },
292
+ required: ['artifactId', 'name', 'content']
293
+ },
294
+ handler: async ({ artifactId, name, content }) => {
295
+ return await api.post(`/api/mcp/artifacts/${artifactId}/files`, { name, content });
296
+ }
297
+ },
298
+
273
299
  {
274
300
  name: 'vibe_list_artifacts',
275
301
  description: 'List your stored artifacts with optional filters. Use this to see what governance documents you have stored.',