@girardmedia/bootspring 2.2.0 → 2.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.
Files changed (50) hide show
  1. package/README.md +2 -2
  2. package/bin/bootspring.js +35 -96
  3. package/claude-commands/agent.md +34 -0
  4. package/claude-commands/bs.md +31 -0
  5. package/claude-commands/build.md +25 -0
  6. package/claude-commands/skill.md +31 -0
  7. package/claude-commands/todo.md +25 -0
  8. package/dist/cli/index.cjs +17808 -0
  9. package/dist/core/index.d.ts +5814 -0
  10. package/dist/core.js +5780 -0
  11. package/dist/mcp/index.d.ts +1 -0
  12. package/dist/mcp-server.js +2299 -0
  13. package/generators/api-docs.js +2 -2
  14. package/generators/decisions.js +3 -3
  15. package/generators/health.js +16 -16
  16. package/generators/sprint.js +2 -2
  17. package/package.json +27 -59
  18. package/core/api-client.d.ts +0 -69
  19. package/core/api-client.js +0 -1482
  20. package/core/auth.d.ts +0 -98
  21. package/core/auth.js +0 -737
  22. package/core/build-orchestrator.js +0 -508
  23. package/core/build-state.js +0 -612
  24. package/core/config.d.ts +0 -106
  25. package/core/config.js +0 -1328
  26. package/core/context-loader.js +0 -580
  27. package/core/context.d.ts +0 -61
  28. package/core/context.js +0 -327
  29. package/core/entitlements.d.ts +0 -70
  30. package/core/entitlements.js +0 -322
  31. package/core/index.d.ts +0 -53
  32. package/core/index.js +0 -62
  33. package/core/mcp-config.js +0 -115
  34. package/core/policies.d.ts +0 -43
  35. package/core/policies.js +0 -113
  36. package/core/policy-matrix.js +0 -303
  37. package/core/project-activity.js +0 -175
  38. package/core/redaction.d.ts +0 -5
  39. package/core/redaction.js +0 -63
  40. package/core/self-update.js +0 -259
  41. package/core/session.js +0 -353
  42. package/core/task-extractor.js +0 -1098
  43. package/core/telemetry.d.ts +0 -55
  44. package/core/telemetry.js +0 -617
  45. package/core/tier-enforcement.js +0 -928
  46. package/core/utils.d.ts +0 -90
  47. package/core/utils.js +0 -455
  48. package/core/validation.js +0 -572
  49. package/mcp/server.d.ts +0 -57
  50. 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
- };