@girardmedia/bootspring 2.1.2 → 2.2.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/bootspring.js +102 -82
- package/generators/api-docs.js +3 -3
- package/generators/decisions.js +14 -14
- package/generators/health.js +6 -6
- package/generators/sprint.js +4 -4
- package/generators/templates/build-planning.template.js +2 -2
- package/generators/visual-doc-generator.js +1 -1
- package/package.json +2 -15
- package/cli/agent.js +0 -799
- package/cli/auth.js +0 -896
- package/cli/billing.js +0 -320
- package/cli/build.js +0 -1442
- package/cli/dashboard.js +0 -123
- package/cli/init.js +0 -669
- package/cli/mcp.js +0 -240
- package/cli/orchestrator.js +0 -240
- package/cli/project.js +0 -825
- package/cli/quality.js +0 -281
- package/cli/skill.js +0 -503
- package/cli/switch.js +0 -453
- package/cli/todo.js +0 -629
- package/cli/update.js +0 -132
package/cli/project.js
DELETED
|
@@ -1,825 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bootspring Project Command
|
|
3
|
-
*
|
|
4
|
-
* Manage projects from the CLI.
|
|
5
|
-
*
|
|
6
|
-
* @package bootspring
|
|
7
|
-
* @module cli/project
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
const https = require('https');
|
|
11
|
-
const http = require('http');
|
|
12
|
-
const auth = require('../core/auth');
|
|
13
|
-
const session = require('../core/session');
|
|
14
|
-
const projectActivity = require('../core/project-activity');
|
|
15
|
-
const readline = require('readline');
|
|
16
|
-
|
|
17
|
-
const API_BASE = process.env.BOOTSPRING_API_URL || 'https://www.bootspring.com';
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Make direct API request (without v1 prefix)
|
|
21
|
-
*/
|
|
22
|
-
async function apiRequest(method, path, data = null) {
|
|
23
|
-
const token = auth.getToken();
|
|
24
|
-
const apiKey = auth.getApiKey();
|
|
25
|
-
const url = new URL(`/api${path}`, API_BASE);
|
|
26
|
-
const isHttps = url.protocol === 'https:';
|
|
27
|
-
const httpModule = isHttps ? https : http;
|
|
28
|
-
|
|
29
|
-
const headers = {
|
|
30
|
-
'Content-Type': 'application/json',
|
|
31
|
-
'User-Agent': `bootspring-cli/${require('../package.json').version}`,
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
// Use API key if available, otherwise use JWT token
|
|
35
|
-
if (apiKey) {
|
|
36
|
-
headers['X-API-Key'] = apiKey;
|
|
37
|
-
} else if (token) {
|
|
38
|
-
headers['Authorization'] = `Bearer ${token}`;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return new Promise((resolve, reject) => {
|
|
42
|
-
const req = httpModule.request(url, {
|
|
43
|
-
method,
|
|
44
|
-
headers,
|
|
45
|
-
timeout: 30000
|
|
46
|
-
}, (res) => {
|
|
47
|
-
let body = '';
|
|
48
|
-
res.on('data', chunk => body += chunk);
|
|
49
|
-
res.on('end', () => {
|
|
50
|
-
try {
|
|
51
|
-
const json = JSON.parse(body);
|
|
52
|
-
if (res.statusCode >= 400) {
|
|
53
|
-
const error = new Error(json.message || json.error || 'API Error');
|
|
54
|
-
error.status = res.statusCode;
|
|
55
|
-
error.code = json.error || json.code;
|
|
56
|
-
reject(error);
|
|
57
|
-
} else {
|
|
58
|
-
resolve(json);
|
|
59
|
-
}
|
|
60
|
-
} catch {
|
|
61
|
-
if (res.statusCode >= 400) {
|
|
62
|
-
const error = new Error(body || 'API Error');
|
|
63
|
-
error.status = res.statusCode;
|
|
64
|
-
reject(error);
|
|
65
|
-
} else {
|
|
66
|
-
resolve(body);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
req.on('error', reject);
|
|
73
|
-
req.on('timeout', () => {
|
|
74
|
-
req.destroy();
|
|
75
|
-
reject(new Error('Request timeout'));
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
if (data) {
|
|
79
|
-
req.write(JSON.stringify(data));
|
|
80
|
-
}
|
|
81
|
-
req.end();
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const BRAND = {
|
|
86
|
-
name: 'Bootspring',
|
|
87
|
-
color: '\x1b[36m',
|
|
88
|
-
reset: '\x1b[0m',
|
|
89
|
-
bold: '\x1b[1m',
|
|
90
|
-
dim: '\x1b[2m',
|
|
91
|
-
green: '\x1b[32m',
|
|
92
|
-
yellow: '\x1b[33m',
|
|
93
|
-
red: '\x1b[31m'
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Prompt for input
|
|
98
|
-
*/
|
|
99
|
-
function prompt(question) {
|
|
100
|
-
const rl = readline.createInterface({
|
|
101
|
-
input: process.stdin,
|
|
102
|
-
output: process.stdout
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
return new Promise((resolve) => {
|
|
106
|
-
rl.question(question, (answer) => {
|
|
107
|
-
rl.close();
|
|
108
|
-
resolve(answer.trim());
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Print a single project
|
|
115
|
-
*/
|
|
116
|
-
function printProject(project, currentProject, showOwner = false) {
|
|
117
|
-
const isCurrent = currentProject && currentProject.id === project.id;
|
|
118
|
-
const marker = isCurrent ? `${BRAND.green}→${BRAND.reset} ` : ' ';
|
|
119
|
-
const activeTag = project.isActive ? '' : ` ${BRAND.dim}(inactive)${BRAND.reset}`;
|
|
120
|
-
const roleTag = project.role && project.role !== 'owner' ? ` ${BRAND.dim}(${project.role})${BRAND.reset}` : '';
|
|
121
|
-
|
|
122
|
-
console.log(`${marker}${BRAND.color}${project.name}${BRAND.reset}${roleTag}${activeTag}`);
|
|
123
|
-
console.log(` ${BRAND.dim}ID: ${project.id}${BRAND.reset}`);
|
|
124
|
-
console.log(` ${BRAND.dim}Slug: ${project.slug}${BRAND.reset}`);
|
|
125
|
-
if (showOwner && project.owner) {
|
|
126
|
-
console.log(` ${BRAND.dim}Owner: ${project.owner.email}${BRAND.reset}`);
|
|
127
|
-
}
|
|
128
|
-
if (project.description) {
|
|
129
|
-
console.log(` ${BRAND.dim}${project.description}${BRAND.reset}`);
|
|
130
|
-
}
|
|
131
|
-
console.log(` ${BRAND.dim}API Keys: ${project.apiKeyCount}${BRAND.reset}`);
|
|
132
|
-
if (project.memberCount > 1) {
|
|
133
|
-
console.log(` ${BRAND.dim}Members: ${project.memberCount}${BRAND.reset}`);
|
|
134
|
-
}
|
|
135
|
-
console.log();
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* List all projects (owned and shared)
|
|
140
|
-
*/
|
|
141
|
-
async function listProjects() {
|
|
142
|
-
if (!auth.isAuthenticated()) {
|
|
143
|
-
console.log(`${BRAND.red}Not logged in.${BRAND.reset} Run: bootspring auth login`);
|
|
144
|
-
process.exit(1);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
try {
|
|
148
|
-
const response = await apiRequest('GET', '/projects?grouped=true');
|
|
149
|
-
const owned = response.owned || [];
|
|
150
|
-
const shared = response.shared || [];
|
|
151
|
-
|
|
152
|
-
if (owned.length === 0 && shared.length === 0) {
|
|
153
|
-
console.log(`\n${BRAND.dim}No projects found.${BRAND.reset}`);
|
|
154
|
-
console.log(`Create one with: ${BRAND.color}bootspring project create${BRAND.reset}\n`);
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const currentProject = session.getEffectiveProject();
|
|
159
|
-
|
|
160
|
-
// Show owned projects
|
|
161
|
-
if (owned.length > 0) {
|
|
162
|
-
console.log(`\n${BRAND.bold}Your Projects${BRAND.reset}\n`);
|
|
163
|
-
for (const project of owned) {
|
|
164
|
-
printProject(project, currentProject, false);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Show shared projects
|
|
169
|
-
if (shared.length > 0) {
|
|
170
|
-
console.log(`${BRAND.bold}Shared With You${BRAND.reset}\n`);
|
|
171
|
-
for (const project of shared) {
|
|
172
|
-
printProject(project, currentProject, true);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
console.log(`${BRAND.dim}Limits: ${response.limits.current}/${response.limits.limit} projects${BRAND.reset}\n`);
|
|
177
|
-
} catch (error) {
|
|
178
|
-
console.log(`${BRAND.red}Error: ${error.message}${BRAND.reset}`);
|
|
179
|
-
process.exit(1);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Parse flags from args
|
|
185
|
-
*/
|
|
186
|
-
function parseFlags(args) {
|
|
187
|
-
const flags = {};
|
|
188
|
-
const positional = [];
|
|
189
|
-
|
|
190
|
-
for (let i = 0; i < args.length; i++) {
|
|
191
|
-
const arg = args[i];
|
|
192
|
-
if (arg.startsWith('--')) {
|
|
193
|
-
const [key, ...valueParts] = arg.slice(2).split('=');
|
|
194
|
-
flags[key] = valueParts.length > 0 ? valueParts.join('=') : (args[i + 1] && !args[i + 1].startsWith('-') ? args[++i] : true);
|
|
195
|
-
} else if (arg.startsWith('-')) {
|
|
196
|
-
flags[arg.slice(1)] = args[i + 1] && !args[i + 1].startsWith('-') ? args[++i] : true;
|
|
197
|
-
} else {
|
|
198
|
-
positional.push(arg);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
return { flags, positional };
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function ensureAuthenticated() {
|
|
206
|
-
if (!auth.isAuthenticated()) {
|
|
207
|
-
console.log(`${BRAND.red}Not logged in.${BRAND.reset} Run: bootspring auth login`);
|
|
208
|
-
process.exit(1);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function resolveProjectId(flags, positionalProjectId) {
|
|
213
|
-
const fromFlags = typeof flags.project === 'string' ? flags.project : (typeof flags.p === 'string' ? flags.p : undefined);
|
|
214
|
-
const resolved = positionalProjectId || fromFlags || session.getEffectiveProject()?.id;
|
|
215
|
-
if (!resolved) {
|
|
216
|
-
console.log(`\n${BRAND.yellow}No project specified.${BRAND.reset}`);
|
|
217
|
-
console.log(`${BRAND.dim}Use --project <id> or run bootspring switch.${BRAND.reset}\n`);
|
|
218
|
-
process.exit(1);
|
|
219
|
-
}
|
|
220
|
-
return resolved;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function trackCollaborationEvent(event, payload = {}) {
|
|
224
|
-
projectActivity.trackProjectActivity(event, payload);
|
|
225
|
-
|
|
226
|
-
const messageMap = {
|
|
227
|
-
project_invitation_sent: `Invitation sent to ${String(payload.email || payload.target || 'member')}`,
|
|
228
|
-
project_invitation_accepted: `${String(payload.email || payload.target || 'Invitation')} accepted`,
|
|
229
|
-
project_invitation_declined: `${String(payload.email || payload.target || 'Invitation')} declined`,
|
|
230
|
-
project_member_added: `Member added: ${String(payload.email || payload.target || 'member')}`,
|
|
231
|
-
project_member_removed: `Member removed: ${String(payload.targetUserId || payload.target || 'member')}`,
|
|
232
|
-
project_member_role_updated: `Role updated for ${String(payload.targetUserId || payload.target || 'member')}`,
|
|
233
|
-
project_owner_transferred: `Ownership transferred to ${String(payload.newOwnerId || payload.target || 'new owner')}`
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
projectActivity.trackMembershipNotification({
|
|
237
|
-
event,
|
|
238
|
-
projectId: payload.projectId,
|
|
239
|
-
actor: payload.actor || 'cli',
|
|
240
|
-
target: payload.target || payload.email || payload.targetUserId || payload.newOwnerId,
|
|
241
|
-
message: messageMap[event] || String(payload.message || 'Project membership updated'),
|
|
242
|
-
metadata: payload
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function formatTimestamp(value) {
|
|
247
|
-
const ts = new Date(String(value || ''));
|
|
248
|
-
if (Number.isNaN(ts.getTime())) return 'unknown time';
|
|
249
|
-
return ts.toLocaleString();
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Check for similar projects (duplicate detection)
|
|
254
|
-
*/
|
|
255
|
-
async function checkSimilarProjects(name, repoUrl) {
|
|
256
|
-
try {
|
|
257
|
-
const params = new URLSearchParams();
|
|
258
|
-
if (name) params.set('name', name);
|
|
259
|
-
if (repoUrl) params.set('repo', repoUrl);
|
|
260
|
-
|
|
261
|
-
const response = await apiRequest('GET', `/projects/similar?${params.toString()}`);
|
|
262
|
-
return response.matches || [];
|
|
263
|
-
} catch (error) {
|
|
264
|
-
// Don't block creation if similar check fails
|
|
265
|
-
console.log(`${BRAND.dim}(Could not check for similar projects)${BRAND.reset}`);
|
|
266
|
-
return [];
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Join an existing project as a member
|
|
272
|
-
*/
|
|
273
|
-
async function joinProject(projectId) {
|
|
274
|
-
try {
|
|
275
|
-
// Get current user's email to add themselves
|
|
276
|
-
// For now, we'll inform the user to request access
|
|
277
|
-
console.log(`\n${BRAND.yellow}To join this project, ask the owner to add you as a member.${BRAND.reset}`);
|
|
278
|
-
console.log(`${BRAND.dim}They can do this from the project settings in the dashboard.${BRAND.reset}\n`);
|
|
279
|
-
return false;
|
|
280
|
-
} catch (error) {
|
|
281
|
-
console.log(`${BRAND.red}Error joining project: ${error.message}${BRAND.reset}`);
|
|
282
|
-
return false;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
async function listMembers(args) {
|
|
287
|
-
ensureAuthenticated();
|
|
288
|
-
const { flags, positional } = parseFlags(args);
|
|
289
|
-
const projectId = resolveProjectId(flags, positional[0]);
|
|
290
|
-
|
|
291
|
-
try {
|
|
292
|
-
const response = await apiRequest('GET', `/projects/${encodeURIComponent(projectId)}/members`);
|
|
293
|
-
const members = Array.isArray(response) ? response : (response.members || []);
|
|
294
|
-
|
|
295
|
-
console.log(`\n${BRAND.bold}Project Members${BRAND.reset} ${BRAND.dim}(${projectId})${BRAND.reset}\n`);
|
|
296
|
-
if (members.length === 0) {
|
|
297
|
-
console.log(`${BRAND.dim}No members found.${BRAND.reset}\n`);
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
for (const member of members) {
|
|
302
|
-
const email = member.email || member.userId;
|
|
303
|
-
const invited = member.invitedAt ? ` ${BRAND.dim}(invited ${formatTimestamp(member.invitedAt)})${BRAND.reset}` : '';
|
|
304
|
-
console.log(` ${BRAND.color}${email}${BRAND.reset} ${BRAND.dim}[${member.role}]${BRAND.reset}${invited}`);
|
|
305
|
-
}
|
|
306
|
-
console.log();
|
|
307
|
-
} catch (error) {
|
|
308
|
-
console.log(`\n${BRAND.red}Error listing members: ${error.message}${BRAND.reset}\n`);
|
|
309
|
-
process.exit(1);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
async function inviteMember(args) {
|
|
314
|
-
ensureAuthenticated();
|
|
315
|
-
const { flags, positional } = parseFlags(args);
|
|
316
|
-
const email = positional[0];
|
|
317
|
-
if (!email) {
|
|
318
|
-
console.log(`${BRAND.red}Email is required.${BRAND.reset}`);
|
|
319
|
-
console.log(`${BRAND.dim}Usage: bootspring project invite <email> [--role member] [--project <id>]${BRAND.reset}\n`);
|
|
320
|
-
process.exit(1);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
const projectId = resolveProjectId(flags, typeof flags.project === 'string' ? flags.project : undefined);
|
|
324
|
-
const role = typeof flags.role === 'string' ? flags.role : 'member';
|
|
325
|
-
|
|
326
|
-
try {
|
|
327
|
-
const invitation = await apiRequest('POST', `/projects/${encodeURIComponent(projectId)}/invitations`, {
|
|
328
|
-
email,
|
|
329
|
-
role
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
trackCollaborationEvent('project_invitation_sent', {
|
|
333
|
-
projectId,
|
|
334
|
-
invitationId: invitation.id,
|
|
335
|
-
email,
|
|
336
|
-
role,
|
|
337
|
-
actor: 'cli'
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
console.log(`\n${BRAND.green}✓${BRAND.reset} Invitation sent to ${BRAND.color}${email}${BRAND.reset} (${role}).`);
|
|
341
|
-
if (invitation.id) {
|
|
342
|
-
console.log(`${BRAND.dim}Invitation ID: ${invitation.id}${BRAND.reset}`);
|
|
343
|
-
}
|
|
344
|
-
console.log();
|
|
345
|
-
} catch (error) {
|
|
346
|
-
if (error.status === 404 || error.status === 405) {
|
|
347
|
-
const member = await apiRequest('POST', `/projects/${encodeURIComponent(projectId)}/members`, {
|
|
348
|
-
email,
|
|
349
|
-
role
|
|
350
|
-
});
|
|
351
|
-
trackCollaborationEvent('project_member_added', {
|
|
352
|
-
projectId,
|
|
353
|
-
email: member.email || email,
|
|
354
|
-
role: member.role || role,
|
|
355
|
-
actor: 'cli',
|
|
356
|
-
fallback: 'direct_member_add'
|
|
357
|
-
});
|
|
358
|
-
console.log(`\n${BRAND.green}✓${BRAND.reset} Added ${BRAND.color}${email}${BRAND.reset} directly as ${role}.`);
|
|
359
|
-
console.log(`${BRAND.dim}(Invitation API unavailable; used direct add fallback.)${BRAND.reset}\n`);
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
console.log(`\n${BRAND.red}Error inviting member: ${error.message}${BRAND.reset}\n`);
|
|
363
|
-
process.exit(1);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
async function listInvitations(args) {
|
|
368
|
-
ensureAuthenticated();
|
|
369
|
-
const { flags, positional } = parseFlags(args);
|
|
370
|
-
const projectId = resolveProjectId(flags, positional[0]);
|
|
371
|
-
|
|
372
|
-
try {
|
|
373
|
-
const response = await apiRequest('GET', `/projects/${encodeURIComponent(projectId)}/invitations`);
|
|
374
|
-
const invitations = Array.isArray(response) ? response : (response.invitations || []);
|
|
375
|
-
|
|
376
|
-
console.log(`\n${BRAND.bold}Pending Invitations${BRAND.reset} ${BRAND.dim}(${projectId})${BRAND.reset}\n`);
|
|
377
|
-
if (invitations.length === 0) {
|
|
378
|
-
console.log(`${BRAND.dim}No pending invitations.${BRAND.reset}\n`);
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
for (const invitation of invitations) {
|
|
383
|
-
const status = invitation.status || 'pending';
|
|
384
|
-
const role = invitation.role || 'member';
|
|
385
|
-
const expires = invitation.expiresAt ? ` expires ${formatTimestamp(invitation.expiresAt)}` : '';
|
|
386
|
-
console.log(` ${BRAND.color}${invitation.email}${BRAND.reset} ${BRAND.dim}[${role}] [${status}]${BRAND.reset}`);
|
|
387
|
-
console.log(` ${BRAND.dim}id=${invitation.id}${expires}${BRAND.reset}`);
|
|
388
|
-
}
|
|
389
|
-
console.log();
|
|
390
|
-
} catch (error) {
|
|
391
|
-
if (error.status === 404 || error.status === 405) {
|
|
392
|
-
console.log(`\n${BRAND.yellow}Invitation listing is not supported by this API version.${BRAND.reset}\n`);
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
console.log(`\n${BRAND.red}Error loading invitations: ${error.message}${BRAND.reset}\n`);
|
|
396
|
-
process.exit(1);
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
async function respondToInvitation(args, decision) {
|
|
401
|
-
ensureAuthenticated();
|
|
402
|
-
const { flags, positional } = parseFlags(args);
|
|
403
|
-
const invitationId = positional[0];
|
|
404
|
-
if (!invitationId) {
|
|
405
|
-
console.log(`${BRAND.red}Invitation ID is required.${BRAND.reset}`);
|
|
406
|
-
console.log(`${BRAND.dim}Usage: bootspring project ${decision} <invitation-id>${BRAND.reset}\n`);
|
|
407
|
-
process.exit(1);
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
try {
|
|
411
|
-
const response = await apiRequest('POST', `/projects/invitations/${encodeURIComponent(invitationId)}/${decision}`);
|
|
412
|
-
const project = response.project || {};
|
|
413
|
-
const projectId = String(response.projectId || project.id || '');
|
|
414
|
-
const email = String(response.email || response.targetEmail || '');
|
|
415
|
-
const event = decision === 'accept' ? 'project_invitation_accepted' : 'project_invitation_declined';
|
|
416
|
-
trackCollaborationEvent(event, {
|
|
417
|
-
invitationId,
|
|
418
|
-
projectId,
|
|
419
|
-
email,
|
|
420
|
-
actor: 'cli'
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
console.log(`\n${BRAND.green}✓${BRAND.reset} Invitation ${decision}ed.`);
|
|
424
|
-
if (decision === 'accept' && project.id && project.name) {
|
|
425
|
-
const shouldSwitch = flags.switch !== false && flags.switch !== 'false';
|
|
426
|
-
if (shouldSwitch) {
|
|
427
|
-
session.setCurrentProject({
|
|
428
|
-
id: project.id,
|
|
429
|
-
name: project.name,
|
|
430
|
-
slug: project.slug || project.id
|
|
431
|
-
});
|
|
432
|
-
console.log(`${BRAND.dim}Switched to project ${project.name}.${BRAND.reset}`);
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
console.log();
|
|
436
|
-
} catch (error) {
|
|
437
|
-
console.log(`\n${BRAND.red}Error updating invitation: ${error.message}${BRAND.reset}\n`);
|
|
438
|
-
process.exit(1);
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
async function updateMemberRole(args) {
|
|
443
|
-
ensureAuthenticated();
|
|
444
|
-
const { flags, positional } = parseFlags(args);
|
|
445
|
-
const userId = positional[0];
|
|
446
|
-
const role = typeof flags.role === 'string' ? flags.role : '';
|
|
447
|
-
if (!userId || !role) {
|
|
448
|
-
console.log(`${BRAND.red}userId and --role are required.${BRAND.reset}`);
|
|
449
|
-
console.log(`${BRAND.dim}Usage: bootspring project update-member <user-id> --role <owner|admin|member|viewer>${BRAND.reset}\n`);
|
|
450
|
-
process.exit(1);
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
const projectId = resolveProjectId(flags, typeof flags.project === 'string' ? flags.project : undefined);
|
|
454
|
-
try {
|
|
455
|
-
await apiRequest('PATCH', `/projects/${encodeURIComponent(projectId)}/members/${encodeURIComponent(userId)}`, { role });
|
|
456
|
-
trackCollaborationEvent('project_member_role_updated', {
|
|
457
|
-
projectId,
|
|
458
|
-
targetUserId: userId,
|
|
459
|
-
role,
|
|
460
|
-
actor: 'cli'
|
|
461
|
-
});
|
|
462
|
-
console.log(`\n${BRAND.green}✓${BRAND.reset} Updated role for ${userId} to ${role}.\n`);
|
|
463
|
-
} catch (error) {
|
|
464
|
-
console.log(`\n${BRAND.red}Error updating member role: ${error.message}${BRAND.reset}\n`);
|
|
465
|
-
process.exit(1);
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
async function removeMember(args) {
|
|
470
|
-
ensureAuthenticated();
|
|
471
|
-
const { flags, positional } = parseFlags(args);
|
|
472
|
-
const userId = positional[0];
|
|
473
|
-
if (!userId) {
|
|
474
|
-
console.log(`${BRAND.red}userId is required.${BRAND.reset}`);
|
|
475
|
-
console.log(`${BRAND.dim}Usage: bootspring project remove-member <user-id> [--project <id>]${BRAND.reset}\n`);
|
|
476
|
-
process.exit(1);
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
const projectId = resolveProjectId(flags, typeof flags.project === 'string' ? flags.project : undefined);
|
|
480
|
-
try {
|
|
481
|
-
await apiRequest('DELETE', `/projects/${encodeURIComponent(projectId)}/members/${encodeURIComponent(userId)}`);
|
|
482
|
-
trackCollaborationEvent('project_member_removed', {
|
|
483
|
-
projectId,
|
|
484
|
-
targetUserId: userId,
|
|
485
|
-
actor: 'cli'
|
|
486
|
-
});
|
|
487
|
-
console.log(`\n${BRAND.green}✓${BRAND.reset} Removed member ${userId}.\n`);
|
|
488
|
-
} catch (error) {
|
|
489
|
-
console.log(`\n${BRAND.red}Error removing member: ${error.message}${BRAND.reset}\n`);
|
|
490
|
-
process.exit(1);
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
async function transferOwnership(args) {
|
|
495
|
-
ensureAuthenticated();
|
|
496
|
-
const { flags, positional } = parseFlags(args);
|
|
497
|
-
const newOwnerId = positional[0];
|
|
498
|
-
if (!newOwnerId) {
|
|
499
|
-
console.log(`${BRAND.red}newOwnerId is required.${BRAND.reset}`);
|
|
500
|
-
console.log(`${BRAND.dim}Usage: bootspring project transfer <new-owner-user-id> [--project <id>]${BRAND.reset}\n`);
|
|
501
|
-
process.exit(1);
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
const projectId = resolveProjectId(flags, typeof flags.project === 'string' ? flags.project : undefined);
|
|
505
|
-
try {
|
|
506
|
-
await apiRequest('POST', `/projects/${encodeURIComponent(projectId)}/transfer`, { newOwnerId });
|
|
507
|
-
trackCollaborationEvent('project_owner_transferred', {
|
|
508
|
-
projectId,
|
|
509
|
-
newOwnerId,
|
|
510
|
-
actor: 'cli'
|
|
511
|
-
});
|
|
512
|
-
console.log(`\n${BRAND.green}✓${BRAND.reset} Transferred ownership to ${newOwnerId}.\n`);
|
|
513
|
-
} catch (error) {
|
|
514
|
-
console.log(`\n${BRAND.red}Error transferring ownership: ${error.message}${BRAND.reset}\n`);
|
|
515
|
-
process.exit(1);
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
async function showActivity(args) {
|
|
520
|
-
ensureAuthenticated();
|
|
521
|
-
const { flags, positional } = parseFlags(args);
|
|
522
|
-
const projectId = resolveProjectId(flags, positional[0]);
|
|
523
|
-
const limit = Number(flags.limit);
|
|
524
|
-
const effectiveLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, 100) : 20;
|
|
525
|
-
|
|
526
|
-
let entries = [];
|
|
527
|
-
try {
|
|
528
|
-
const response = await apiRequest('GET', `/projects/${encodeURIComponent(projectId)}/activity?limit=${effectiveLimit}`);
|
|
529
|
-
entries = Array.isArray(response) ? response : (response.events || response.activities || []);
|
|
530
|
-
} catch {
|
|
531
|
-
entries = projectActivity.getProjectActivityFeed({ projectId, limit: effectiveLimit });
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
console.log(`\n${BRAND.bold}Project Activity${BRAND.reset} ${BRAND.dim}(${projectId})${BRAND.reset}\n`);
|
|
535
|
-
if (entries.length === 0) {
|
|
536
|
-
console.log(`${BRAND.dim}No activity found.${BRAND.reset}\n`);
|
|
537
|
-
return;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
for (const entry of entries) {
|
|
541
|
-
const actor = entry.actor ? `${entry.actor}: ` : '';
|
|
542
|
-
console.log(` ${BRAND.dim}${formatTimestamp(entry.timestamp)}${BRAND.reset} ${actor}${entry.message}`);
|
|
543
|
-
}
|
|
544
|
-
console.log();
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
async function showNotifications(args) {
|
|
548
|
-
ensureAuthenticated();
|
|
549
|
-
const { flags, positional } = parseFlags(args);
|
|
550
|
-
const projectId = resolveProjectId(flags, positional[0]);
|
|
551
|
-
const limit = Number(flags.limit);
|
|
552
|
-
const notifications = projectActivity.getMembershipNotifications({
|
|
553
|
-
projectId,
|
|
554
|
-
limit: Number.isFinite(limit) && limit > 0 ? Math.min(limit, 100) : 20
|
|
555
|
-
});
|
|
556
|
-
|
|
557
|
-
console.log(`\n${BRAND.bold}Membership Notifications${BRAND.reset} ${BRAND.dim}(${projectId})${BRAND.reset}\n`);
|
|
558
|
-
if (notifications.length === 0) {
|
|
559
|
-
console.log(`${BRAND.dim}No membership notifications yet.${BRAND.reset}\n`);
|
|
560
|
-
return;
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
for (const notification of notifications) {
|
|
564
|
-
console.log(` ${BRAND.dim}${formatTimestamp(notification.timestamp)}${BRAND.reset} ${notification.message}`);
|
|
565
|
-
}
|
|
566
|
-
console.log();
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
/**
|
|
570
|
-
* Create a new project
|
|
571
|
-
*/
|
|
572
|
-
async function createProject(args) {
|
|
573
|
-
if (!auth.isAuthenticated()) {
|
|
574
|
-
console.log(`${BRAND.red}Not logged in.${BRAND.reset} Run: bootspring auth login`);
|
|
575
|
-
process.exit(1);
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
const { flags, positional } = parseFlags(args);
|
|
579
|
-
const isNonInteractive = flags.y || flags.yes || !process.stdin.isTTY;
|
|
580
|
-
const skipDuplicateCheck = flags['skip-duplicate-check'] || flags.force;
|
|
581
|
-
|
|
582
|
-
console.log(`\n${BRAND.bold}Create New Project${BRAND.reset}\n`);
|
|
583
|
-
|
|
584
|
-
// Get project name from args or prompt
|
|
585
|
-
let name = positional[0] || flags.name;
|
|
586
|
-
if (!name && !isNonInteractive) {
|
|
587
|
-
name = await prompt(`${BRAND.color}Project name:${BRAND.reset} `);
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
if (!name || name.trim().length === 0) {
|
|
591
|
-
console.log(`${BRAND.red}Project name is required.${BRAND.reset}`);
|
|
592
|
-
console.log(`\n${BRAND.dim}Usage: bootspring project create <name> [--description "..."] [--framework nodejs] [-y]${BRAND.reset}\n`);
|
|
593
|
-
process.exit(1);
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
// Get repository URL from flag
|
|
597
|
-
const repoUrl = flags.repo || flags.repository;
|
|
598
|
-
|
|
599
|
-
// Check for similar projects (duplicate detection)
|
|
600
|
-
if (!skipDuplicateCheck && !isNonInteractive) {
|
|
601
|
-
console.log(`${BRAND.dim}Checking for similar projects...${BRAND.reset}`);
|
|
602
|
-
const similar = await checkSimilarProjects(name.trim(), repoUrl);
|
|
603
|
-
|
|
604
|
-
if (similar.length > 0) {
|
|
605
|
-
console.log(`\n${BRAND.yellow}Similar project(s) found:${BRAND.reset}\n`);
|
|
606
|
-
|
|
607
|
-
for (const match of similar) {
|
|
608
|
-
const reasonMap = {
|
|
609
|
-
github_repo: 'GitHub repo match',
|
|
610
|
-
slug_match: 'Same slug',
|
|
611
|
-
name_similar: 'Similar name'
|
|
612
|
-
};
|
|
613
|
-
const reason = reasonMap[match.matchReason] || match.matchReason;
|
|
614
|
-
const ownerInfo = match.owner ? ` (owned by ${match.owner.email})` : '';
|
|
615
|
-
console.log(` ${BRAND.color}${match.name}${BRAND.reset}${ownerInfo}`);
|
|
616
|
-
console.log(` ${BRAND.dim}[${reason}]${BRAND.reset}\n`);
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
const proceed = await prompt(`${BRAND.color}Create a new project anyway? (y/N):${BRAND.reset} `);
|
|
620
|
-
if (proceed.toLowerCase() !== 'y') {
|
|
621
|
-
console.log(`\n${BRAND.dim}To join an existing project, ask the owner to add you.${BRAND.reset}\n`);
|
|
622
|
-
return;
|
|
623
|
-
}
|
|
624
|
-
console.log();
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
// Get description from flag or prompt
|
|
629
|
-
let description = flags.description || flags.d;
|
|
630
|
-
if (!description && !isNonInteractive) {
|
|
631
|
-
description = await prompt(`${BRAND.dim}Description (optional):${BRAND.reset} `);
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
// Get framework from flag or prompt
|
|
635
|
-
let framework = flags.framework || flags.f;
|
|
636
|
-
if (!framework && !isNonInteractive) {
|
|
637
|
-
framework = await prompt(`${BRAND.dim}Framework (e.g., nextjs, react, nodejs):${BRAND.reset} `);
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
console.log(`\n${BRAND.dim}Creating project...${BRAND.reset}`);
|
|
641
|
-
|
|
642
|
-
try {
|
|
643
|
-
const project = await apiRequest('POST', '/projects', {
|
|
644
|
-
name: name.trim(),
|
|
645
|
-
description: description || undefined,
|
|
646
|
-
framework: framework || undefined,
|
|
647
|
-
repositoryUrl: repoUrl || undefined
|
|
648
|
-
});
|
|
649
|
-
|
|
650
|
-
console.log(`\n${BRAND.green}✓${BRAND.reset} Project created successfully!\n`);
|
|
651
|
-
console.log(` ${BRAND.bold}Name:${BRAND.reset} ${project.name}`);
|
|
652
|
-
console.log(` ${BRAND.bold}ID:${BRAND.reset} ${project.id}`);
|
|
653
|
-
console.log(` ${BRAND.bold}Slug:${BRAND.reset} ${project.slug}`);
|
|
654
|
-
|
|
655
|
-
// Ask if user wants to switch to this project (auto-yes in non-interactive mode)
|
|
656
|
-
let switchTo = 'y';
|
|
657
|
-
if (!isNonInteractive) {
|
|
658
|
-
switchTo = await prompt(`\n${BRAND.color}Switch to this project now? (Y/n):${BRAND.reset} `);
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
if (switchTo.toLowerCase() !== 'n') {
|
|
662
|
-
session.setCurrentProject({
|
|
663
|
-
id: project.id,
|
|
664
|
-
name: project.name,
|
|
665
|
-
slug: project.slug
|
|
666
|
-
});
|
|
667
|
-
console.log(`\n${BRAND.green}✓${BRAND.reset} Switched to ${BRAND.color}${project.name}${BRAND.reset}\n`);
|
|
668
|
-
} else {
|
|
669
|
-
console.log(`\n${BRAND.dim}Run 'bootspring switch ${project.slug}' to switch to this project.${BRAND.reset}\n`);
|
|
670
|
-
}
|
|
671
|
-
} catch (error) {
|
|
672
|
-
if (error.status === 403 && error.message.includes('limit')) {
|
|
673
|
-
console.log(`\n${BRAND.yellow}Project limit reached.${BRAND.reset}`);
|
|
674
|
-
console.log(`${BRAND.dim}Upgrade your plan at: https://bootspring.com/dashboard/billing${BRAND.reset}\n`);
|
|
675
|
-
} else {
|
|
676
|
-
console.log(`\n${BRAND.red}Error: ${error.message}${BRAND.reset}`);
|
|
677
|
-
}
|
|
678
|
-
process.exit(1);
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
/**
|
|
683
|
-
* Show current project info
|
|
684
|
-
*/
|
|
685
|
-
async function showProjectInfo() {
|
|
686
|
-
const project = session.getEffectiveProject();
|
|
687
|
-
|
|
688
|
-
if (!project) {
|
|
689
|
-
console.log(`\n${BRAND.yellow}No project context set.${BRAND.reset}`);
|
|
690
|
-
console.log(`${BRAND.dim}Run 'bootspring switch' to select a project.${BRAND.reset}\n`);
|
|
691
|
-
return;
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
console.log(`\n${BRAND.bold}Current Project${BRAND.reset}\n`);
|
|
695
|
-
console.log(` ${BRAND.bold}Name:${BRAND.reset} ${BRAND.color}${project.name}${BRAND.reset}`);
|
|
696
|
-
console.log(` ${BRAND.bold}ID:${BRAND.reset} ${project.id}`);
|
|
697
|
-
if (project.slug) {
|
|
698
|
-
console.log(` ${BRAND.bold}Slug:${BRAND.reset} ${project.slug}`);
|
|
699
|
-
}
|
|
700
|
-
console.log(` ${BRAND.bold}Source:${BRAND.reset} ${project.source || 'session'}`);
|
|
701
|
-
console.log();
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
/**
|
|
705
|
-
* Show usage for this command
|
|
706
|
-
*/
|
|
707
|
-
function showUsage() {
|
|
708
|
-
console.log(`
|
|
709
|
-
${BRAND.bold}Usage:${BRAND.reset} bootspring project <command> [args]
|
|
710
|
-
|
|
711
|
-
${BRAND.bold}Commands:${BRAND.reset}
|
|
712
|
-
list List all projects (owned and shared)
|
|
713
|
-
create [name] Create a new project
|
|
714
|
-
info Show current project info
|
|
715
|
-
members [projectId] List project members
|
|
716
|
-
invite <email> [--role role] Invite a member (accept/decline via invitation id)
|
|
717
|
-
invitations [projectId] List pending invitations
|
|
718
|
-
accept <invitationId> Accept an invitation
|
|
719
|
-
decline <invitationId> Decline an invitation
|
|
720
|
-
update-member <userId> --role <r> Update member role
|
|
721
|
-
remove-member <userId> Remove a project member
|
|
722
|
-
transfer <newOwnerUserId> Transfer project ownership
|
|
723
|
-
activity [projectId] Show auditable collaboration feed
|
|
724
|
-
notifications [projectId] Show membership change notifications
|
|
725
|
-
|
|
726
|
-
${BRAND.bold}Common options:${BRAND.reset}
|
|
727
|
-
--project, -p Project id/slug override
|
|
728
|
-
--limit <n> Limit list size for activity/notifications
|
|
729
|
-
|
|
730
|
-
${BRAND.bold}Options for create:${BRAND.reset}
|
|
731
|
-
--description, -d Project description
|
|
732
|
-
--framework, -f Project framework (e.g., nextjs, nodejs)
|
|
733
|
-
--repo Repository URL
|
|
734
|
-
--force Skip duplicate project check
|
|
735
|
-
-y, --yes Non-interactive mode
|
|
736
|
-
|
|
737
|
-
${BRAND.bold}Examples:${BRAND.reset}
|
|
738
|
-
bootspring project list
|
|
739
|
-
bootspring project create "My App"
|
|
740
|
-
bootspring project create "My App" --framework nextjs --repo https://github.com/user/repo
|
|
741
|
-
bootspring project invite dev@example.com --role admin
|
|
742
|
-
bootspring project accept inv_123
|
|
743
|
-
bootspring project activity --limit 15
|
|
744
|
-
bootspring project info
|
|
745
|
-
`);
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
/**
|
|
749
|
-
* Main entry point
|
|
750
|
-
*/
|
|
751
|
-
async function run(args) {
|
|
752
|
-
const subcommand = args[0];
|
|
753
|
-
|
|
754
|
-
switch (subcommand) {
|
|
755
|
-
case 'list':
|
|
756
|
-
case 'ls':
|
|
757
|
-
await listProjects();
|
|
758
|
-
break;
|
|
759
|
-
|
|
760
|
-
case 'create':
|
|
761
|
-
case 'new':
|
|
762
|
-
await createProject(args.slice(1));
|
|
763
|
-
break;
|
|
764
|
-
|
|
765
|
-
case 'info':
|
|
766
|
-
case 'current':
|
|
767
|
-
await showProjectInfo();
|
|
768
|
-
break;
|
|
769
|
-
|
|
770
|
-
case 'members':
|
|
771
|
-
await listMembers(args.slice(1));
|
|
772
|
-
break;
|
|
773
|
-
|
|
774
|
-
case 'invite':
|
|
775
|
-
await inviteMember(args.slice(1));
|
|
776
|
-
break;
|
|
777
|
-
|
|
778
|
-
case 'invitations':
|
|
779
|
-
case 'invites':
|
|
780
|
-
await listInvitations(args.slice(1));
|
|
781
|
-
break;
|
|
782
|
-
|
|
783
|
-
case 'accept':
|
|
784
|
-
await respondToInvitation(args.slice(1), 'accept');
|
|
785
|
-
break;
|
|
786
|
-
|
|
787
|
-
case 'decline':
|
|
788
|
-
await respondToInvitation(args.slice(1), 'decline');
|
|
789
|
-
break;
|
|
790
|
-
|
|
791
|
-
case 'update-member':
|
|
792
|
-
await updateMemberRole(args.slice(1));
|
|
793
|
-
break;
|
|
794
|
-
|
|
795
|
-
case 'remove-member':
|
|
796
|
-
await removeMember(args.slice(1));
|
|
797
|
-
break;
|
|
798
|
-
|
|
799
|
-
case 'transfer':
|
|
800
|
-
await transferOwnership(args.slice(1));
|
|
801
|
-
break;
|
|
802
|
-
|
|
803
|
-
case 'activity':
|
|
804
|
-
await showActivity(args.slice(1));
|
|
805
|
-
break;
|
|
806
|
-
|
|
807
|
-
case 'notifications':
|
|
808
|
-
await showNotifications(args.slice(1));
|
|
809
|
-
break;
|
|
810
|
-
|
|
811
|
-
case undefined:
|
|
812
|
-
case 'help':
|
|
813
|
-
case '--help':
|
|
814
|
-
case '-h':
|
|
815
|
-
showUsage();
|
|
816
|
-
break;
|
|
817
|
-
|
|
818
|
-
default:
|
|
819
|
-
console.log(`${BRAND.red}Unknown subcommand: ${subcommand}${BRAND.reset}`);
|
|
820
|
-
showUsage();
|
|
821
|
-
process.exit(1);
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
module.exports = { run };
|