wayfind 2.0.38 → 2.0.39

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 (2) hide show
  1. package/bin/team-context.js +211 -2
  2. package/package.json +1 -1
@@ -4664,6 +4664,194 @@ function runCheckVersion() {
4664
4664
  }, CLI_USER);
4665
4665
  }
4666
4666
 
4667
+ // ── Container doctor ────────────────────────────────────────────────────────
4668
+
4669
+ /**
4670
+ * Detect if we're running inside a Docker container.
4671
+ */
4672
+ function isRunningInContainer() {
4673
+ try {
4674
+ return fs.existsSync('/.dockerenv');
4675
+ } catch {
4676
+ return false;
4677
+ }
4678
+ }
4679
+
4680
+ /**
4681
+ * Container-specific health checks. Prints PASS/WARN lines and exits with
4682
+ * appropriate code. Useful as a post-startup self-check or via
4683
+ * `docker exec wayfind npx wayfind doctor --container`.
4684
+ */
4685
+ async function runContainerDoctor() {
4686
+ const GREEN = '\x1b[32m', YELLOW = '\x1b[33m', RED = '\x1b[31m', RESET = '\x1b[0m';
4687
+ const pass = (msg) => console.log(`${GREEN}PASS${RESET} ${msg}`);
4688
+ const warn = (msg) => console.log(`${YELLOW}WARN${RESET} ${msg}`);
4689
+ let issues = 0;
4690
+
4691
+ console.log('');
4692
+ console.log('Wayfind — Container Doctor');
4693
+ console.log('══════════════════════════════');
4694
+
4695
+ // 1. Backend type — is SQLite active or JSON fallback?
4696
+ const storePath = path.join(EFFECTIVE_DIR, 'content-store');
4697
+ try {
4698
+ const storage = require('./storage/index.js');
4699
+ storage.getBackend(storePath);
4700
+ const info = storage.getBackendInfo(storePath);
4701
+ if (!info) {
4702
+ warn('Storage backend: not initialized');
4703
+ issues++;
4704
+ } else if (info.type === 'sqlite' && !info.fallback) {
4705
+ pass(`Storage backend: sqlite`);
4706
+ } else if (info.type === 'json' && info.fallback) {
4707
+ warn('Storage backend: JSON (fallback — SQLite failed to load)');
4708
+ console.log(' Install better-sqlite3 or rebuild the container image');
4709
+ issues++;
4710
+ } else if (info.type === 'json') {
4711
+ warn('Storage backend: JSON (not SQLite)');
4712
+ issues++;
4713
+ } else {
4714
+ pass(`Storage backend: ${info.type}`);
4715
+ }
4716
+ } catch (e) {
4717
+ warn(`Storage backend: error — ${e.message}`);
4718
+ issues++;
4719
+ }
4720
+
4721
+ // 2. Entry count — are there entries in the content store?
4722
+ let entryCount = 0;
4723
+ try {
4724
+ const storage = require('./storage/index.js');
4725
+ const backend = storage.getBackend(storePath);
4726
+ const idx = backend.loadIndex();
4727
+ if (idx && idx.entries) {
4728
+ entryCount = Object.keys(idx.entries).length;
4729
+ }
4730
+ if (entryCount > 0) {
4731
+ pass(`Content store: ${entryCount} entries`);
4732
+ } else {
4733
+ warn('Content store: 0 entries — no signals have been indexed');
4734
+ console.log(' Run: wayfind pull --all');
4735
+ issues++;
4736
+ }
4737
+ } catch (e) {
4738
+ warn(`Content store: error — ${e.message}`);
4739
+ issues++;
4740
+ }
4741
+
4742
+ // 3. Embedding coverage — what % of entries have embeddings?
4743
+ if (entryCount > 0) {
4744
+ try {
4745
+ const storage = require('./storage/index.js');
4746
+ const backend = storage.getBackend(storePath);
4747
+ const idx = backend.loadIndex();
4748
+ let total = 0, embedded = 0;
4749
+ if (idx && idx.entries) {
4750
+ for (const e of Object.values(idx.entries)) {
4751
+ total++;
4752
+ if (e.hasEmbedding) embedded++;
4753
+ }
4754
+ }
4755
+ const pct = total > 0 ? Math.round(100 * embedded / total) : 0;
4756
+ if (pct >= 50) {
4757
+ pass(`Embedding coverage: ${embedded}/${total} (${pct}%)`);
4758
+ } else {
4759
+ warn(`Embedding coverage: ${embedded}/${total} (${pct}%) — search quality degraded`);
4760
+ console.log(' Run: wayfind reindex');
4761
+ issues++;
4762
+ }
4763
+ } catch (e) {
4764
+ warn(`Embedding coverage: error — ${e.message}`);
4765
+ issues++;
4766
+ }
4767
+ }
4768
+
4769
+ // 4. Signal freshness — are there signal files from today?
4770
+ const signalsDir = path.join(EFFECTIVE_DIR, 'signals');
4771
+ const today = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
4772
+ try {
4773
+ if (!fs.existsSync(signalsDir)) {
4774
+ warn('Signal freshness: no signals directory');
4775
+ issues++;
4776
+ } else {
4777
+ const channels = fs.readdirSync(signalsDir).filter((f) => {
4778
+ try { return fs.statSync(path.join(signalsDir, f)).isDirectory(); } catch { return false; }
4779
+ });
4780
+ if (channels.length === 0) {
4781
+ warn('Signal freshness: no signal channels found');
4782
+ issues++;
4783
+ } else {
4784
+ let freshCount = 0;
4785
+ for (const ch of channels) {
4786
+ const chDir = path.join(signalsDir, ch);
4787
+ const files = fs.readdirSync(chDir).filter((f) => f.endsWith('.md')).sort().reverse();
4788
+ const newest = files[0] || '';
4789
+ // Signal files are named with date prefix, e.g. 2026-03-28-....md
4790
+ if (newest.startsWith(today)) {
4791
+ freshCount++;
4792
+ }
4793
+ }
4794
+ if (freshCount === channels.length) {
4795
+ pass(`Signal freshness: ${freshCount}/${channels.length} channels have signals from today`);
4796
+ } else {
4797
+ warn(`Signal freshness: ${freshCount}/${channels.length} channels have signals from today`);
4798
+ console.log(' Run: wayfind pull --all');
4799
+ issues++;
4800
+ }
4801
+ }
4802
+ }
4803
+ } catch (e) {
4804
+ warn(`Signal freshness: error — ${e.message}`);
4805
+ issues++;
4806
+ }
4807
+
4808
+ // 5. Slack bot / health endpoint — is /healthz responding?
4809
+ const healthPort = parseInt(process.env.TEAM_CONTEXT_HEALTH_PORT || '3141', 10);
4810
+ try {
4811
+ const healthResult = await new Promise((resolve) => {
4812
+ const req = http.get(`http://localhost:${healthPort}/healthz`, { timeout: 3000 }, (res) => {
4813
+ let body = '';
4814
+ res.on('data', (chunk) => { body += chunk; });
4815
+ res.on('end', () => resolve({ status: res.statusCode, body }));
4816
+ });
4817
+ req.on('error', (e) => resolve({ status: 0, error: e.message }));
4818
+ req.on('timeout', () => { req.destroy(); resolve({ status: 0, error: 'timeout' }); });
4819
+ });
4820
+
4821
+ if (healthResult.status === 200) {
4822
+ let detail = '';
4823
+ try {
4824
+ const data = JSON.parse(healthResult.body);
4825
+ if (data.slack && data.slack.connected) {
4826
+ detail = ' (Slack connected)';
4827
+ } else if (data.slack && !data.slack.connected) {
4828
+ detail = ' (Slack disconnected)';
4829
+ }
4830
+ } catch { /* ignore parse errors */ }
4831
+ pass(`Health endpoint: http://localhost:${healthPort}/healthz${detail}`);
4832
+ } else if (healthResult.status > 0) {
4833
+ warn(`Health endpoint: responded with ${healthResult.status}`);
4834
+ issues++;
4835
+ } else {
4836
+ warn(`Health endpoint: not reachable — ${healthResult.error}`);
4837
+ console.log(' Is the container running? Check: docker ps');
4838
+ issues++;
4839
+ }
4840
+ } catch (e) {
4841
+ warn(`Health endpoint: error — ${e.message}`);
4842
+ issues++;
4843
+ }
4844
+
4845
+ console.log('');
4846
+ console.log('══════════════════════════════');
4847
+ if (issues === 0) {
4848
+ console.log(`${GREEN}All container checks passed${RESET}`);
4849
+ } else {
4850
+ console.log(`${YELLOW}${issues} issue(s) found${RESET}`);
4851
+ process.exit(1);
4852
+ }
4853
+ }
4854
+
4667
4855
  // ── Command registry ────────────────────────────────────────────────────────
