@girardmedia/bootspring 2.3.7 → 2.5.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.
@@ -3214,13 +3214,15 @@ function isMCPContext() {
3214
3214
  function deepClone(obj) {
3215
3215
  return JSON.parse(JSON.stringify(obj));
3216
3216
  }
3217
- var import_fs, import_path, REDACTED, SENSITIVE_KEY_PATTERN, COLORS, print;
3217
+ var import_fs, import_path, BOOTSPRING_VERSION, BOOTSPRING_PACKAGE_NAME, REDACTED, SENSITIVE_KEY_PATTERN, COLORS, print;
3218
3218
  var init_dist = __esm({
3219
3219
  "../../packages/shared/dist/index.mjs"() {
3220
3220
  "use strict";
3221
3221
  init_cjs_shims();
3222
3222
  import_fs = __toESM(require("fs"), 1);
3223
3223
  import_path = __toESM(require("path"), 1);
3224
+ BOOTSPRING_VERSION = "2.5.0";
3225
+ BOOTSPRING_PACKAGE_NAME = "@girardmedia/bootspring";
3224
3226
  REDACTED = "[REDACTED]";
3225
3227
  SENSITIVE_KEY_PATTERN = /(?:^|[_-])(api[_-]?key|token|refresh[_-]?token|authorization|x[_-]?api[_-]?key|project[_-]?id)$/i;
3226
3228
  COLORS = {
@@ -3269,6 +3271,8 @@ __export(dist_exports, {
3269
3271
  API_BASE: () => API_BASE,
3270
3272
  API_VERSION: () => API_VERSION,
3271
3273
  BOOTSPRING_DIR: () => BOOTSPRING_DIR,
3274
+ BOOTSPRING_PACKAGE_NAME: () => BOOTSPRING_PACKAGE_NAME,
3275
+ BOOTSPRING_VERSION: () => BOOTSPRING_VERSION,
3272
3276
  COLORS: () => COLORS,
3273
3277
  CONFIG_FILE: () => CONFIG_FILE,
3274
3278
  CREDENTIALS_FILE: () => CREDENTIALS_FILE,
@@ -3333,7 +3337,9 @@ __export(dist_exports, {
3333
3337
  login: () => login,
3334
3338
  loginWithApiKey: () => loginWithApiKey,
3335
3339
  logout: () => logout,
3340
+ maybeAutoUploadTelemetry: () => maybeAutoUploadTelemetry,
3336
3341
  pollDeviceToken: () => pollDeviceToken,
3342
+ presence: () => presence_exports,
3337
3343
  print: () => print,
3338
3344
  readFile: () => readFile,
3339
3345
  readNearestProjectConfig: () => readNearestProjectConfig,
@@ -3349,6 +3355,8 @@ __export(dist_exports, {
3349
3355
  saveProjectScopedSession: () => saveProjectScopedSession,
3350
3356
  saveSession: () => saveSession,
3351
3357
  selfHeal: () => self_heal_exports,
3358
+ sendHealthReport: () => sendHealthReport,
3359
+ sendHeartbeat: () => sendHeartbeat,
3352
3360
  session: () => session_exports,
3353
3361
  setAuthFailureHandler: () => setAuthFailureHandler,
3354
3362
  setCurrentProject: () => setCurrentProject,
@@ -4363,7 +4371,89 @@ function runDiagnostics(autoFix = false) {
4363
4371
  }
4364
4372
  return results;
4365
4373
  }
4366
- var import_fs2, import_path2, import_os, import_crypto, import_https, import_http, import_fs3, import_path3, import_fs4, import_path4, import_os2, import_child_process, __defProp2, __export2, auth_exports, BOOTSPRING_DIR, CREDENTIALS_FILE, CONFIG_FILE, DEVICE_FILE, _credentialCache, _credentialDecryptFailed, getEncryptionKey, api_client_exports, API_BASE, API_VERSION, _onAuthFailure, cache, CACHE_TTL, session_exports, SESSION_FILE, LOCAL_CONFIG_NAME, PROJECT_SCOPE_MARKERS, LIMITS, PATTERNS, SHELL_DANGEROUS_CHARS, self_heal_exports, healers;
4374
+ function getAuthHeaders() {
4375
+ const headers = {
4376
+ "content-type": "application/json"
4377
+ };
4378
+ const apiKey = getApiKey();
4379
+ if (apiKey) {
4380
+ headers["x-api-key"] = apiKey;
4381
+ } else {
4382
+ const token = getToken();
4383
+ if (token) {
4384
+ headers["authorization"] = `Bearer ${token}`;
4385
+ } else {
4386
+ return null;
4387
+ }
4388
+ }
4389
+ return headers;
4390
+ }
4391
+ function sendHeartbeat(options) {
4392
+ if (!isAuthenticated()) return;
4393
+ const project = getEffectiveProject();
4394
+ if (!project?.id) return;
4395
+ const now = Date.now();
4396
+ if (now - _lastHeartbeatAt < HEARTBEAT_DEBOUNCE_MS) return;
4397
+ _lastHeartbeatAt = now;
4398
+ const headers = getAuthHeaders();
4399
+ if (!headers) return;
4400
+ headers["x-project-id"] = project.id;
4401
+ let deviceId;
4402
+ try {
4403
+ deviceId = getDeviceId();
4404
+ } catch {
4405
+ deviceId = "unknown";
4406
+ }
4407
+ const body = JSON.stringify({
4408
+ projectId: project.id,
4409
+ status: options?.status || "online",
4410
+ activity: options?.activity || "cli",
4411
+ deviceId
4412
+ });
4413
+ const controller = new AbortController();
4414
+ const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
4415
+ fetch(`${DASHBOARD_BASE}/api/v1/presence/heartbeat`, {
4416
+ method: "POST",
4417
+ headers,
4418
+ body,
4419
+ signal: controller.signal
4420
+ }).catch(() => {
4421
+ }).finally(() => clearTimeout(timer));
4422
+ }
4423
+ async function sendHealthReport(payload) {
4424
+ if (!isAuthenticated()) return;
4425
+ const project = getEffectiveProject();
4426
+ if (!project?.id) return;
4427
+ const headers = getAuthHeaders();
4428
+ if (!headers) return;
4429
+ headers["x-project-id"] = project.id;
4430
+ const controller = new AbortController();
4431
+ const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
4432
+ try {
4433
+ await fetch(`${DASHBOARD_BASE}/api/v1/health/report`, {
4434
+ method: "POST",
4435
+ headers,
4436
+ body: JSON.stringify({
4437
+ projectId: project.id,
4438
+ score: payload.score,
4439
+ grade: payload.grade,
4440
+ data: payload.data
4441
+ }),
4442
+ signal: controller.signal
4443
+ });
4444
+ } catch {
4445
+ } finally {
4446
+ clearTimeout(timer);
4447
+ }
4448
+ }
4449
+ function maybeAutoUploadTelemetry() {
4450
+ if (!isAuthenticated()) return;
4451
+ const now = Date.now();
4452
+ const ONE_HOUR = 60 * 60 * 1e3;
4453
+ if (now - _lastTelemetryUploadAt < ONE_HOUR) return;
4454
+ _lastTelemetryUploadAt = now;
4455
+ }
4456
+ var import_fs2, import_path2, import_os, import_crypto, import_https, import_http, import_fs3, import_path3, import_fs4, import_path4, import_os2, import_child_process, __defProp2, __export2, auth_exports, BOOTSPRING_DIR, CREDENTIALS_FILE, CONFIG_FILE, DEVICE_FILE, _credentialCache, _credentialDecryptFailed, getEncryptionKey, api_client_exports, API_BASE, API_VERSION, _onAuthFailure, cache, CACHE_TTL, session_exports, SESSION_FILE, LOCAL_CONFIG_NAME, PROJECT_SCOPE_MARKERS, LIMITS, PATTERNS, SHELL_DANGEROUS_CHARS, self_heal_exports, healers, presence_exports, DASHBOARD_BASE, HEARTBEAT_DEBOUNCE_MS, REQUEST_TIMEOUT_MS, _lastHeartbeatAt, _lastTelemetryUploadAt;
4367
4457
  var init_dist2 = __esm({
4368
4458
  "../../packages/core/dist/index.mjs"() {
4369
4459
  "use strict";
@@ -4558,6 +4648,17 @@ var init_dist2 = __esm({
4558
4648
  return { issue: classifyError({ message: "cannot find module" }), healed: false };
4559
4649
  }
4560
4650
  };
4651
+ presence_exports = {};
4652
+ __export2(presence_exports, {
4653
+ maybeAutoUploadTelemetry: () => maybeAutoUploadTelemetry,
4654
+ sendHealthReport: () => sendHealthReport,
4655
+ sendHeartbeat: () => sendHeartbeat
4656
+ });
4657
+ DASHBOARD_BASE = process.env.BOOTSPRING_SITE_URL || "https://www.bootspring.com";
4658
+ HEARTBEAT_DEBOUNCE_MS = 2e4;
4659
+ REQUEST_TIMEOUT_MS = 5e3;
4660
+ _lastHeartbeatAt = 0;
4661
+ _lastTelemetryUploadAt = 0;
4561
4662
  }
4562
4663
  });
4563
4664
 
@@ -4584,6 +4685,7 @@ var {
4584
4685
 
4585
4686
  // src/index.ts
4586
4687
  init_dist2();
4688
+ init_dist();
4587
4689
 
4588
4690
  // src/middleware.ts
4589
4691
  init_cjs_shims();
@@ -4596,6 +4698,9 @@ function wrapCommand(cmd) {
4596
4698
  if (listeners) {
4597
4699
  const originalAction = listeners;
4598
4700
  cmd._actionHandler = async (...args) => {
4701
+ const commandName = cmd.name();
4702
+ presence_exports.sendHeartbeat({ activity: `bootspring ${commandName}` });
4703
+ presence_exports.maybeAutoUploadTelemetry();
4599
4704
  try {
4600
4705
  return await originalAction(...args);
4601
4706
  } catch (err) {
@@ -7598,6 +7703,367 @@ init_cjs_shims();
7598
7703
  var fs8 = __toESM(require("fs"), 1);
7599
7704
  var path9 = __toESM(require("path"), 1);
7600
7705
  init_dist();
7706
+ function getPlanningSurfaceStatus() {
7707
+ const planningDir = path9.join(process.cwd(), "planning");
7708
+ return {
7709
+ hasTodo: fs8.existsSync(path9.join(planningDir, "TODO.md")),
7710
+ hasQueue: fs8.existsSync(path9.join(planningDir, "TASK_QUEUE.md"))
7711
+ };
7712
+ }
7713
+ function printLegacyQueueWarningIfNeeded() {
7714
+ const { hasTodo, hasQueue } = getPlanningSurfaceStatus();
7715
+ if (hasTodo && hasQueue) {
7716
+ print.warning("Legacy planning conflict detected: planning/TODO.md is canonical, but planning/TASK_QUEUE.md is still present.");
7717
+ print.info("Run `bootspring build migrate-todo` to archive the lingering queue file.");
7718
+ }
7719
+ }
7720
+ function formatPlanningSourceLabel(source) {
7721
+ if (source === "TODO.md") return "planning/TODO.md";
7722
+ if (source === "TASK_QUEUE.md") return "planning/TASK_QUEUE.md (legacy fallback)";
7723
+ if (source === "BUILD_STATE.json") return "planning/BUILD_STATE.json";
7724
+ return "not detected";
7725
+ }
7726
+ function normalizeStatus(value) {
7727
+ const normalized = String(value || "pending").trim().toLowerCase().replace(/[\s-]+/g, "_");
7728
+ if (normalized === "done" || normalized === "complete" || normalized === "completed") {
7729
+ return "completed";
7730
+ }
7731
+ if (normalized === "inprogress" || normalized === "in_progress") {
7732
+ return "in_progress";
7733
+ }
7734
+ if (normalized === "blocked" || normalized === "skipped") {
7735
+ return normalized;
7736
+ }
7737
+ return normalized === "pending" ? "pending" : "pending";
7738
+ }
7739
+ function normalizePhase(value) {
7740
+ const normalized = String(value || "mvp").trim().toLowerCase().replace(/[\s-]+/g, "_");
7741
+ return ["foundation", "mvp", "launch"].includes(normalized) ? normalized : "mvp";
7742
+ }
7743
+ function formatPhaseName(phase) {
7744
+ if (!phase) return "Unknown";
7745
+ if (phase.toLowerCase() === "mvp") return "MVP";
7746
+ const normalized = normalizePhase(phase);
7747
+ return normalized.charAt(0).toUpperCase() + normalized.slice(1);
7748
+ }
7749
+ function parseDependencyIds(rawText) {
7750
+ const matches = rawText.match(/[a-z0-9][a-z0-9_-]*/gi) || [];
7751
+ const ids = [];
7752
+ const seen = /* @__PURE__ */ new Set();
7753
+ for (const match of matches) {
7754
+ const token = match.trim();
7755
+ if (!token.includes("-") || !/\d/.test(token) || seen.has(token)) continue;
7756
+ seen.add(token);
7757
+ ids.push(token);
7758
+ }
7759
+ return ids;
7760
+ }
7761
+ function parseSourceMetadata(rawSource) {
7762
+ const trimmed = rawSource.trim();
7763
+ const match = trimmed.match(/^(.+?)\s+\((.+)\)$/);
7764
+ if (match?.[1] && match?.[2]) {
7765
+ return {
7766
+ source: match[1].trim(),
7767
+ sourceSection: match[2].trim()
7768
+ };
7769
+ }
7770
+ return { source: trimmed };
7771
+ }
7772
+ function parseTodoTasks(content) {
7773
+ const tasks = [];
7774
+ const lines = content.split("\n");
7775
+ let currentPhase = "mvp";
7776
+ let currentTask = null;
7777
+ const pushTask = () => {
7778
+ if (!currentTask) return;
7779
+ tasks.push(currentTask);
7780
+ currentTask = null;
7781
+ };
7782
+ for (const line of lines) {
7783
+ const phaseMatch = line.match(/^##\s+(Foundation|MVP|Launch)\b/i);
7784
+ if (phaseMatch?.[1]) {
7785
+ pushTask();
7786
+ currentPhase = normalizePhase(phaseMatch[1]);
7787
+ continue;
7788
+ }
7789
+ const taskMatch = line.match(/^-\s+\[([ xX])\]\s+`([^`]+)`\s+(.+?)(?:\s+\(`?([\w-]+)`?\))?\s*$/);
7790
+ if (taskMatch?.[2] && taskMatch?.[3]) {
7791
+ pushTask();
7792
+ const checked = taskMatch[1]?.toLowerCase() === "x";
7793
+ currentTask = {
7794
+ id: taskMatch[2].trim(),
7795
+ title: taskMatch[3].trim(),
7796
+ phase: currentPhase,
7797
+ status: normalizeStatus(taskMatch[4] || (checked ? "completed" : "pending")),
7798
+ acceptanceCriteria: [],
7799
+ dependencies: []
7800
+ };
7801
+ continue;
7802
+ }
7803
+ if (!currentTask) {
7804
+ continue;
7805
+ }
7806
+ const sourceMatch = line.match(/^\s{2,}-\s+\*\*Source:\*\*\s+(.+)\s*$/);
7807
+ if (sourceMatch?.[1]) {
7808
+ const sourceMetadata = parseSourceMetadata(sourceMatch[1]);
7809
+ currentTask.source = sourceMetadata.source;
7810
+ currentTask.sourceSection = sourceMetadata.sourceSection;
7811
+ continue;
7812
+ }
7813
+ const descriptionMatch = line.match(/^\s{2,}-\s+\*\*Description:\*\*\s+(.+)\s*$/);
7814
+ if (descriptionMatch?.[1]) {
7815
+ currentTask.description = descriptionMatch[1].trim();
7816
+ continue;
7817
+ }
7818
+ const complexityMatch = line.match(/^\s{2,}-\s+\*\*Complexity:\*\*\s+(.+)\s*$/);
7819
+ if (complexityMatch?.[1]) {
7820
+ currentTask.complexity = complexityMatch[1].trim().toLowerCase();
7821
+ continue;
7822
+ }
7823
+ const dependenciesMatch = line.match(/^\s{2,}-\s+\*\*Dependencies:\*\*\s+(.+)\s*$/);
7824
+ if (dependenciesMatch?.[1]) {
7825
+ currentTask.dependencies = parseDependencyIds(dependenciesMatch[1]);
7826
+ continue;
7827
+ }
7828
+ const criteriaMatch = line.match(/^\s{2,}-\s+\[[ xX]\]\s+(.+)\s*$/);
7829
+ if (criteriaMatch?.[1]) {
7830
+ currentTask.acceptanceCriteria?.push(criteriaMatch[1].trim());
7831
+ }
7832
+ }
7833
+ pushTask();
7834
+ return tasks;
7835
+ }
7836
+ function parseQueueTasks(content) {
7837
+ const tasks = [];
7838
+ const tableMatches = content.matchAll(/^\|\s*([^|]+)\s*\|\s*([a-z0-9][a-z0-9_-]*)\s*\|\s*([^|]+)\s*\|\s*([^|]+)\s*\|\s*([^|]+)\s*\|\s*([^|]+)\s*\|$/gmi);
7839
+ for (const match of tableMatches) {
7840
+ const id = match[2]?.trim() ?? "";
7841
+ const title = match[3]?.trim() ?? "";
7842
+ if (id.toLowerCase() === "id" || title.toLowerCase() === "task" || id.includes("---") || title.includes("---")) {
7843
+ continue;
7844
+ }
7845
+ tasks.push({
7846
+ id,
7847
+ title,
7848
+ phase: normalizePhase(match[4]?.trim()),
7849
+ complexity: String(match[5] || "medium").trim().toLowerCase(),
7850
+ status: normalizeStatus(match[6]?.trim()),
7851
+ acceptanceCriteria: [],
7852
+ dependencies: []
7853
+ });
7854
+ }
7855
+ const detailMatches = content.matchAll(/^###\s+([a-z0-9][a-z0-9_-]*):\s*(.+)$/gmi);
7856
+ for (const match of detailMatches) {
7857
+ const id = match[1]?.trim() ?? "";
7858
+ const sectionStart = (match.index ?? 0) + match[0].length;
7859
+ const nextSectionOffset = content.slice(sectionStart).search(/^###\s+/m);
7860
+ const sectionContent = nextSectionOffset === -1 ? content.slice(sectionStart) : content.slice(sectionStart, sectionStart + nextSectionOffset);
7861
+ const existing = tasks.find((task) => task.id === id);
7862
+ const acceptanceCriteria = [];
7863
+ const criteriaBlock = sectionContent.match(/\*{0,2}Acceptance\s+Criteria:\*{0,2}\s*([\s\S]*?)(?=\n\*{0,2}[A-Z]|\n---|\n###|$)/i);
7864
+ if (criteriaBlock?.[1]) {
7865
+ for (const criteriaMatch of criteriaBlock[1].matchAll(/^\s*[-*]\s+(?:\[[ xX]\]\s*)?(.+)$/gm)) {
7866
+ if (criteriaMatch[1]) {
7867
+ acceptanceCriteria.push(criteriaMatch[1].trim());
7868
+ }
7869
+ }
7870
+ }
7871
+ const dependencies = (() => {
7872
+ const dependencyMatch = sectionContent.match(/\*{0,2}(?:Dependencies|Depends on):\*{0,2}\s*([\s\S]*?)(?=\n\*{0,2}[A-Z]|\n---|\n###|$)/i);
7873
+ return dependencyMatch?.[1] ? parseDependencyIds(dependencyMatch[1]) : [];
7874
+ })();
7875
+ if (existing) {
7876
+ existing.acceptanceCriteria = acceptanceCriteria;
7877
+ existing.dependencies = dependencies;
7878
+ }
7879
+ }
7880
+ return tasks;
7881
+ }
7882
+ function loadPlanningTasks() {
7883
+ const todoFile = path9.join(process.cwd(), "planning", "TODO.md");
7884
+ const queueFile = path9.join(process.cwd(), "planning", "TASK_QUEUE.md");
7885
+ if (fs8.existsSync(todoFile)) {
7886
+ try {
7887
+ const tasks = parseTodoTasks(fs8.readFileSync(todoFile, "utf-8"));
7888
+ if (tasks.length > 0) {
7889
+ return { tasks, source: "TODO.md" };
7890
+ }
7891
+ } catch {
7892
+ }
7893
+ }
7894
+ if (fs8.existsSync(queueFile)) {
7895
+ try {
7896
+ const tasks = parseQueueTasks(fs8.readFileSync(queueFile, "utf-8"));
7897
+ if (tasks.length > 0) {
7898
+ return { tasks, source: "TASK_QUEUE.md" };
7899
+ }
7900
+ } catch {
7901
+ }
7902
+ }
7903
+ return { tasks: [], source: null };
7904
+ }
7905
+ function renderTodo(tasks, projectName) {
7906
+ const byPhase = /* @__PURE__ */ new Map();
7907
+ for (const task of tasks) {
7908
+ const phase = normalizePhase(task.phase);
7909
+ const phaseTasks = byPhase.get(phase) ?? [];
7910
+ phaseTasks.push({ ...task, phase });
7911
+ byPhase.set(phase, phaseTasks);
7912
+ }
7913
+ const orderedPhases = ["foundation", "mvp", "launch", ...Array.from(byPhase.keys()).filter((phase) => !["foundation", "mvp", "launch"].includes(phase))];
7914
+ const total = tasks.length;
7915
+ const completed = tasks.filter((task) => normalizeStatus(task.status) === "completed").length;
7916
+ const pending = tasks.filter((task) => normalizeStatus(task.status) === "pending").length;
7917
+ const inProgress = tasks.filter((task) => normalizeStatus(task.status) === "in_progress").length;
7918
+ let content = `# ${projectName} - Build Todo
7919
+
7920
+ > Single source of truth for autonomous build execution
7921
+ > Updated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}
7922
+
7923
+ ---
7924
+
7925
+ ## Program Status
7926
+
7927
+ | Metric | Value |
7928
+ |---|---:|
7929
+ | Total Tasks | ${total} |
7930
+ | Completed | ${completed} |
7931
+ | Remaining | ${pending} |
7932
+ | In Progress | ${inProgress} |
7933
+
7934
+ ---
7935
+
7936
+ `;
7937
+ for (const phase of orderedPhases) {
7938
+ const phaseTasks = byPhase.get(phase);
7939
+ if (!phaseTasks || phaseTasks.length === 0) continue;
7940
+ const phaseCompleted = phaseTasks.filter((task) => normalizeStatus(task.status) === "completed").length;
7941
+ const percent = phaseTasks.length > 0 ? Math.round(phaseCompleted / phaseTasks.length * 100) : 0;
7942
+ content += `## ${formatPhaseName(phase)} (${phaseCompleted}/${phaseTasks.length} \u2014 ${percent}%)
7943
+
7944
+ `;
7945
+ for (const task of phaseTasks) {
7946
+ const status = normalizeStatus(task.status);
7947
+ const checkbox = status === "completed" ? "[x]" : "[ ]";
7948
+ const statusTag = status === "pending" ? "" : ` (\`${status}\`)`;
7949
+ content += `- ${checkbox} \`${task.id}\` ${task.title}${statusTag}
7950
+ `;
7951
+ if (task.source) {
7952
+ const sourceLabel = task.sourceSection ? `${task.source} (${task.sourceSection})` : task.source;
7953
+ content += ` - **Source:** ${sourceLabel}
7954
+ `;
7955
+ }
7956
+ if (task.description) {
7957
+ content += ` - **Description:** ${task.description}
7958
+ `;
7959
+ }
7960
+ if (task.complexity && task.complexity !== "medium") {
7961
+ content += ` - **Complexity:** ${task.complexity}
7962
+ `;
7963
+ }
7964
+ if (task.dependencies && task.dependencies.length > 0) {
7965
+ content += ` - **Dependencies:** ${task.dependencies.join(", ")}
7966
+ `;
7967
+ }
7968
+ if (task.acceptanceCriteria && task.acceptanceCriteria.length > 0) {
7969
+ const criteriaCheckbox = status === "completed" ? "[x]" : "[ ]";
7970
+ for (const criterion of task.acceptanceCriteria) {
7971
+ content += ` - ${criteriaCheckbox} ${criterion}
7972
+ `;
7973
+ }
7974
+ }
7975
+ }
7976
+ content += "\n---\n\n";
7977
+ }
7978
+ content += `*Updated: ${(/* @__PURE__ */ new Date()).toISOString()}*
7979
+ `;
7980
+ return content;
7981
+ }
7982
+ function getCurrentPhase(queue) {
7983
+ const active = queue?.find((task) => normalizeStatus(task.status) === "in_progress") ?? queue?.find((task) => normalizeStatus(task.status) === "pending");
7984
+ return active?.phase ? formatPhaseName(active.phase) : "N/A";
7985
+ }
7986
+ function getTaskEntriesFromState(state) {
7987
+ return (state?.implementationQueue ?? []).map((task) => ({
7988
+ id: task.id,
7989
+ title: task.title,
7990
+ phase: task.phase,
7991
+ complexity: task.estimatedComplexity,
7992
+ status: normalizeStatus(task.status),
7993
+ description: task.description,
7994
+ source: task.source,
7995
+ sourceSection: task.sourceSection,
7996
+ acceptanceCriteria: task.acceptanceCriteria ?? [],
7997
+ dependencies: task.dependencies ?? []
7998
+ }));
7999
+ }
8000
+ function extractSupplementaryContent(queueContent) {
8001
+ const lines = queueContent.split("\n");
8002
+ const sections = [];
8003
+ let currentSection = null;
8004
+ let inTaskTable = false;
8005
+ let inTaskDetail = false;
8006
+ for (const rawLine of lines) {
8007
+ const line = rawLine ?? "";
8008
+ if (/^\|\s*(Priority|Position|#)\s*\|/i.test(line)) {
8009
+ inTaskTable = true;
8010
+ continue;
8011
+ }
8012
+ if (inTaskTable && /^\|/.test(line)) continue;
8013
+ if (inTaskTable && !/^\|/.test(line)) inTaskTable = false;
8014
+ if (/^###\s+([a-z0-9][a-z0-9_-]*)\s*:/.test(line)) {
8015
+ inTaskDetail = true;
8016
+ continue;
8017
+ }
8018
+ if (inTaskDetail && /^#{2,3}\s+/.test(line) && !/^###\s+([a-z0-9][a-z0-9_-]*)\s*:/.test(line)) {
8019
+ inTaskDetail = false;
8020
+ }
8021
+ if (inTaskDetail) continue;
8022
+ if (/^#\s+.*(?:Implementation Queue|Task Queue)\b/i.test(line)) continue;
8023
+ if (/^>\s*(Ordered task queue|Last Updated|Current Version)/i.test(line)) continue;
8024
+ if (/^##\s+(Queue Status|Task Details)\b/i.test(line)) {
8025
+ if (/^##\s+Queue Status\b/i.test(line)) inTaskTable = true;
8026
+ if (/^##\s+Task Details\b/i.test(line)) inTaskDetail = true;
8027
+ continue;
8028
+ }
8029
+ if (/^\*Generated by/i.test(line)) continue;
8030
+ if (/^---$/.test(line.trim())) continue;
8031
+ if (/^##\s+/.test(line)) {
8032
+ if (currentSection) sections.push(currentSection);
8033
+ currentSection = { heading: line, lines: [] };
8034
+ continue;
8035
+ }
8036
+ if (currentSection) {
8037
+ currentSection.lines.push(line);
8038
+ }
8039
+ }
8040
+ if (currentSection) {
8041
+ sections.push(currentSection);
8042
+ }
8043
+ return sections.filter((section) => section.lines.some((line) => line.trim().length > 0)).map((section) => `${section.heading}
8044
+
8045
+ ${section.lines.join("\n").trim()}`).join("\n\n---\n\n").trim();
8046
+ }
8047
+ function getAvailableArchivePath(filePath) {
8048
+ const baseArchivePath = `${filePath}.bak`;
8049
+ if (!fs8.existsSync(baseArchivePath)) {
8050
+ return baseArchivePath;
8051
+ }
8052
+ let counter = 1;
8053
+ while (fs8.existsSync(`${baseArchivePath}.${counter}`)) {
8054
+ counter += 1;
8055
+ }
8056
+ return `${baseArchivePath}.${counter}`;
8057
+ }
8058
+ function syncTodoFromState(state) {
8059
+ const queue = state.implementationQueue ?? [];
8060
+ if (queue.length === 0) return;
8061
+ const dir = path9.join(process.cwd(), "planning");
8062
+ if (!fs8.existsSync(dir)) fs8.mkdirSync(dir, { recursive: true });
8063
+ const todoPath = path9.join(dir, "TODO.md");
8064
+ const tasks = getTaskEntriesFromState(state);
8065
+ fs8.writeFileSync(todoPath, renderTodo(tasks, state.projectName ?? path9.basename(process.cwd())));
8066
+ }
7601
8067
  function loadBuildState() {
7602
8068
  const stateFile = path9.join(process.cwd(), "planning", "BUILD_STATE.json");
7603
8069
  if (!fs8.existsSync(stateFile)) return null;
@@ -7610,67 +8076,21 @@ function loadBuildState() {
7610
8076
  function saveBuildState(state) {
7611
8077
  const dir = path9.join(process.cwd(), "planning");
7612
8078
  if (!fs8.existsSync(dir)) fs8.mkdirSync(dir, { recursive: true });
8079
+ if (state.implementationQueue?.length) {
8080
+ state.currentPhase = getCurrentPhase(state.implementationQueue);
8081
+ }
7613
8082
  if (state.metadata) state.metadata.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
7614
8083
  fs8.writeFileSync(path9.join(dir, "BUILD_STATE.json"), JSON.stringify(state, null, 2));
8084
+ syncTodoFromState(state);
7615
8085
  }
7616
8086
  function loadTasks() {
7617
- const todoFile = path9.join(process.cwd(), "planning", "TODO.md");
7618
- const queueFile = path9.join(process.cwd(), "planning", "TASK_QUEUE.md");
7619
- const files = [];
7620
- if (fs8.existsSync(todoFile)) files.push(todoFile);
7621
- if (fs8.existsSync(queueFile)) files.push(queueFile);
7622
- for (const file of files) {
7623
- try {
7624
- const content = fs8.readFileSync(file, "utf-8");
7625
- const tasks = [];
7626
- if (file.endsWith("TODO.md")) {
7627
- let currentPhase = "Unknown";
7628
- for (const line of content.split("\n")) {
7629
- const phaseMatch = line.match(/^##\s+(Foundation|MVP|Launch)\b/i);
7630
- if (phaseMatch) {
7631
- currentPhase = phaseMatch[1];
7632
- continue;
7633
- }
7634
- const todoMatch = line.match(/^-\s+\[([ xX])\]\s+`(bs-\d+)`\s+(.+?)(?:\s+\(`?(\w+)`?\))?$/);
7635
- if (todoMatch) {
7636
- const checked = todoMatch[1].toLowerCase() === "x";
7637
- const explicitStatus = todoMatch[4]?.toLowerCase();
7638
- tasks.push({
7639
- id: todoMatch[2],
7640
- title: todoMatch[3].trim(),
7641
- phase: currentPhase,
7642
- status: explicitStatus || (checked ? "completed" : "pending")
7643
- });
7644
- }
7645
- }
7646
- if (tasks.length > 0) return tasks;
7647
- }
7648
- for (const line of content.split("\n")) {
7649
- const match = line.match(/\|\s*\d+\s*\|\s*(bs-\d+)\s*\|\s*(.+?)\s*\|\s*(\w+)\s*\|\s*(\w+)\s*\|\s*(\w+)\s*\|/);
7650
- if (match) {
7651
- tasks.push({
7652
- id: match[1],
7653
- title: match[2].trim(),
7654
- phase: match[3],
7655
- complexity: match[4],
7656
- status: match[5]
7657
- });
7658
- }
7659
- }
7660
- if (tasks.length > 0) return tasks;
7661
- } catch {
7662
- continue;
7663
- }
8087
+ const planningTasks = loadPlanningTasks();
8088
+ if (planningTasks.tasks.length > 0) {
8089
+ return planningTasks.tasks;
7664
8090
  }
7665
8091
  const state = loadBuildState();
7666
8092
  if (state?.implementationQueue?.length) {
7667
- return state.implementationQueue.map((t) => ({
7668
- id: t.id,
7669
- title: t.title,
7670
- phase: t.phase,
7671
- complexity: t.estimatedComplexity,
7672
- status: t.status
7673
- }));
8093
+ return getTaskEntriesFromState(state);
7674
8094
  }
7675
8095
  return [];
7676
8096
  }
@@ -7678,8 +8098,10 @@ function registerBuildCommand(program3) {
7678
8098
  const build = program3.command("build").description("Manage the build loop");
7679
8099
  build.command("status").description("Check build progress").action(() => {
7680
8100
  const state = loadBuildState();
7681
- const tasks = loadTasks();
8101
+ const planningTasks = loadPlanningTasks();
8102
+ const tasks = planningTasks.tasks.length > 0 ? planningTasks.tasks : getTaskEntriesFromState(state);
7682
8103
  print.header("Build Status");
8104
+ printLegacyQueueWarningIfNeeded();
7683
8105
  if (!state) {
7684
8106
  print.warning("No build state found. Run `bootspring build start` to begin.");
7685
8107
  return;
@@ -7697,11 +8119,16 @@ function registerBuildCommand(program3) {
7697
8119
  const sessionId = state.loopSession?.sessionId ?? "N/A";
7698
8120
  print.info(`Project: ${state.projectName ?? "Unknown"}`);
7699
8121
  print.info(`Status: ${state.status}`);
7700
- print.info(`Phase: ${state.currentPhase ?? "N/A"}`);
8122
+ print.info(`Phase: ${formatPhaseName(state.currentPhase ?? getCurrentPhase(state.implementationQueue))}`);
8123
+ print.info(`Planning Source: ${formatPlanningSourceLabel(planningTasks.source ?? (state ? "BUILD_STATE.json" : null))}`);
7701
8124
  print.info(`Progress: ${completed}/${total} (${pct}%)`);
7702
8125
  print.info(`Pending: ${pending}`);
7703
8126
  if (inProgress > 0) print.info(`In Progress: ${inProgress}`);
7704
- print.info(`Iteration: ${iteration}/${maxIter}`);
8127
+ if (iteration > maxIter) {
8128
+ print.info(`Iteration: ${iteration} (session max ${maxIter})`);
8129
+ } else {
8130
+ print.info(`Iteration: ${iteration}/${maxIter}`);
8131
+ }
7705
8132
  print.info(`Session: ${sessionId}`);
7706
8133
  const currentTask = tasks.find((t) => t.status.toLowerCase() === "in_progress");
7707
8134
  if (currentTask) {
@@ -7713,6 +8140,7 @@ function registerBuildCommand(program3) {
7713
8140
  console.log(` [${bar}] ${pct}%`);
7714
8141
  });
7715
8142
  build.command("task").description("Show the current task").action(() => {
8143
+ printLegacyQueueWarningIfNeeded();
7716
8144
  const tasks = loadTasks();
7717
8145
  const current = tasks.find((t) => t.status.toLowerCase() === "in_progress");
7718
8146
  const next = current ?? tasks.find((t) => t.status.toLowerCase() === "pending");
@@ -7757,6 +8185,7 @@ function registerBuildCommand(program3) {
7757
8185
  }
7758
8186
  });
7759
8187
  build.command("plan").description("View the full build plan").action(() => {
8188
+ printLegacyQueueWarningIfNeeded();
7760
8189
  const tasks = loadTasks();
7761
8190
  if (tasks.length === 0) {
7762
8191
  print.warning("No tasks found in planning/TODO.md or planning/TASK_QUEUE.md");
@@ -7856,10 +8285,11 @@ function registerBuildCommand(program3) {
7856
8285
  saveBuildState(state);
7857
8286
  print.info("Build resumed");
7858
8287
  });
7859
- build.command("sync").description("Sync tasks from TASK_QUEUE.md to BUILD_STATE.json").action(() => {
7860
- const queueFile = path9.join(process.cwd(), "planning", "TASK_QUEUE.md");
7861
- if (!fs8.existsSync(queueFile)) {
7862
- print.warning("No planning/TASK_QUEUE.md found");
8288
+ build.command("sync").description("Sync planning tasks from TODO.md into BUILD_STATE.json").option("--replace", "Replace runtime queue instead of merging").action((opts) => {
8289
+ printLegacyQueueWarningIfNeeded();
8290
+ const planningTasks = loadPlanningTasks();
8291
+ if (planningTasks.tasks.length === 0) {
8292
+ print.warning("No planning/TODO.md found (legacy fallback: planning/TASK_QUEUE.md)");
7863
8293
  return;
7864
8294
  }
7865
8295
  const state = loadBuildState();
@@ -7867,24 +8297,62 @@ function registerBuildCommand(program3) {
7867
8297
  print.error("No build state found");
7868
8298
  return;
7869
8299
  }
7870
- const tasks = loadTasks();
7871
- const existingIds = new Set((state.implementationQueue ?? []).map((t) => t.id));
8300
+ const replace = Boolean(opts.replace);
8301
+ const planningIds = new Set(planningTasks.tasks.map((task) => task.id));
8302
+ const existingQueue = replace ? [] : state.implementationQueue ?? [];
8303
+ const existingIds = new Set(existingQueue.map((t) => t.id));
7872
8304
  let added = 0;
7873
- for (const task of tasks) {
8305
+ let updated = 0;
8306
+ for (const task of planningTasks.tasks) {
7874
8307
  if (!existingIds.has(task.id)) {
7875
- state.implementationQueue = state.implementationQueue ?? [];
7876
- state.implementationQueue.push({
8308
+ existingQueue.push({
7877
8309
  id: task.id,
7878
8310
  title: task.title,
7879
8311
  phase: task.phase,
7880
8312
  status: task.status,
7881
- estimatedComplexity: task.complexity
8313
+ estimatedComplexity: task.complexity,
8314
+ description: task.description,
8315
+ source: task.source,
8316
+ sourceSection: task.sourceSection,
8317
+ acceptanceCriteria: task.acceptanceCriteria,
8318
+ dependencies: task.dependencies
7882
8319
  });
7883
8320
  added++;
8321
+ continue;
8322
+ }
8323
+ const index = existingQueue.findIndex((existing) => existing.id === task.id);
8324
+ if (index >= 0) {
8325
+ const previous = existingQueue[index];
8326
+ const nextTask = {
8327
+ ...previous,
8328
+ id: task.id,
8329
+ title: task.title,
8330
+ phase: task.phase,
8331
+ status: task.status,
8332
+ estimatedComplexity: task.complexity,
8333
+ description: task.description,
8334
+ source: task.source,
8335
+ sourceSection: task.sourceSection,
8336
+ acceptanceCriteria: task.acceptanceCriteria,
8337
+ dependencies: task.dependencies
8338
+ };
8339
+ if (JSON.stringify(previous) !== JSON.stringify(nextTask)) {
8340
+ existingQueue[index] = nextTask;
8341
+ updated++;
8342
+ }
7884
8343
  }
7885
8344
  }
8345
+ const orderedQueue = [
8346
+ ...planningTasks.tasks.map((task) => existingQueue.find((existing) => existing.id === task.id)).filter(Boolean),
8347
+ ...existingQueue.filter((task) => !planningIds.has(task.id))
8348
+ ];
8349
+ state.implementationQueue = orderedQueue;
8350
+ if (!state.metadata) {
8351
+ state.metadata = { updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
8352
+ }
7886
8353
  saveBuildState(state);
7887
- print.success(`Synced ${added} new tasks (${state.implementationQueue?.length ?? 0} total)`);
8354
+ print.success(`Synced ${added} new tasks and updated ${updated} tasks (${state.implementationQueue?.length ?? 0} total)`);
8355
+ print.info(`Planning source: ${planningTasks.source ?? "unknown"}`);
7888
8356
  });
7889
8357
  build.command("stop").description("Graceful stop the build loop").action(() => {
7890
8358
  const state = loadBuildState();
@@ -7940,6 +8408,7 @@ function registerBuildCommand(program3) {
7940
8408
  print.success(`Started: ${first.id} \u2014 ${first.title}`);
7941
8409
  });
7942
8410
  build.command("start").description("Initialize build from seed/planning documents").action(() => {
8411
+ printLegacyQueueWarningIfNeeded();
7943
8412
  const state = loadBuildState();
7944
8413
  if (state) {
7945
8414
  print.warning("Build already initialized. Use `bootspring build status` to check progress.");
@@ -7947,7 +8416,7 @@ function registerBuildCommand(program3) {
7947
8416
  }
7948
8417
  const tasks = loadTasks();
7949
8418
  if (tasks.length === 0) {
7950
- print.warning("No tasks found in planning/TODO.md or planning/TASK_QUEUE.md");
8419
+ print.warning("No tasks found in planning/TODO.md (legacy fallback: planning/TASK_QUEUE.md)");
7951
8420
  print.info("Run `bootspring preseed` or `bootspring prd create` first to generate tasks.");
7952
8421
  return;
7953
8422
  }
@@ -7975,54 +8444,56 @@ function registerBuildCommand(program3) {
7975
8444
  print.success(`Build initialized with ${tasks.length} tasks`);
7976
8445
  print.info("Run `bootspring build next` to start the first task");
7977
8446
  });
7978
- build.command("migrate-todo").alias("migrate").description("Convert TASK_QUEUE.md to enriched TODO.md format").action(() => {
8447
+ build.command("migrate-todo").alias("migrate").description("Convert legacy TASK_QUEUE.md into canonical TODO.md format and archive the queue by default").option("--keep-queue", "Keep legacy planning/TASK_QUEUE.md instead of archiving it").action((options) => {
7979
8448
  const queueFile = path9.join(process.cwd(), "planning", "TASK_QUEUE.md");
7980
8449
  const todoFile = path9.join(process.cwd(), "planning", "TODO.md");
7981
- if (!fs8.existsSync(queueFile)) {
7982
- print.warning("No planning/TASK_QUEUE.md found to migrate");
7983
- return;
7984
- }
7985
- if (fs8.existsSync(todoFile)) {
7986
- print.warning("planning/TODO.md already exists. Delete it first to re-migrate.");
7987
- return;
7988
- }
8450
+ const state = loadBuildState();
8451
+ const keepQueue = Boolean(options.keepQueue);
8452
+ const hasTodo = fs8.existsSync(todoFile);
8453
+ const hasQueue = fs8.existsSync(queueFile);
7989
8454
  try {
7990
- const content = fs8.readFileSync(queueFile, "utf-8");
7991
- const tasks = [];
7992
- for (const line of content.split("\n")) {
7993
- const match = line.match(/\|\s*\d+\s*\|\s*(bs-\d+)\s*\|\s*(.+?)\s*\|\s*(\w+)\s*\|\s*(\w+)\s*\|\s*(\w+)\s*\|/);
7994
- if (match) {
7995
- tasks.push({
7996
- id: match[1],
7997
- title: match[2].trim(),
7998
- phase: match[3],
7999
- complexity: match[4],
8000
- status: match[5]
8001
- });
8002
- }
8455
+ const planningTasks = loadPlanningTasks();
8456
+ let tasks = planningTasks.tasks;
8457
+ let source = planningTasks.source;
8458
+ if (tasks.length === 0) {
8459
+ tasks = getTaskEntriesFromState(state);
8460
+ source = tasks.length > 0 ? "BUILD_STATE.json" : null;
8003
8461
  }
8004
8462
  if (tasks.length === 0) {
8005
- print.warning("No tasks found in TASK_QUEUE.md");
8463
+ print.warning("No tasks found in planning/TODO.md, planning/TASK_QUEUE.md, or planning/BUILD_STATE.json");
8006
8464
  return;
8007
8465
  }
8008
- const phases = [...new Set(tasks.map((t) => t.phase))];
8009
- let todoContent = "# Build TODO\n\n";
8010
- for (const phase of phases) {
8011
- todoContent += `## ${phase}
8466
+ const dir = path9.dirname(todoFile);
8467
+ if (!fs8.existsSync(dir)) fs8.mkdirSync(dir, { recursive: true });
8468
+ const projectName = state?.projectName ?? path9.basename(process.cwd());
8469
+ if (!hasTodo || source !== "TODO.md") {
8470
+ let todoContent = renderTodo(tasks, projectName);
8471
+ if (hasQueue && source === "TASK_QUEUE.md") {
8472
+ const supplementaryContent = extractSupplementaryContent(fs8.readFileSync(queueFile, "utf-8"));
8473
+ if (supplementaryContent) {
8474
+ todoContent += `
8475
+ ---
8012
8476
 
8477
+ ## Reference (from TASK_QUEUE.md)
8478
+
8479
+ ${supplementaryContent}
8013
8480
  `;
8014
- const phaseTasks = tasks.filter((t) => t.phase === phase);
8015
- for (const task of phaseTasks) {
8016
- const checked = task.status.toLowerCase() === "completed" || task.status.toLowerCase() === "done";
8017
- todoContent += `- [${checked ? "x" : " "}] \`${task.id}\` ${task.title}
8018
- `;
8481
+ }
8019
8482
  }
8020
- todoContent += "\n";
8483
+ fs8.writeFileSync(todoFile, todoContent);
8484
+ print.success(`Wrote planning/TODO.md from ${source ?? "planning data"} (${tasks.length} tasks)`);
8485
+ } else {
8486
+ print.info("Retained existing planning/TODO.md as the canonical task file");
8487
+ }
8488
+ if (hasQueue && !keepQueue) {
8489
+ const archivePath = getAvailableArchivePath(queueFile);
8490
+ fs8.renameSync(queueFile, archivePath);
8491
+ print.success(`Archived legacy TASK_QUEUE.md to planning/${path9.basename(archivePath)}`);
8492
+ } else if (hasQueue && keepQueue) {
8493
+ print.info("Retained legacy TASK_QUEUE.md for compatibility (--keep-queue)");
8494
+ } else {
8495
+ print.info("No planning/TASK_QUEUE.md present; nothing to archive");
8021
8496
  }
8022
- const dir = path9.dirname(todoFile);
8023
- if (!fs8.existsSync(dir)) fs8.mkdirSync(dir, { recursive: true });
8024
- fs8.writeFileSync(todoFile, todoContent);
8025
- print.success(`Migrated ${tasks.length} tasks to planning/TODO.md`);
8026
8497
  } catch (err) {
8027
8498
  print.error(`Migration failed: ${err.message}`);
8028
8499
  }
@@ -8030,6 +8501,7 @@ function registerBuildCommand(program3) {
8030
8501
  build.command("backfill").aliases(["recover", "hydrate", "bootstrap"]).description("Recover build artifacts from existing codebase").action(() => {
8031
8502
  const planningDir = path9.join(process.cwd(), "planning");
8032
8503
  print.header("Build Artifact Recovery");
8504
+ printLegacyQueueWarningIfNeeded();
8033
8505
  const hasState = fs8.existsSync(path9.join(planningDir, "BUILD_STATE.json"));
8034
8506
  const hasTodo = fs8.existsSync(path9.join(planningDir, "TODO.md"));
8035
8507
  const hasQueue = fs8.existsSync(path9.join(planningDir, "TASK_QUEUE.md"));
@@ -8250,6 +8722,16 @@ function registerHealthCommand(program3) {
8250
8722
  }
8251
8723
  console.log(`
8252
8724
  ${passCount} passed, ${warnCount} warnings, ${failCount} failed`);
8725
+ try {
8726
+ const score = passCount > 0 ? Math.round(passCount / (passCount + failCount + warnCount) * 100) : 0;
8727
+ const grade = failCount === 0 ? warnCount === 0 ? "A" : "B" : "F";
8728
+ await presence_exports.sendHealthReport({
8729
+ score,
8730
+ grade,
8731
+ data: { checks, passCount, warnCount, failCount }
8732
+ });
8733
+ } catch {
8734
+ }
8253
8735
  if (failCount > 0) {
8254
8736
  process.exitCode = 1;
8255
8737
  }
@@ -8310,7 +8792,7 @@ function registerGenerateCommand(program3) {
8310
8792
  const sections = [
8311
8793
  `# ${projectName} - AI Context`,
8312
8794
  "",
8313
- "**Generated by**: Bootspring v2.0.0",
8795
+ `**Generated by**: Bootspring v${BOOTSPRING_VERSION}`,
8314
8796
  `**Last Updated**: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
8315
8797
  "",
8316
8798
  "---",
@@ -9215,7 +9697,7 @@ function registerLoopCommand(program3) {
9215
9697
  }
9216
9698
  const tasks = loadTasks2();
9217
9699
  if (tasks.length === 0) {
9218
- print.warning("No tasks found in planning/TODO.md or planning/TASK_QUEUE.md");
9700
+ print.warning("No tasks found in planning/TODO.md (legacy fallback: planning/TASK_QUEUE.md)");
9219
9701
  return;
9220
9702
  }
9221
9703
  const pending = tasks.filter((t) => t.status.toLowerCase() === "pending");
@@ -13556,9 +14038,227 @@ init_cjs_shims();
13556
14038
  var fs30 = __toESM(require("fs"), 1);
13557
14039
  var path31 = __toESM(require("path"), 1);
13558
14040
  init_dist();
13559
- var OUTPUT_DIR = "planning";
13560
- function getTemplates(projectName, date) {
14041
+
14042
+ // src/commands/business-templates.ts
14043
+ init_cjs_shims();
14044
+
14045
+ // src/commands/business-strategy-modules.ts
14046
+ init_cjs_shims();
14047
+ var STRATEGY_MODULES = [
14048
+ {
14049
+ title: "Precision ICP Builder",
14050
+ useWhen: "You need a buyer definition that changes product, sales, and messaging decisions immediately.",
14051
+ prompt: ["You are a market research strategist for early-stage software and services.", "Build a decision-ready ideal customer profile for the project."],
14052
+ returns: ["buyer filters that materially affect purchase behavior", "identity, professional self-image, status fears, and desired reputation", "urgent pains versus tolerable pains", "active buying triggers and decision style", "exact customer language, reachable communities, objections, first-buyer profile, and one opening sentence"],
14053
+ inputs: ["Offer: <describe what you sell>", "Current customer guess: <who you think buys today>"]
14054
+ },
14055
+ {
14056
+ title: "Distinctive Positioning Builder",
14057
+ useWhen: "You need messaging that sounds like you, not a generic competitor.",
14058
+ prompt: ["You are a positioning strategist.", "Write positioning for the project that survives a specificity test."],
14059
+ returns: ["one sharp positioning statement", "a rewrite pass that removes vague phrases", "a competitor difference matrix on the 4 factors buyers actually care about", "variations for cold outreach, homepage copy, and spoken pitch", "a short validation question to ask live prospects"],
14060
+ inputs: ["Product: <what you do>", "Target customer: <who buys this>", "Main alternative: <what buyers do instead>", "Real differentiator: <what is true and hard to copy>"]
14061
+ },
14062
+ {
14063
+ title: "Pricing Architecture Builder",
14064
+ useWhen: "You want pricing to steer buyers toward the right plan instead of just listing options.",
14065
+ prompt: ["You are a monetization strategist.", "Design a 3-tier pricing system for the project with strong anchoring."],
14066
+ returns: ["entry, core, and premium tier structure", "why each tier exists and what should stay out of it", "recommended monthly and annual price points", "upgrade triggers between tiers", "page-ordering, trial logic, and the pricing mistake most likely to hurt us"],
14067
+ inputs: ["Offer: <describe what you sell>", "Pricing instinct: <what you were planning to charge>", "Target customer: <who buys>"]
14068
+ },
14069
+ {
14070
+ title: "90-Day GTM Sprint Planner",
14071
+ useWhen: "You need a week-by-week launch plan with zero fluff and no paid ads.",
14072
+ prompt: ["You are a zero-to-one go-to-market operator.", "Create a 90-day GTM plan for the project."],
14073
+ returns: ["one-line ICP restatement", "top 3 launch channels ranked by likely first-90-day ROI", "week-by-week actions for validation, first closes, system building, and momentum", "measurable weekly targets", "2 daily non-negotiables for month 1", "one signal that proves the market is responding"],
14074
+ inputs: ["Product: <describe the product or service>", "Ideal buyer: <specific role and context>", "Available channels: <email, LinkedIn, communities, network, etc.>"]
14075
+ },
14076
+ {
14077
+ title: "Partnership Lever Scanner",
14078
+ useWhen: "You want distribution through other people\u2019s audiences instead of only direct outreach.",
14079
+ prompt: ["You are a partnership strategist.", "Find partnership and co-marketing opportunities for the project."],
14080
+ returns: ["complementary product partners", "audience owners and the co-marketing format that fits each", "referral partners and incentive logic", "integration partners that place us near the moment of need", "first outreach message and top 3 fastest opportunities"],
14081
+ inputs: ["Product: <what you sell>", "ICP: <who you want to reach>", "Category: <your niche or market>"]
14082
+ },
14083
+ {
14084
+ title: "Competitive Gap and Threat Review",
14085
+ useWhen: "You need actionable white space, not a generic list of rivals.",
14086
+ prompt: ["You are a competitive intelligence analyst.", "Map the competitive field for the project and show where we can win."],
14087
+ returns: ["the 5 most relevant competitors ranked by directness", "each rival\u2019s model, strengths, recurring weaknesses, and ignored segment", "the open category gap buyers would pay for", "the competitor most likely to copy us and how to raise the bar before they do", "3 quick wins to take buyers from a named rival this month"],
14088
+ inputs: ["Product: <what you sell>", "Known competitors: <names or descriptions>"]
14089
+ },
14090
+ {
14091
+ title: "Moat and Defensibility Planner",
14092
+ useWhen: "You need a credible path to defensibility before scale makes copying easier.",
14093
+ prompt: ["You are a startup strategy advisor.", "Evaluate defensibility for the project."],
14094
+ returns: ["current moat score across network effects, switching costs, cost advantage, intangible assets, and efficient scale", "the 1 or 2 moat types most realistic right now", "90-day actions that start compounding defensibility", "an incumbent attack scenario and how to reduce vulnerability", "a 2-sentence investor explanation of the moat"],
14095
+ inputs: ["Business model: <describe product and monetization>", "Current unfair advantage: <what is hard to copy today>"]
14096
+ },
14097
+ {
14098
+ title: "Revenue Model Stress Test",
14099
+ useWhen: "Your model looks fine on paper but you need to find the weak assumptions early.",
14100
+ prompt: ["You are a financial model reviewer.", "Pressure-test the revenue model for the project."],
14101
+ returns: ["unit economics summary", "break-even math and runway implications", "churn and concentration risks", "pricing-pressure scenario analysis", "customer growth math to reach key revenue milestones", "the single assumption that could break the model", "the one improvement lever with the highest 90-day impact"],
14102
+ inputs: ["Revenue model: <how money is made>", "Current pricing: <what you charge>", "Cost structure: <rough CAC and cost to serve>"]
14103
+ },
14104
+ {
14105
+ title: "Investor One-Pager Draft Builder",
14106
+ useWhen: "You need a fundraising artifact that is short, specific, and credible.",
14107
+ prompt: ["You are a fundraising narrative advisor.", "Write a one-page investor brief for the project."],
14108
+ returns: ["clear problem, solution, market framing, model, traction, team, and ask", "a competition section that differentiates without posturing", "a one-page layout with suggested word count per section", "the closing line that makes a meeting feel like the next obvious step"],
14109
+ inputs: ["Product: <what is being built>", "Traction: <revenue, pilots, waitlist, design partners, etc.>", "Team: <who is building it>"]
14110
+ },
14111
+ {
14112
+ title: "Startup Launch Brief",
14113
+ useWhen: "You want one document that turns strategy into immediate execution.",
14114
+ prompt: ["You are a founding-team operator.", "Synthesize the current plan for the project into a one-page execution brief."],
14115
+ returns: ["business in one line", "ICP in one line", "positioning in one line", "pricing summary", "30-day action plan", "first 5 customers with outreach angle", "top 3 risks and the mitigation for each", "the single highest-leverage action for the next 7 days", "the measurable 90-day success definition"],
14116
+ inputs: ["Idea context: <product, market, stage, founder context>", "Biggest uncertainty: <what feels most risky>", "90-day goal: <what success looks like>"]
14117
+ },
14118
+ {
14119
+ title: "Customer Interview Guide",
14120
+ useWhen: "You want interviews that produce decisions, not polite anecdotes.",
14121
+ prompt: ["You are a customer interview coach.", "Create a buyer interview guide for the project."],
14122
+ returns: ["12 interview questions in the right order", "follow-up probes for pain, urgency, alternatives, and budget", "signals that indicate real buying intent", "the 3 mistakes that bias the conversation"],
14123
+ inputs: ["Who you want to interview: <specific buyer type>", "Hypothesis to test: <belief you want validated or disproved>"]
14124
+ },
14125
+ {
14126
+ title: "Offer Packaging Designer",
14127
+ useWhen: "Your product is clear but the way it is sold still feels muddy.",
14128
+ prompt: ["You are an offer designer.", "Turn the project into a clear, high-conversion offer."],
14129
+ returns: ["core deliverable and boundary definition", "what is included, excluded, guaranteed, and time-bound", "add-ons, bonuses, urgency devices, and risk reversal", "the offer version best suited for first customers"],
14130
+ inputs: ["Product: <what you sell>", "Buyer: <who buys>", "Delivery model: <software, service, hybrid, one-time, recurring>"]
14131
+ },
14132
+ {
14133
+ title: "Homepage Messaging Brief",
14134
+ useWhen: "The product is real but the homepage does not yet explain why anyone should care.",
14135
+ prompt: ["You are a conversion copy strategist.", "Draft the messaging architecture for the homepage."],
14136
+ returns: ["hero headline and subhead", "proof strip, problem framing, value explanation, objection handling, and CTA logic", "section order for a high-intent homepage", "copy notes for designers and implementers"],
14137
+ inputs: ["ICP: <who lands on the page>", "Offer: <what you sell>", "Proof available: <logos, results, founder story, testimonials>"]
14138
+ },
14139
+ {
14140
+ title: "Outbound Sequence Builder",
14141
+ useWhen: "You need a repeatable outbound message system rooted in real buyer pain.",
14142
+ prompt: ["You are an outbound operator.", "Write a short outbound sequence for the project."],
14143
+ returns: ["first-touch email or DM", "3 follow-ups with different angles", "subject lines or hooks", "reply handling for curiosity, no timing, wrong fit, and no response"],
14144
+ inputs: ["Target role: <who you are contacting>", "Trigger event: <what happened that makes them timely>", "Offer angle: <why they should care now>"]
14145
+ },
14146
+ {
14147
+ title: "Sales Discovery Blueprint",
14148
+ useWhen: "You need a better structure for live calls and demos.",
14149
+ prompt: ["You are a sales discovery coach.", "Design a discovery and demo flow for the project."],
14150
+ returns: ["call agenda", "key diagnostic questions", "what to show in the product and in what order", "deal qualification criteria", "close question and next-step framing"],
14151
+ inputs: ["Buyer type: <role and company>", "Typical use case: <what they want done>", "Common objection: <what usually stalls the sale>"]
14152
+ },
14153
+ {
14154
+ title: "Activation Friction Audit",
14155
+ useWhen: "People sign up or book demos but stall before reaching value.",
14156
+ prompt: ["You are a product activation strategist.", "Audit the first-use journey for the project."],
14157
+ returns: ["the first-value milestone", "the top friction points before that milestone", "manual assists, product changes, and lifecycle messaging fixes", "one metric that best measures activation progress"],
14158
+ inputs: ["Onboarding flow: <how new users start>", "Desired outcome: <what first value looks like>", "Current drop-off: <where people stall today>"]
14159
+ },
14160
+ {
14161
+ title: "Referral Engine Designer",
14162
+ useWhen: "You have some happy users but no deliberate referral motion.",
14163
+ prompt: ["You are a referral growth strategist.", "Design a referral loop for the project."],
14164
+ returns: ["the ideal moment to ask for a referral", "what the referrer and referred person each get", "the message and CTA to use", "how to track quality, not just volume"],
14165
+ inputs: ["Best-fit customer: <who is happiest today>", "Product outcome: <what result they got>", "Incentive limits: <what you can realistically offer>"]
14166
+ },
14167
+ {
14168
+ title: "Case Study Capture Kit",
14169
+ useWhen: "You are getting wins but not turning them into reusable proof.",
14170
+ prompt: ["You are a case-study editor.", "Create a case-study capture system for the project."],
14171
+ returns: ["interview questions for customers", "the before/during/after story arc", "proof formats for website, outbound, investor updates, and sales calls", "release and approval checklist"],
14172
+ inputs: ["Customer win: <what changed for them>", "Available evidence: <quotes, screenshots, metrics, call notes>"]
14173
+ },
14174
+ {
14175
+ title: "Founder Narrative Builder",
14176
+ useWhen: "You need a tighter founder story for customers, hires, and investors.",
14177
+ prompt: ["You are a founder narrative strategist.", "Shape the founder story for the project."],
14178
+ returns: ["short founder story for homepage and deck", "longer origin story for meetings and recruiting", "why-now logic", "credibility anchors that feel earned rather than inflated"],
14179
+ inputs: ["Founder background: <relevant experience>", "Why this matters: <personal reason or market insight>", "What gives credibility: <evidence you can execute>"]
14180
+ },
14181
+ {
14182
+ title: "Channel Experiment Scorecard",
14183
+ useWhen: "You have too many growth ideas and need to prioritize the next 30 days.",
14184
+ prompt: ["You are a growth experiment lead.", "Build a channel experiment scorecard for the project."],
14185
+ returns: ["10 experiments scored by impact, speed, confidence, and cost", "one experiment to run this week", "kill criteria, success metric, and owner for each", "the reporting cadence for a 30-day sprint"],
14186
+ inputs: ["Current channels: <where you can reach buyers today>", "Team capacity: <how much time or headcount you have>", "Revenue goal: <what outcome matters most next>"]
14187
+ }
14188
+ ];
14189
+
14190
+ // src/commands/business-templates.ts
14191
+ function renderList(items) {
14192
+ return items.map((item) => `- ${item}`).join("\n");
14193
+ }
14194
+ function renderStrategyModule(module2, index) {
14195
+ return `### ${index}. ${module2.title}
14196
+
14197
+ **Use when**: ${module2.useWhen}
14198
+
14199
+ **Prompt**
14200
+ ${module2.prompt.join("\n")}
14201
+
14202
+ Return:
14203
+ ${renderList(module2.returns)}
14204
+
14205
+ Inputs:
14206
+ ${renderList(module2.inputs)}`;
14207
+ }
14208
+ function buildFounderStrategyPack(projectName, date) {
14209
+ const coreModules = STRATEGY_MODULES.slice(0, 10).map((module2, index) => renderStrategyModule(module2, index + 1)).join("\n\n");
14210
+ const expansionModules = STRATEGY_MODULES.slice(10).map((module2, index) => renderStrategyModule(module2, index + 11)).join("\n\n");
14211
+ return `# Founder Strategy Pack: ${projectName}
14212
+
14213
+ > **Version**: 1.0 | **Created**: ${date} | **Purpose**: Founder strategy prompts for customer research, positioning, pricing, GTM, fundraising, and growth
14214
+
14215
+ ---
14216
+
14217
+ ## How to Use This Pack
14218
+
14219
+ - Start with modules 1 through 10 to sharpen customer, market, and business fundamentals.
14220
+ - Use modules 11 through 20 to tighten execution, messaging, activation, and growth loops.
14221
+ - Feed the outputs back into \`AUDIENCE.md\`, \`BUSINESS_MODEL.md\`, \`COMPETITORS.md\`, \`ROADMAP.md\`, and your launch docs.
14222
+ - Replace the angle-bracket inputs before pasting a prompt into your preferred assistant or Bootspring workflow.
14223
+
14224
+ ---
14225
+
14226
+ ## Core Strategy Modules
14227
+
14228
+ ${coreModules}
14229
+
14230
+ ---
14231
+
14232
+ ## Execution Expansion Modules
14233
+
14234
+ ${expansionModules}
14235
+
14236
+ ---
14237
+
14238
+ ## Recommended Order
14239
+
14240
+ 1. Precision ICP Builder
14241
+ 2. Distinctive Positioning Builder
14242
+ 3. Pricing Architecture Builder
14243
+ 4. 90-Day GTM Sprint Planner
14244
+ 5. Competitive Gap and Threat Review
14245
+ 6. Startup Launch Brief
14246
+
14247
+ Then use the expansion modules based on the bottleneck you hit first.
14248
+
14249
+ ---
14250
+
14251
+ *Generated with Bootspring*
14252
+ `;
14253
+ }
14254
+ function getBusinessTemplates(projectName, date) {
13561
14255
  return {
14256
+ strategy: {
14257
+ name: "Founder Strategy Pack",
14258
+ output: "FOUNDER_STRATEGY_PACK.md",
14259
+ description: "20 founder strategy prompts for ICP, pricing, GTM, fundraising, and growth",
14260
+ content: buildFounderStrategyPack(projectName, date)
14261
+ },
13562
14262
  plan: {
13563
14263
  name: "Business Plan",
13564
14264
  output: "BUSINESS_PLAN.md",
@@ -13731,6 +14431,9 @@ For [target customer] who [need], [product] is a [category] that [key benefit].
13731
14431
  }
13732
14432
  };
13733
14433
  }
14434
+
14435
+ // src/commands/business.ts
14436
+ var OUTPUT_DIR = "planning";
13734
14437
  function registerBusinessCommand(program3) {
13735
14438
  const business = program3.command("business").description("Business planning and strategy tools");
13736
14439
  business.command("init").description("Initialize all business planning documents").action(async () => {
@@ -13743,7 +14446,7 @@ function registerBusinessCommand(program3) {
13743
14446
  if (!fs30.existsSync(outputDir)) {
13744
14447
  fs30.mkdirSync(outputDir, { recursive: true });
13745
14448
  }
13746
- const templates = getTemplates(projectName, date);
14449
+ const templates = getBusinessTemplates(projectName, date);
13747
14450
  const created = [];
13748
14451
  const skipped = [];
13749
14452
  const spinner = createSpinner("Creating business documents...").start();
@@ -13770,15 +14473,37 @@ function registerBusinessCommand(program3) {
13770
14473
  }
13771
14474
  }
13772
14475
  console.log("\nNext Steps:");
13773
- console.log(` 1. Edit ${OUTPUT_DIR}/BUSINESS_PLAN.md`);
13774
- console.log(" 2. Run `bootspring agent invoke business-strategy-expert`");
13775
- console.log(" 3. Run `bootspring business status` to check progress");
14476
+ console.log(` 1. Edit ${OUTPUT_DIR}/FOUNDER_STRATEGY_PACK.md`);
14477
+ console.log(` 2. Edit ${OUTPUT_DIR}/BUSINESS_PLAN.md`);
14478
+ console.log(" 3. Run `bootspring agent invoke business-strategy-expert`");
14479
+ console.log(" 4. Run `bootspring business status` to check progress");
14480
+ });
14481
+ business.command("strategy").description("Create founder strategy pack").action(async () => {
14482
+ const projectRoot = process.cwd();
14483
+ const projectName = path31.basename(projectRoot);
14484
+ const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
14485
+ const templates = getBusinessTemplates(projectName, date);
14486
+ const template = templates.strategy;
14487
+ print.header(template.name);
14488
+ const outputDir = path31.join(projectRoot, OUTPUT_DIR);
14489
+ const outputPath = path31.join(outputDir, template.output);
14490
+ if (fs30.existsSync(outputPath)) {
14491
+ print.warning(`File already exists: ${OUTPUT_DIR}/${template.output}`);
14492
+ return;
14493
+ }
14494
+ if (!fs30.existsSync(outputDir)) {
14495
+ fs30.mkdirSync(outputDir, { recursive: true });
14496
+ }
14497
+ fs30.writeFileSync(outputPath, template.content);
14498
+ print.success(`Created ${template.name}`);
14499
+ console.log(`
14500
+ File: ${OUTPUT_DIR}/${template.output}`);
13776
14501
  });
13777
14502
  business.command("plan").description("Create business plan").action(async () => {
13778
14503
  const projectRoot = process.cwd();
13779
14504
  const projectName = path31.basename(projectRoot);
13780
14505
  const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
13781
- const templates = getTemplates(projectName, date);
14506
+ const templates = getBusinessTemplates(projectName, date);
13782
14507
  const template = templates.plan;
13783
14508
  print.header(template.name);
13784
14509
  const outputDir = path31.join(projectRoot, OUTPUT_DIR);
@@ -13799,7 +14524,7 @@ File: ${OUTPUT_DIR}/${template.output}`);
13799
14524
  const projectRoot = process.cwd();
13800
14525
  const projectName = path31.basename(projectRoot);
13801
14526
  const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
13802
- const templates = getTemplates(projectName, date);
14527
+ const templates = getBusinessTemplates(projectName, date);
13803
14528
  const template = templates.model;
13804
14529
  print.header(template.name);
13805
14530
  const outputDir = path31.join(projectRoot, OUTPUT_DIR);
@@ -13820,7 +14545,7 @@ File: ${OUTPUT_DIR}/${template.output}`);
13820
14545
  const projectRoot = process.cwd();
13821
14546
  const projectName = path31.basename(projectRoot);
13822
14547
  const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
13823
- const templates = getTemplates(projectName, date);
14548
+ const templates = getBusinessTemplates(projectName, date);
13824
14549
  const template = templates.competitors;
13825
14550
  print.header(template.name);
13826
14551
  const outputDir = path31.join(projectRoot, OUTPUT_DIR);
@@ -13843,7 +14568,7 @@ File: ${OUTPUT_DIR}/${template.output}`);
13843
14568
  const projectName = path31.basename(projectRoot);
13844
14569
  const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
13845
14570
  const outputDir = path31.join(projectRoot, OUTPUT_DIR);
13846
- const templates = getTemplates(projectName, date);
14571
+ const templates = getBusinessTemplates(projectName, date);
13847
14572
  let found = 0;
13848
14573
  const total = Object.keys(templates).length;
13849
14574
  for (const [key, template] of Object.entries(templates)) {
@@ -13868,11 +14593,11 @@ Progress: ${found}/${total} documents created`);
13868
14593
  console.log(" bootspring business init - Create all documents");
13869
14594
  }
13870
14595
  });
13871
- business.command("show").description("Preview a business document").argument("<type>", "Document type (plan, model, competitors)").action((type) => {
14596
+ business.command("show").description("Preview a business document").argument("<type>", "Document type (strategy, plan, model, competitors)").action((type) => {
13872
14597
  const projectRoot = process.cwd();
13873
14598
  const projectName = path31.basename(projectRoot);
13874
14599
  const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
13875
- const templates = getTemplates(projectName, date);
14600
+ const templates = getBusinessTemplates(projectName, date);
13876
14601
  const template = templates[type];
13877
14602
  if (!template) {
13878
14603
  print.error(`Unknown template: ${type}`);
@@ -19668,6 +20393,9 @@ function registerSetupCommand(program3) {
19668
20393
  setup.command("status").description("Show current assistant MCP setup status").action(() => {
19669
20394
  showStatus4();
19670
20395
  });
20396
+ setup.command("ecosystem").description("Configure popular third-party MCP servers alongside Bootspring").action(async () => {
20397
+ await setupEcosystem();
20398
+ });
19671
20399
  setup.action(async () => {
19672
20400
  await setupAssistants();
19673
20401
  });
@@ -19827,10 +20555,150 @@ ${COLORS.cyan}${COLORS.bold}Bootspring Setup Status${COLORS.reset}
19827
20555
  }
19828
20556
  console.log();
19829
20557
  }
20558
+ var ECOSYSTEM_SERVERS = [
20559
+ {
20560
+ key: "context7",
20561
+ label: "Context7",
20562
+ description: "Library documentation via MCP (Bootspring also provides this via bootspring_docs)",
20563
+ mcp: { command: "npx", args: ["-y", "@upstash/context7-mcp"] },
20564
+ tomlBlock: `[mcp_servers.context7]
20565
+ command = "npx"
20566
+ args = ["-y", "@upstash/context7-mcp"]
20567
+ `
20568
+ },
20569
+ {
20570
+ key: "sequential-thinking",
20571
+ label: "Sequential Thinking",
20572
+ description: "Step-by-step reasoning for complex problem solving",
20573
+ mcp: { command: "npx", args: ["-y", "@modelcontextprotocol/server-sequential-thinking"] },
20574
+ tomlBlock: `[mcp_servers.sequential-thinking]
20575
+ command = "npx"
20576
+ args = ["-y", "@modelcontextprotocol/server-sequential-thinking"]
20577
+ `
20578
+ }
20579
+ ];
20580
+ function upsertEcosystemJsonMcp(configPath, servers) {
20581
+ const added = [];
20582
+ const skipped = [];
20583
+ try {
20584
+ const dir = path55.dirname(configPath);
20585
+ if (!fs54.existsSync(dir)) fs54.mkdirSync(dir, { recursive: true });
20586
+ let settings = {};
20587
+ if (fs54.existsSync(configPath)) {
20588
+ settings = JSON.parse(fs54.readFileSync(configPath, "utf-8"));
20589
+ }
20590
+ if (!settings.mcpServers) settings.mcpServers = {};
20591
+ for (const server of servers) {
20592
+ if (settings.mcpServers[server.key]) {
20593
+ skipped.push(server.key);
20594
+ } else {
20595
+ settings.mcpServers[server.key] = server.mcp;
20596
+ added.push(server.key);
20597
+ }
20598
+ }
20599
+ if (added.length > 0) {
20600
+ fs54.writeFileSync(configPath, `${JSON.stringify(settings, null, 2)}
20601
+ `, "utf-8");
20602
+ }
20603
+ } catch {
20604
+ }
20605
+ return { added, skipped };
20606
+ }
20607
+ function upsertEcosystemCodexToml(servers) {
20608
+ const configPath = path55.join(HOME, ".codex", "config.toml");
20609
+ const added = [];
20610
+ const skipped = [];
20611
+ try {
20612
+ const dir = path55.dirname(configPath);
20613
+ if (!fs54.existsSync(dir)) fs54.mkdirSync(dir, { recursive: true });
20614
+ let existing = fs54.existsSync(configPath) ? fs54.readFileSync(configPath, "utf-8") : "";
20615
+ for (const server of servers) {
20616
+ const marker = `[mcp_servers.${server.key}]`;
20617
+ if (existing.includes(marker)) {
20618
+ skipped.push(server.key);
20619
+ } else {
20620
+ existing = existing.length > 0 ? `${existing.trimEnd()}
20621
+
20622
+ ${server.tomlBlock}` : server.tomlBlock;
20623
+ added.push(server.key);
20624
+ }
20625
+ }
20626
+ if (added.length > 0) {
20627
+ fs54.writeFileSync(configPath, existing, "utf-8");
20628
+ }
20629
+ } catch {
20630
+ }
20631
+ return { added, skipped };
20632
+ }
20633
+ async function setupEcosystem() {
20634
+ console.log(`
20635
+ ${COLORS.cyan}${COLORS.bold}Bootspring Ecosystem Setup${COLORS.reset}
20636
+ `);
20637
+ console.log(`Configuring third-party MCP servers alongside Bootspring...
20638
+ `);
20639
+ const claudePath = findConfigPath([
20640
+ path55.join(HOME, ".claude", "settings.json"),
20641
+ path55.join(HOME, ".config", "claude-code", "settings.json")
20642
+ ]);
20643
+ const claudeResult = upsertEcosystemJsonMcp(claudePath, ECOSYSTEM_SERVERS);
20644
+ console.log(`${COLORS.bold}Claude Code${COLORS.reset} ${COLORS.dim}(${claudePath})${COLORS.reset}`);
20645
+ for (const key of claudeResult.added) {
20646
+ const srv = ECOSYSTEM_SERVERS.find((s) => s.key === key);
20647
+ console.log(` ${COLORS.green}+${COLORS.reset} ${srv.label}: ${srv.description}`);
20648
+ }
20649
+ for (const key of claudeResult.skipped) {
20650
+ const srv = ECOSYSTEM_SERVERS.find((s) => s.key === key);
20651
+ console.log(` ${COLORS.dim}-${COLORS.reset} ${srv.label}: already configured`);
20652
+ }
20653
+ const codexResult = upsertEcosystemCodexToml(ECOSYSTEM_SERVERS);
20654
+ const codexPath = path55.join(HOME, ".codex", "config.toml");
20655
+ console.log(`
20656
+ ${COLORS.bold}Codex${COLORS.reset} ${COLORS.dim}(${codexPath})${COLORS.reset}`);
20657
+ for (const key of codexResult.added) {
20658
+ const srv = ECOSYSTEM_SERVERS.find((s) => s.key === key);
20659
+ console.log(` ${COLORS.green}+${COLORS.reset} ${srv.label}: ${srv.description}`);
20660
+ }
20661
+ for (const key of codexResult.skipped) {
20662
+ const srv = ECOSYSTEM_SERVERS.find((s) => s.key === key);
20663
+ console.log(` ${COLORS.dim}-${COLORS.reset} ${srv.label}: already configured`);
20664
+ }
20665
+ const geminiPath = findConfigPath([
20666
+ path55.join(HOME, ".gemini", "settings.json"),
20667
+ path55.join(HOME, ".config", "gemini", "settings.json")
20668
+ ]);
20669
+ const geminiResult = upsertEcosystemJsonMcp(geminiPath, ECOSYSTEM_SERVERS);
20670
+ console.log(`
20671
+ ${COLORS.bold}Gemini CLI${COLORS.reset} ${COLORS.dim}(${geminiPath})${COLORS.reset}`);
20672
+ for (const key of geminiResult.added) {
20673
+ const srv = ECOSYSTEM_SERVERS.find((s) => s.key === key);
20674
+ console.log(` ${COLORS.green}+${COLORS.reset} ${srv.label}: ${srv.description}`);
20675
+ }
20676
+ for (const key of geminiResult.skipped) {
20677
+ const srv = ECOSYSTEM_SERVERS.find((s) => s.key === key);
20678
+ console.log(` ${COLORS.dim}-${COLORS.reset} ${srv.label}: already configured`);
20679
+ }
20680
+ const totalAdded = claudeResult.added.length + codexResult.added.length + geminiResult.added.length;
20681
+ const totalSkipped = claudeResult.skipped.length + codexResult.skipped.length + geminiResult.skipped.length;
20682
+ console.log(`
20683
+ ${COLORS.bold}Summary${COLORS.reset}`);
20684
+ console.log(` ${COLORS.green}${totalAdded}${COLORS.reset} server entries added`);
20685
+ if (totalSkipped > 0) {
20686
+ console.log(` ${COLORS.dim}${totalSkipped} already configured (skipped)${COLORS.reset}`);
20687
+ }
20688
+ console.log(`
20689
+ ${COLORS.dim}Available MCP servers after setup:${COLORS.reset}`);
20690
+ console.log(` ${COLORS.cyan}bootspring${COLORS.reset} \u2014 Full dev workflow (context, skills, agents, quality, build)`);
20691
+ for (const srv of ECOSYSTEM_SERVERS) {
20692
+ console.log(` ${COLORS.cyan}${srv.key.padEnd(22)}${COLORS.reset} \u2014 ${srv.description}`);
20693
+ }
20694
+ console.log(`
20695
+ ${COLORS.green}Done.${COLORS.reset} Restart assistant clients to load new MCP servers.
20696
+ `);
20697
+ }
19830
20698
 
19831
20699
  // src/index.ts
19832
20700
  var program2 = new Command();
19833
- program2.name("bootspring").description("Development scaffolding with intelligence - CLI for AI-assisted workflows").version("2.0.0");
20701
+ program2.name("bootspring").description("Development scaffolding with intelligence - CLI for AI-assisted workflows").version(BOOTSPRING_VERSION);
19834
20702
  registerAuthCommand(program2);
19835
20703
  registerProjectCommand(program2);
19836
20704
  registerSwitchCommand(program2);