@girardmedia/bootspring 2.1.3 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/bin/bootspring.js +157 -83
  2. package/claude-commands/agent.md +34 -0
  3. package/claude-commands/bs.md +31 -0
  4. package/claude-commands/build.md +25 -0
  5. package/claude-commands/skill.md +31 -0
  6. package/claude-commands/todo.md +25 -0
  7. package/dist/core/index.d.ts +5814 -0
  8. package/dist/core.js +5779 -0
  9. package/dist/index.js +93883 -0
  10. package/dist/mcp/index.d.ts +1 -0
  11. package/dist/mcp-server.js +2298 -0
  12. package/generators/api-docs.js +3 -3
  13. package/generators/decisions.js +14 -14
  14. package/generators/health.js +6 -6
  15. package/generators/sprint.js +4 -4
  16. package/generators/templates/build-planning.template.js +2 -2
  17. package/generators/visual-doc-generator.js +1 -1
  18. package/package.json +22 -68
  19. package/cli/agent.js +0 -799
  20. package/cli/auth.js +0 -896
  21. package/cli/billing.js +0 -320
  22. package/cli/build.js +0 -1442
  23. package/cli/dashboard.js +0 -123
  24. package/cli/init.js +0 -669
  25. package/cli/mcp.js +0 -240
  26. package/cli/orchestrator.js +0 -240
  27. package/cli/project.js +0 -825
  28. package/cli/quality.js +0 -281
  29. package/cli/skill.js +0 -503
  30. package/cli/switch.js +0 -453
  31. package/cli/todo.js +0 -629
  32. package/cli/update.js +0 -132
  33. package/core/api-client.d.ts +0 -69
  34. package/core/api-client.js +0 -1482
  35. package/core/auth.d.ts +0 -98
  36. package/core/auth.js +0 -737
  37. package/core/build-orchestrator.js +0 -508
  38. package/core/build-state.js +0 -612
  39. package/core/config.d.ts +0 -106
  40. package/core/config.js +0 -1328
  41. package/core/context-loader.js +0 -580
  42. package/core/context.d.ts +0 -61
  43. package/core/context.js +0 -327
  44. package/core/entitlements.d.ts +0 -70
  45. package/core/entitlements.js +0 -322
  46. package/core/index.d.ts +0 -53
  47. package/core/index.js +0 -62
  48. package/core/mcp-config.js +0 -115
  49. package/core/policies.d.ts +0 -43
  50. package/core/policies.js +0 -113
  51. package/core/policy-matrix.js +0 -303
  52. package/core/project-activity.js +0 -175
  53. package/core/redaction.d.ts +0 -5
  54. package/core/redaction.js +0 -63
  55. package/core/self-update.js +0 -259
  56. package/core/session.js +0 -353
  57. package/core/task-extractor.js +0 -1098
  58. package/core/telemetry.d.ts +0 -55
  59. package/core/telemetry.js +0 -617
  60. package/core/tier-enforcement.js +0 -928
  61. package/core/utils.d.ts +0 -90
  62. package/core/utils.js +0 -455
  63. package/core/validation.js +0 -572
  64. package/mcp/server.d.ts +0 -57
  65. package/mcp/server.js +0 -264