4668
4856
 
4669
4857
  const COMMANDS = {
@@ -4777,6 +4965,22 @@ const COMMANDS = {
4777
4965
  console.log('Recreating container with new image...');
4778
4966
  spawnSync('docker', ['compose', 'up', '-d'], { cwd: composeDir, stdio: 'inherit' });
4779
4967
  console.log('Container updated.');
4968
+
4969
+ // Post-deploy smoke check: scan container logs for warnings/errors
4970
+ console.log('\nRunning post-deploy smoke check (waiting 5s for container startup)...');
4971
+ spawnSync('sleep', ['5']);
4972
+ const logsResult = spawnSync('docker', ['logs', '--tail', '50', 'wayfind'], { stdio: 'pipe' });
4973
+ const logOutput = (logsResult.stdout || '').toString() + (logsResult.stderr || '').toString();
4974
+ const warningPatterns = /Warning:|Error:|failed|fallback/i;
4975
+ const logLines = logOutput.split('\n').filter(l => l.trim());
4976
+ const warnings = logLines.filter(l => warningPatterns.test(l));
4977
+ if (warnings.length > 0) {
4978
+ console.log('');
4979
+ warnings.forEach(w => console.log(` \u26A0 ${w.trim()}`));
4980
+ console.log('\n Post-deploy warnings detected \u2014 review above');
4981
+ } else {
4982
+ console.log(' Post-deploy check: no warnings');
4983
+ }
4780
4984
  } else {
4781
4985
  console.error('Docker pull failed — container not updated.');
4782
4986
  }
@@ -4829,9 +5033,13 @@ const COMMANDS = {
4829
5033
  run: (args) => runMigrateToPlugin(args),
4830
5034
  },
4831
5035
  doctor: {
4832
- desc: 'Check your Wayfind installation for issues',
5036
+ desc: 'Check your Wayfind installation for issues (--container for container checks)',
4833
5037
  run: (args) => {
4834
- spawn('bash', [path.join(ROOT, 'doctor.sh'), ...args]);
5038
+ if (args.includes('--container') || isRunningInContainer()) {
5039
+ runContainerDoctor();
5040
+ } else {
5041
+ spawn('bash', [path.join(ROOT, 'doctor.sh'), ...args]);
5042
+ }
4835
5043
  },
4836
5044
  },
4837
5045
  version: {
@@ -5210,6 +5418,7 @@ function showHelp() {
5210
5418
  console.log(' wayfind update Update from npm, re-sync hooks, update container');
5211
5419
  console.log(' wayfind migrate-to-plugin Remove old hooks/commands — let the plugin handle them');
5212
5420
  console.log(' wayfind doctor Check installation health');
5421
+ console.log(' wayfind doctor --container Container-specific health checks (auto-detected in Docker)');
5213
5422
  console.log('');
5214
5423
  console.log('Publishing:');
5215
5424
  console.log(' wayfind sync-public Sync code to usewayfind/wayfind (triggers npm + Docker publish)');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wayfind",
3
- "version": "2.0.38",
3
+ "version": "2.0.39",
4
4
  "description": "Team decision trail for AI-assisted development. The connective tissue between product, engineering, and strategy.",
5
5
  "bin": {
6
6
  "wayfind": "./bin/team-context.js"