@@ -1,175 +0,0 @@
1
- /**
2
- * Project collaboration activity + notification helpers.
3
- *
4
- * Uses telemetry JSONL as an append-only audit source.
5
- */
6
-
7
- const crypto = require('crypto');
8
-
9
- const NOTIFICATION_EVENT = 'project_membership_notification';
10
- const DEFAULT_ACTIVITY_LIMIT = 25;
11
- const MAX_ACTIVITY_SCAN = 500;
12
- const KNOWN_ACTIVITY_EVENTS = new Set([
13
- NOTIFICATION_EVENT,
14
- 'project_invitation_sent',
15
- 'project_invitation_accepted',
16
- 'project_invitation_declined',
17
- 'project_member_added',
18
- 'project_member_removed',
19
- 'project_member_role_updated',
20
- 'project_owner_transferred'
21
- ]);
22
-
23
- function getTelemetry() {
24
- return require('./telemetry');
25
- }
26
-
27
- function sanitizeToken(value, fallback) {
28
- const normalized = String(value || '')
29
- .trim()
30
- .toLowerCase()
31
- .replace(/[^a-z0-9_-]+/g, '-')
32
- .replace(/^-+|-+$/g, '');
33
- return normalized || fallback;
34
- }
35
-
36
- function projectIdFromPayload(payload) {
37
- return String(payload.projectId || payload.project || payload.projectSlug || '').trim();
38
- }
39
-
40
- function projectScopeFromProjectId(projectId) {
41
- if (!projectId) return '';
42
- return `ps_${crypto.createHash('sha256').update(projectId).digest('hex').slice(0, 16)}`;
43
- }
44
-
45
- function projectScopeFromPayload(payload) {
46
- return String(payload.projectScope || payload.project_scope || '').trim();
47
- }
48
-
49
- function describeEvent(event, payload = {}) {
50
- const email = String(payload.email || payload.targetEmail || payload.target || '').trim();
51
- const role = String(payload.role || '').trim();
52
- const target = String(payload.targetUserId || payload.userId || '').trim();
53
-
54
- if (event === 'project_invitation_sent') {
55
- return email ? `Invitation sent to ${email}${role ? ` (${role})` : ''}` : 'Invitation sent';
56
- }
57
- if (event === 'project_invitation_accepted') {
58
- return email ? `${email} accepted invitation` : 'Invitation accepted';
59
- }
60
- if (event === 'project_invitation_declined') {
61
- return email ? `${email} declined invitation` : 'Invitation declined';
62
- }
63
- if (event === 'project_member_added') {
64
- return email ? `Member added: ${email}${role ? ` (${role})` : ''}` : 'Member added';
65
- }
66
- if (event === 'project_member_removed') {
67
- return target ? `Member removed: ${target}` : 'Member removed';
68
- }
69
- if (event === 'project_member_role_updated') {
70
- return target ? `Member role updated: ${target}${role ? ` -> ${role}` : ''}` : 'Member role updated';
71
- }
72
- if (event === 'project_owner_transferred') {
73
- const owner = String(payload.newOwnerId || target || '').trim();
74
- return owner ? `Ownership transferred to ${owner}` : 'Ownership transferred';
75
- }
76
- return String(payload.message || event).trim();
77
- }
78
-
79
- function toActivityEntry(record) {
80
- const payload = record.payload || {};
81
- const projectId = projectIdFromPayload(payload);
82
- return {
83
- timestamp: String(record.timestamp || ''),
84
- projectId,
85
- projectScope: projectScopeFromPayload(payload),
86
- event: String(record.event || ''),
87
- action: sanitizeToken(payload.action || record.event, 'project_activity'),
88
- actor: String(payload.actor || payload.actorId || 'system'),
89
- target: String(payload.target || payload.email || payload.targetUserId || ''),
90
- message: describeEvent(String(record.event || ''), payload),
91
- metadata: payload
92
- };
93
- }
94
-
95
- function trackProjectActivity(event, payload = {}, options = {}) {
96
- const normalizedEvent = sanitizeToken(event, 'project_activity');
97
- const projectScope = projectScopeFromProjectId(projectIdFromPayload(payload));
98
- const telemetry = getTelemetry();
99
- telemetry.emitEvent(normalizedEvent, {
100
- ...payload,
101
- ...(projectScope ? { projectScope } : {}),
102
- action: normalizedEvent,
103
- occurredAt: options.now ? new Date(options.now).toISOString() : new Date().toISOString()
104
- }, options);
105
- }
106
-
107
- function trackMembershipNotification(payload = {}, options = {}) {
108
- const event = String(payload.event || '').trim();
109
- const type = sanitizeToken(payload.type || event || 'membership_update', 'membership_update');
110
- const message = String(payload.message || describeEvent(event || type, payload)).trim() || 'Membership updated';
111
- const projectScope = projectScopeFromProjectId(projectIdFromPayload(payload));
112
-
113
- const telemetry = getTelemetry();
114
- telemetry.emitEvent(NOTIFICATION_EVENT, {
115
- ...payload,
116
- ...(projectScope ? { projectScope } : {}),
117
- type,
118
- message,
119
- occurredAt: options.now ? new Date(options.now).toISOString() : new Date().toISOString()
120
- }, options);
121
- }
122
-
123
- function getProjectActivityFeed(options = {}) {
124
- const telemetry = getTelemetry();
125
- const limit = Number(options.limit);
126
- const effectiveLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, MAX_ACTIVITY_SCAN) : DEFAULT_ACTIVITY_LIMIT;
127
-
128
- const records = telemetry.listEvents({
129
- projectRoot: options.projectRoot,
130
- limit: MAX_ACTIVITY_SCAN
131
- });
132
-
133
- const projectId = String(options.projectId || '').trim();
134
- const projectScope = projectScopeFromProjectId(projectId);
135
- return records
136
- .filter(record => KNOWN_ACTIVITY_EVENTS.has(String(record.event || '')))
137
- .map(toActivityEntry)
138
- .filter(entry => {
139
- if (!projectId) return true;
140
- if (entry.projectId === projectId) return true;
141
- return !!projectScope && entry.projectScope === projectScope;
142
- })
143
- .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
144
- .slice(0, effectiveLimit);
145
- }
146
-
147
- function getMembershipNotifications(options = {}) {
148
- const limit = Number(options.limit);
149
- const effectiveLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, MAX_ACTIVITY_SCAN) : DEFAULT_ACTIVITY_LIMIT;
150
-
151
- return getProjectActivityFeed({
152
- projectId: options.projectId,
153
- limit: MAX_ACTIVITY_SCAN,
154
- projectRoot: options.projectRoot
155
- })
156
- .filter(entry => entry.event === NOTIFICATION_EVENT || entry.event.startsWith('project_invitation_') || entry.event.startsWith('project_member_') || entry.event === 'project_owner_transferred')
157
- .map(entry => ({
158
- timestamp: entry.timestamp,
159
- projectId: entry.projectId,
160
- event: entry.event,
161
- type: sanitizeToken(entry.metadata.type || entry.action, 'membership_update'),
162
- message: String(entry.metadata.message || entry.message || 'Membership updated'),
163
- actor: entry.actor,
164
- target: entry.target,
165
- metadata: entry.metadata
166
- }))
167
- .slice(0, effectiveLimit);
168
- }
169
-
170
- module.exports = {
171
- trackProjectActivity,
172
- trackMembershipNotification,
173
- getProjectActivityFeed,
174
- getMembershipNotifications
175
- };
@@ -1,5 +0,0 @@
1
- export const REDACTED: '[REDACTED]';
2
-
3
- export function redactSensitiveString(value: string): string;
4
- export function redactSensitiveData<T>(input: T, depth?: number): T;
5
- export function redactErrorMessage(error: unknown): string;
package/core/redaction.js DELETED
@@ -1,63 +0,0 @@
1
- /**
2
- * Centralized sensitive-data redaction helpers for runtime JS modules.
3
- */
4
-
5
- const REDACTED = '[REDACTED]';
6
-
7
- const SENSITIVE_KEY_PATTERN = /(?:^|[_-])(api[_-]?key|token|refresh[_-]?token|authorization|x[_-]?api[_-]?key|project[_-]?id)$/i;
8
-
9
- function redactPatternMatches(value) {
10
- return value
11
- .replace(/\b(?:bs|sk)_(?:live|test)_[A-Za-z0-9_-]{8,}\b/g, REDACTED)
12
- .replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, REDACTED)
13
- .replace(/\bBearer\s+[A-Za-z0-9._-]+\b/gi, `Bearer ${REDACTED}`)
14
- .replace(/\bproj_[A-Za-z0-9_-]{6,}\b/g, `proj_${REDACTED}`)
15
- .replace(/(["']?(?:authorization|x-api-key|apiKey|token|refreshToken|projectId)["']?\s*[:=]\s*["']?)([^"',\s}]+)/gi, `$1${REDACTED}`);
16
- }
17
-
18
- function redactSensitiveString(value) {
19
- return redactPatternMatches(String(value || ''));
20
- }
21
-
22
- function redactSensitiveData(input, depth = 0) {
23
- if (depth > 10) {
24
- return input;
25
- }
26
-
27
- if (typeof input === 'string') {
28
- return redactSensitiveString(input);
29
- }
30
-
31
- if (Array.isArray(input)) {
32
- return input.map(item => redactSensitiveData(item, depth + 1));
33
- }
34
-
35
- if (!input || typeof input !== 'object') {
36
- return input;
37
- }
38
-
39
- const output = {};
40
- for (const [key, value] of Object.entries(input)) {
41
- if (SENSITIVE_KEY_PATTERN.test(key)) {
42
- output[key] = REDACTED;
43
- continue;
44
- }
45
- output[key] = redactSensitiveData(value, depth + 1);
46
- }
47
- return output;
48
- }
49
-
50
- function redactErrorMessage(error) {
51
- if (!error) return '';
52
- if (error instanceof Error) {
53
- return redactSensitiveString(error.message || String(error));
54
- }
55
- return redactSensitiveString(String(error));
56
- }
57
-
58
- module.exports = {
59
- REDACTED,
60
- redactSensitiveString,
61
- redactSensitiveData,
62
- redactErrorMessage
63
- };
@@ -1,259 +0,0 @@
1
- const fs = require('fs');
2
- const os = require('os');
3
- const path = require('path');
4
- const { execFileSync, spawnSync } = require('child_process');
5
- const { PACKAGE_NAME } = require('./mcp-config');
6
-
7
- const pkg = require('../package.json');
8
- const CURRENT_VERSION = pkg.version || '0.0.0';
9
- const DEFAULT_INTERVAL_MS = Number.parseInt(
10
- process.env.BOOTSPRING_AUTO_UPDATE_INTERVAL_MS || `${6 * 60 * 60 * 1000}`,
11
- 10
12
- );
13
- const STATE_PATH = path.join(os.homedir(), '.bootspring', 'update-state.json');
14
-
15
- function getNpmCommand() {
16
- if (process.env.BOOTSPRING_NPM_COMMAND) {
17
- return process.env.BOOTSPRING_NPM_COMMAND;
18
- }
19
- return process.platform === 'win32' ? 'npm.cmd' : 'npm';
20
- }
21
-
22
- function compareVersions(a, b) {
23
- const aParts = String(a || '0.0.0').split('.').map((part) => Number.parseInt(part, 10) || 0);
24
- const bParts = String(b || '0.0.0').split('.').map((part) => Number.parseInt(part, 10) || 0);
25
-
26
- for (let index = 0; index < 3; index += 1) {
27
- if ((aParts[index] || 0) < (bParts[index] || 0)) {
28
- return -1;
29
- }
30
- if ((aParts[index] || 0) > (bParts[index] || 0)) {
31
- return 1;
32
- }
33
- }
34
-
35
- return 0;
36
- }
37
-
38
- function readState() {
39
- try {
40
- return JSON.parse(fs.readFileSync(STATE_PATH, 'utf8'));
41
- } catch {
42
- return {};
43
- }
44
- }
45
-
46
- function writeState(nextState) {
47
- try {
48
- fs.mkdirSync(path.dirname(STATE_PATH), { recursive: true, mode: 0o700 });
49
- fs.writeFileSync(STATE_PATH, JSON.stringify(nextState, null, 2));
50
- } catch {
51
- // Best-effort cache only.
52
- }
53
- }
54
-
55
- function getInstallContext() {
56
- const packageRoot = path.resolve(__dirname, '..');
57
- const scriptPath = path.resolve(process.argv[1] || path.join(packageRoot, 'bin', 'bootspring.js'));
58
- const nodeModulesSegment = `${path.sep}node_modules${path.sep}`;
59
- const forcedMode = process.env.BOOTSPRING_AUTO_UPDATE_INSTALL_MODE;
60
-
61
- if (forcedMode === 'global' || forcedMode === 'local') {
62
- const projectRoot = forcedMode === 'local'
63
- ? process.env.BOOTSPRING_AUTO_UPDATE_PROJECT_ROOT || process.cwd()
64
- : null;
65
- return { mode: forcedMode, packageRoot, projectRoot, scriptPath };
66
- }
67
-
68
- if (packageRoot.includes(`${path.sep}_npx${path.sep}`) || scriptPath.includes(`${path.sep}_npx${path.sep}`)) {
69
- return { mode: 'ephemeral', packageRoot, projectRoot: null, scriptPath };
70
- }
71
-
72
- if (!packageRoot.includes(nodeModulesSegment)) {
73
- return { mode: 'development', packageRoot, projectRoot: null, scriptPath };
74
- }
75
-
76
- if (
77
- scriptPath.includes(`${nodeModulesSegment}.bin${path.sep}`) ||
78
- scriptPath.includes(`${nodeModulesSegment}@girardmedia${path.sep}bootspring${path.sep}bin${path.sep}`)
79
- ) {
80
- const [projectRoot] = packageRoot.split(nodeModulesSegment);
81
- return { mode: 'local', packageRoot, projectRoot: projectRoot || process.cwd(), scriptPath };
82
- }
83
-
84
- return { mode: 'global', packageRoot, projectRoot: null, scriptPath };
85
- }
86
-
87
- function shouldSkipAutoUpdate(args = []) {
88
- if (
89
- process.env.BOOTSPRING_SKIP_AUTO_UPDATE === 'true' ||
90
- process.env.BOOTSPRING_AUTO_UPDATE_APPLIED === 'true' ||
91
- process.env.NODE_ENV === 'test' ||
92
- process.env.CI
93
- ) {
94
- return true;
95
- }
96
-
97
- const tokens = Array.isArray(args) ? args.filter(Boolean) : [];
98
- if (tokens.length === 0) {
99
- return true;
100
- }
101
-
102
- if (
103
- tokens[0] === 'help' ||
104
- tokens[0] === 'update' ||
105
- tokens[0] === '--version' ||
106
- tokens[0] === '-v' ||
107
- tokens.includes('--help') ||
108
- tokens.includes('-h')
109
- ) {
110
- return true;
111
- }
112
-
113
- const context = getInstallContext();
114
- if (context.mode === 'ephemeral') {
115
- return true;
116
- }
117
-
118
- if (context.mode === 'development' && process.env.BOOTSPRING_ALLOW_DEV_AUTO_UPDATE !== 'true') {
119
- return true;
120
- }
121
-
122
- return false;
123
- }
124
-
125
- function getLatestVersion() {
126
- try {
127
- const output = execFileSync(
128
- getNpmCommand(),
129
- ['view', PACKAGE_NAME, 'version', '--json'],
130
- {
131
- encoding: 'utf8',
132
- stdio: ['ignore', 'pipe', 'pipe'],
133
- timeout: 10000,
134
- env: {
135
- ...process.env,
136
- npm_config_update_notifier: 'false'
137
- }
138
- }
139
- ).trim();
140
-
141
- if (!output) {
142
- return null;
143
- }
144
-
145
- return JSON.parse(output);
146
- } catch {
147
- return null;
148
- }
149
- }
150
-
151
- function applyUpdate(context) {
152
- const args = context.mode === 'local'
153
- ? ['install', `${PACKAGE_NAME}@latest`]
154
- : ['install', '-g', `${PACKAGE_NAME}@latest`];
155
-
156
- execFileSync(getNpmCommand(), args, {
157
- cwd: context.projectRoot || process.cwd(),
158
- encoding: 'utf8',
159
- stdio: ['ignore', 'pipe', 'pipe'],
160
- timeout: 120000,
161
- env: {
162
- ...process.env,
163
- npm_config_update_notifier: 'false'
164
- }
165
- });
166
- }
167
-
168
- function relaunch(args = []) {
169
- const result = spawnSync(process.execPath, [process.argv[1], ...args], {
170
- stdio: 'inherit',
171
- env: {
172
- ...process.env,
173
- BOOTSPRING_AUTO_UPDATE_APPLIED: 'true'
174
- }
175
- });
176
-
177
- if (result.error) {
178
- throw result.error;
179
- }
180
-
181
- return typeof result.status === 'number' ? result.status : 1;
182
- }
183
-
184
- function checkForUpdates(options = {}) {
185
- const latestVersion = getLatestVersion();
186
- const currentVersion = options.currentVersion || CURRENT_VERSION;
187
- return {
188
- current: currentVersion,
189
- latest: latestVersion,
190
- updateAvailable: Boolean(latestVersion) && compareVersions(currentVersion, latestVersion) < 0
191
- };
192
- }
193
-
194
- function ensureLatestVersion(args = []) {
195
- if (shouldSkipAutoUpdate(args)) {
196
- return { updated: false, skipped: true };
197
- }
198
-
199
- const context = getInstallContext();
200
- const state = readState();
201
- const lastCheckedAt = Date.parse(state.lastCheckedAt || '');
202
- const now = Date.now();
203
- const cacheFresh = Number.isFinite(lastCheckedAt) && now - lastCheckedAt < DEFAULT_INTERVAL_MS;
204
-
205
- let latestVersion = state.latestVersion || null;
206
- if (!cacheFresh || state.currentVersion !== CURRENT_VERSION) {
207
- latestVersion = getLatestVersion();
208
- writeState({
209
- ...state,
210
- currentVersion: CURRENT_VERSION,
211
- latestVersion,
212
- lastCheckedAt: new Date(now).toISOString()
213
- });
214
- }
215
-
216
- if (!latestVersion || compareVersions(CURRENT_VERSION, latestVersion) >= 0) {
217
- return { updated: false, skipped: false, current: CURRENT_VERSION, latest: latestVersion };
218
- }
219
-
220
- const installTarget = context.mode === 'local' ? context.projectRoot || process.cwd() : 'global install';
221
- console.error(`[bootspring] Updating ${CURRENT_VERSION} -> ${latestVersion} before continuing (${installTarget}).`);
222
-
223
- try {
224
- applyUpdate(context);
225
- writeState({
226
- ...readState(),
227
- currentVersion: latestVersion,
228
- latestVersion,
229
- lastCheckedAt: new Date(now).toISOString(),
230
- lastUpdatedAt: new Date().toISOString()
231
- });
232
- return {
233
- updated: true,
234
- current: CURRENT_VERSION,
235
- latest: latestVersion,
236
- exitCode: relaunch(args)
237
- };
238
- } catch (error) {
239
- console.error(`[bootspring] Auto-update failed: ${error.message}`);
240
- return {
241
- updated: false,
242
- skipped: false,
243
- current: CURRENT_VERSION,
244
- latest: latestVersion,
245
- error
246
- };
247
- }
248
- }
249
-
250
- module.exports = {
251
- PACKAGE_NAME,
252
- CURRENT_VERSION,
253
- compareVersions,
254
- getInstallContext,
255
- getLatestVersion,
256
- checkForUpdates,
257
- ensureLatestVersion,
258
- applyUpdate
259
- };