@feltdb/core 0.6.9 → 0.6.11

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 (122) hide show
  1. package/dist/cli/application-lifecycle.js +112 -0
  2. package/dist/cli/application.js +10 -0
  3. package/dist/cli/commands.js +407 -119
  4. package/dist/cli/development-session.js +54 -0
  5. package/dist/cli/index.js +1 -1
  6. package/dist/cli/workspace-integration.js +146 -20
  7. package/dist/collection.d.ts +110 -2
  8. package/dist/collection.d.ts.map +1 -1
  9. package/dist/collection.js +228 -3
  10. package/dist/create/cli.js +4 -2
  11. package/dist/create/create.js +42 -6
  12. package/dist/create/frameworks.js +18 -0
  13. package/dist/create/package-versions.js +1 -1
  14. package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +408 -178
  15. package/dist/create/server-source/crates/feltdb/src/lib.rs +49 -5
  16. package/dist/create/server-source/crates/feltdb/src/multi_operation_transaction.rs +659 -0
  17. package/dist/create/server-source/crates/feltdb/src/p1_application_atomicity.rs +285 -0
  18. package/dist/create/server-source/crates/feltdb/src/p1_atomicity_acceptance.rs +582 -0
  19. package/dist/create/server-source/crates/feltdb-server/src/main.rs +141 -0
  20. package/dist/create/workspace-initialization.js +4 -5
  21. package/dist/db.d.ts +63 -1
  22. package/dist/db.d.ts.map +1 -1
  23. package/dist/db.js +145 -4
  24. package/dist/development-runtime-bridge.d.ts +60 -0
  25. package/dist/development-runtime-bridge.d.ts.map +1 -0
  26. package/dist/development-runtime-bridge.js +171 -0
  27. package/dist/embedded-transaction.d.ts +77 -0
  28. package/dist/embedded-transaction.d.ts.map +1 -0
  29. package/dist/embedded-transaction.js +102 -0
  30. package/dist/feltdb.d.ts +49 -1
  31. package/dist/feltdb.d.ts.map +1 -1
  32. package/dist/file-db.d.ts +33 -0
  33. package/dist/file-db.d.ts.map +1 -1
  34. package/dist/file-db.js +128 -4
  35. package/dist/freshness.d.ts +119 -0
  36. package/dist/freshness.d.ts.map +1 -0
  37. package/dist/freshness.js +115 -0
  38. package/dist/http-db.d.ts +48 -0
  39. package/dist/http-db.d.ts.map +1 -1
  40. package/dist/http-db.js +77 -1
  41. package/dist/index-backend.d.ts +8 -0
  42. package/dist/index-backend.d.ts.map +1 -1
  43. package/dist/index-backend.js +13 -0
  44. package/dist/index-core.d.ts +80 -0
  45. package/dist/index-core.d.ts.map +1 -0
  46. package/dist/index-core.js +74 -0
  47. package/dist/index-manager.d.ts +11 -0
  48. package/dist/index-manager.d.ts.map +1 -1
  49. package/dist/index-manager.js +30 -13
  50. package/dist/index-types.d.ts +17 -0
  51. package/dist/index-types.d.ts.map +1 -1
  52. package/dist/index.browser.d.ts +14 -0
  53. package/dist/index.browser.d.ts.map +1 -0
  54. package/dist/index.browser.js +13 -0
  55. package/dist/index.d.ts +8 -59
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +11 -54
  58. package/dist/indexeddb-db.d.ts +32 -0
  59. package/dist/indexeddb-db.d.ts.map +1 -1
  60. package/dist/indexeddb-db.js +151 -1
  61. package/dist/memory-db.d.ts +26 -0
  62. package/dist/memory-db.d.ts.map +1 -1
  63. package/dist/memory-db.js +86 -0
  64. package/dist/runtime-registry.d.ts +32 -0
  65. package/dist/runtime-registry.d.ts.map +1 -0
  66. package/dist/runtime-registry.js +38 -0
  67. package/dist/studio/app.d.ts +2 -1
  68. package/dist/studio/app.d.ts.map +1 -1
  69. package/dist/studio/components/StateExplorer.d.ts +2 -1
  70. package/dist/studio/components/StateExplorer.d.ts.map +1 -1
  71. package/dist/studio/components/index.js +1 -1
  72. package/dist/studio/{components-9kDSWiGL.js → components-C5p2TfIU.js} +25 -14
  73. package/dist/studio/index.js +234 -160
  74. package/dist/studio-app/assets/{feltdb_wasm-B4wq4mqp.js → feltdb_wasm-CJv3wHzi.js} +1 -1
  75. package/dist/studio-app/assets/feltdb_wasm_bg-C8TG8r2n.wasm +0 -0
  76. package/dist/studio-app/assets/index-DospFFYE.js +28 -0
  77. package/dist/studio-app/index.html +1 -1
  78. package/dist/transaction.d.ts +98 -0
  79. package/dist/transaction.d.ts.map +1 -0
  80. package/dist/transaction.js +86 -0
  81. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  82. package/dist/workspace/browser.d.ts +22 -0
  83. package/dist/workspace/browser.d.ts.map +1 -0
  84. package/dist/workspace/browser.js +17 -0
  85. package/dist/workspace/development-node.d.ts +16 -0
  86. package/dist/workspace/development-node.d.ts.map +1 -1
  87. package/dist/workspace/development-node.js +85 -1
  88. package/dist/workspace/git-identity.d.ts +35 -0
  89. package/dist/workspace/git-identity.d.ts.map +1 -0
  90. package/dist/workspace/git-identity.js +81 -0
  91. package/dist/workspace/index.d.ts +12 -2
  92. package/dist/workspace/index.d.ts.map +1 -1
  93. package/dist/workspace/index.js +7 -1
  94. package/dist/workspace/investigation-analysis.d.ts +47 -0
  95. package/dist/workspace/investigation-analysis.d.ts.map +1 -0
  96. package/dist/workspace/investigation-analysis.js +181 -0
  97. package/dist/workspace/investigation-lifecycle-manager.d.ts +88 -0
  98. package/dist/workspace/investigation-lifecycle-manager.d.ts.map +1 -0
  99. package/dist/workspace/investigation-lifecycle-manager.js +259 -0
  100. package/dist/workspace/investigation-supervisor.d.ts +100 -0
  101. package/dist/workspace/investigation-supervisor.d.ts.map +1 -0
  102. package/dist/workspace/investigation-supervisor.js +238 -0
  103. package/dist/workspace/runtime-investigation.d.ts +5 -1
  104. package/dist/workspace/runtime-investigation.d.ts.map +1 -1
  105. package/dist/workspace/runtime-investigation.js +50 -21
  106. package/dist/workspace/runtime-observation.d.ts +82 -0
  107. package/dist/workspace/runtime-observation.d.ts.map +1 -0
  108. package/dist/workspace/runtime-observation.js +170 -0
  109. package/dist/workspace/runtime-observer.d.ts +57 -0
  110. package/dist/workspace/runtime-observer.d.ts.map +1 -0
  111. package/dist/workspace/runtime-observer.js +272 -0
  112. package/dist/workspace/workspace-connection.d.ts.map +1 -1
  113. package/dist/workspace/workspace-connection.js +14 -5
  114. package/dist/workspace/workspace-identity.d.ts +1 -1
  115. package/dist/workspace/workspace-identity.d.ts.map +1 -1
  116. package/dist/workspace/workspace-identity.js +4 -4
  117. package/dist/workspace/workspace-types.d.ts +208 -0
  118. package/dist/workspace/workspace-types.d.ts.map +1 -1
  119. package/package.json +12 -4
  120. package/dist/create/server-source/crates/feltdb/src/phase_1c_three_node.rs +0 -523
  121. package/dist/studio-app/assets/feltdb_wasm_bg-Ceyi7l21.wasm +0 -0
  122. package/dist/studio-app/assets/index-BF45M757.js +0 -28
@@ -7,9 +7,12 @@ import http from 'http';
7
7
  import net from 'net';
8
8
  import { createRequire } from 'module';
9
9
  import { spawn, spawnSync } from 'child_process';
10
- import { createFeltDB, diffFlowSpec, formatFlowSpec, parseFlowSpec, planFlowSpecMigration, startLocalDevelopmentAuthority, validateFlowSpec } from '@feltdb/core';
10
+ import { randomBytes } from 'crypto';
11
+ import { createFeltDB, diffFlowSpec, formatFlowSpec, InvestigationLifecycleManager, parseFlowSpec, planFlowSpecMigration, startLocalDevelopmentAuthority, validateFlowSpec } from '@feltdb/core';
11
12
  import { discoverWorkspace, ensureWorkspaceGitIgnored, generatePairingToken, persistPairingToken, displayWorkspaceStatus, initializeWorkspace, startPairingDiscoveryServer } from './workspace-integration.js';
12
- import { detectApplication, discoverApplicationUrl } from './application.js';
13
+ import { applicationUrlCandidate, detectApplication, discoverApplicationUrl } from './application.js';
14
+ import { resolveApplicationLifecycle, startManagedApplication, waitForManagedApplication } from './application-lifecycle.js';
15
+ import { createDevelopmentSession, developmentSessionEnvironment, developmentSessionSummary, transitionDevelopmentSession } from './development-session.js';
13
16
  function loadProjectEnvironment(file = path.resolve('.env.local')) {
14
17
  if (!fs.existsSync(file))
15
18
  return;
@@ -55,6 +58,8 @@ export async function handleCommand(command, args) {
55
58
  return handleWorkspace(args);
56
59
  case 'dev':
57
60
  return handleDev(args);
61
+ case 'investigations':
62
+ return handleInvestigations(args);
58
63
  case 'build':
59
64
  return handleBuild(args);
60
65
  case 'check':
@@ -76,6 +81,92 @@ export async function handleCommand(command, args) {
76
81
  return handleHelp();
77
82
  }
78
83
  }
84
+ /**
85
+ * Inspect the durable investigation store.
86
+ *
87
+ * The evidence chain is only useful if a developer can get back to it after
88
+ * the browser is closed, so it is queryable from the command line.
89
+ */
90
+ function handleInvestigations(args) {
91
+ if (args.includes('--help')) {
92
+ console.log('Usage: feltdb investigations [<investigation-id>] [--json]\n');
93
+ console.log('List durable runtime investigations, or show one in full.\n');
94
+ console.log('Options:');
95
+ console.log(' --json Emit machine-readable JSON');
96
+ return;
97
+ }
98
+ const store = new InvestigationLifecycleManager(process.cwd());
99
+ const json = args.includes('--json');
100
+ const id = args.find(value => !value.startsWith('--'));
101
+ if (id) {
102
+ const investigation = store.loadInvestigation(id);
103
+ if (!investigation)
104
+ throw new Error(`Investigation not found: ${id}`);
105
+ if (json) {
106
+ console.log(JSON.stringify(investigation, null, 2));
107
+ return;
108
+ }
109
+ const observation = investigation.originalObservation;
110
+ console.log(`\nInvestigation ${investigation.id}`);
111
+ console.log(` Workspace: ${investigation.workspaceId}`);
112
+ console.log(` Investigation ${investigation.investigationState}`);
113
+ console.log(` Remediation: ${investigation.remediationState}`);
114
+ console.log(` Verification: ${investigation.verificationState}\n`);
115
+ if (observation) {
116
+ console.log('Observation');
117
+ console.log(` ${observation.method} ${observation.url} -> HTTP ${observation.status}`);
118
+ console.log(` ${observation.browser || 'unknown'} / ${observation.runtime || 'unknown'}`
119
+ + `${observation.durationMs !== undefined ? ` / ${observation.durationMs}ms` : ''}\n`);
120
+ }
121
+ if (investigation.evidence?.length) {
122
+ console.log('Evidence (observed by the runtime)');
123
+ for (const item of investigation.evidence)
124
+ console.log(` [${item.kind}] ${item.statement}`);
125
+ console.log();
126
+ }
127
+ if (investigation.finding) {
128
+ console.log('Finding (FeltDB analysis, not an observation)');
129
+ console.log(` ${investigation.finding.statement}`);
130
+ console.log(` confidence: ${investigation.finding.confidence}\n`);
131
+ }
132
+ if (investigation.remediation?.changeIdentity) {
133
+ console.log('Remediation');
134
+ console.log(` ${investigation.remediation.changeKind}: ${investigation.remediation.changeIdentity}`);
135
+ if (investigation.remediation.changedPaths.length) {
136
+ console.log(` changed: ${investigation.remediation.changedPaths.join(', ')}`);
137
+ }
138
+ console.log();
139
+ }
140
+ if (investigation.verificationAttempt) {
141
+ const attempt = investigation.verificationAttempt;
142
+ console.log('Verification');
143
+ console.log(` ${attempt.result}: ${attempt.summary}`);
144
+ console.log(` ${attempt.originalStatus} -> ${attempt.observedStatus ?? 'no matching observation'}\n`);
145
+ }
146
+ return;
147
+ }
148
+ const investigations = store.listInvestigations();
149
+ if (json) {
150
+ console.log(JSON.stringify(investigations, null, 2));
151
+ return;
152
+ }
153
+ if (investigations.length === 0) {
154
+ console.log('\nNo investigations recorded.');
155
+ console.log('Run "feltdb dev", exercise the application, and a runtime defect will open one.\n');
156
+ return;
157
+ }
158
+ console.log(`\n${investigations.length} investigation${investigations.length === 1 ? '' : 's'}\n`);
159
+ for (const investigation of investigations) {
160
+ const observation = investigation.originalObservation;
161
+ const summary = observation
162
+ ? `${observation.method} ${observation.url} -> ${observation.status}`
163
+ : investigation.observationId;
164
+ console.log(` ${investigation.id} ${investigation.verificationState.padEnd(12)} ${summary}`);
165
+ }
166
+ const metrics = store.metrics();
167
+ console.log(`\n ${metrics.observations} observations, ${metrics.evidenceRecords} evidence records, `
168
+ + `${metrics.serializedBytes} bytes, loaded in ${metrics.loadMilliseconds}ms\n`);
169
+ }
79
170
  function flowFile(args) {
80
171
  const candidate = args.find(value => !value.startsWith('--')) || 'feltdb.flow';
81
172
  return path.resolve(candidate);
@@ -414,37 +505,30 @@ async function handleWorkspaceConnect(args) {
414
505
  console.log(' This feature will be implemented in the next phase');
415
506
  console.log(' (when @feltdb/client workspace APIs are available)\n');
416
507
  }
417
- function runLocalVite(args, waitForExit) {
418
- const executable = process.platform === 'win32' ? 'npm.cmd' : 'npm';
419
- const child = spawn(executable, ['exec', '--', 'vite', ...args], {
420
- cwd: process.cwd(),
421
- stdio: 'inherit',
422
- env: process.env,
423
- });
424
- if (!waitForExit)
425
- return child;
426
- return new Promise((resolve, reject) => {
427
- child.once('error', reject);
428
- child.once('exit', code => code === 0 ? resolve() : reject(new Error(`Vite exited with status ${code ?? 'unknown'}`)));
429
- });
430
- }
431
508
  async function handleDev(args) {
432
509
  var _a, _b, _c;
510
+ const projectDir = process.cwd();
433
511
  if (args.includes('--help')) {
434
- console.log('Usage: feltdb dev [--app-url URL | --app-port PORT] [--studio-port 7701] [--authority-port 7700] [--discovery-port 7799] [--no-open]');
512
+ console.log('Usage: feltdb dev [--app-url URL] [--studio-port 7701] [--authority-port 7700] [--discovery-port 7799] [--no-open]');
513
+ console.log('\nOptions:');
514
+ console.log(' --app-url <url> Application URL (e.g., http://127.0.0.1:3000)');
515
+ console.log(' --studio-port <port> FeltDB Studio port (default: 7701)');
516
+ console.log(' --authority-port <port> Authority port (default: 7700)');
517
+ console.log(' --discovery-port <port> Pairing discovery port (default: 7799)');
518
+ console.log(' --application-timeout <ms> Managed application readiness timeout (default: 15000)');
519
+ console.log(' --no-open Do not open Studio in browser');
435
520
  return;
436
521
  }
437
522
  loadProjectEnvironment();
438
523
  console.log('🚀 Starting FeltDB development server...\n');
439
- const projectDir = process.cwd();
440
- const configPath = path.join(projectDir, 'feltdb.config.json');
441
524
  // .feltdb/ is reserved local runtime state. Protect it before discovery,
442
525
  // initialization, the authority, or pairing can persist anything there.
526
+ // This runs after `--help` returns, so asking for help never writes anything.
443
527
  ensureWorkspaceGitIgnored(projectDir);
528
+ const configPath = path.join(projectDir, 'feltdb.config.json');
444
529
  // Initialize workspace if needed
445
530
  let workspace = discoverWorkspace(projectDir);
446
531
  if (!workspace) {
447
- // Get project name from package.json
448
532
  const packageJsonPath = path.join(projectDir, 'package.json');
449
533
  let projectId = path.basename(projectDir);
450
534
  if (fs.existsSync(packageJsonPath)) {
@@ -460,31 +544,20 @@ async function handleDev(args) {
460
544
  console.log(`✨ Initialized Development Workspace`);
461
545
  console.log(` Workspace ID: ${workspace.workspaceId}\n`);
462
546
  }
463
- // Create feltdb.config.json if it doesn't exist
464
- if (!fs.existsSync(configPath)) {
465
- const packageJsonPath = path.join(projectDir, 'package.json');
466
- let namespace = path.basename(projectDir);
467
- if (fs.existsSync(packageJsonPath)) {
468
- try {
469
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
470
- namespace = packageJson.name || namespace;
471
- }
472
- catch {
473
- // Use default namespace if package.json parsing fails
474
- }
475
- }
476
- const defaultConfig = {
477
- namespace,
478
- runtime: 'browser',
479
- storage: 'indexeddb',
480
- distributed: true,
481
- agents: { enabled: false },
482
- capabilities: { search: true },
483
- };
484
- fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2) + '\n');
485
- console.log(`✨ Created feltdb.config.json with default configuration\n`);
486
- }
487
- const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
547
+ // Investigation lifecycle manager will be initialized when needed
548
+ // const investigationManager = new InvestigationLifecycleManager(projectDir);
549
+ // Existing applications may attach without adopting generated FeltDB source.
550
+ // Missing developer-owned config is resolved in memory and never written.
551
+ const configDefaults = {
552
+ namespace: workspace.projectId,
553
+ runtime: 'browser',
554
+ storage: 'indexeddb',
555
+ distributed: true,
556
+ applicationUrl: null,
557
+ };
558
+ const config = fs.existsSync(configPath)
559
+ ? { ...configDefaults, ...JSON.parse(fs.readFileSync(configPath, 'utf-8')) }
560
+ : configDefaults;
488
561
  if (config.runtime === 'managed') {
489
562
  (_a = process.env).VITE_FELTDB_URL || (_a.VITE_FELTDB_URL = process.env.VITE_FELTDB_MANAGED_URL);
490
563
  (_b = process.env).VITE_FELTDB_API_KEY || (_b.VITE_FELTDB_API_KEY = process.env.VITE_FELTDB_MANAGED_API_KEY);
@@ -495,17 +568,16 @@ async function handleDev(args) {
495
568
  const runtimeNamespace = config.runtime === 'managed'
496
569
  ? process.env.VITE_FELTDB_MANAGED_NAMESPACE || config.namespace
497
570
  : config.namespace;
498
- const hasAppUrl = args.includes('--app-url');
499
- const appUrlArgument = hasAppUrl ? args[args.indexOf('--app-url') + 1] : undefined;
500
- if (hasAppUrl && !appUrlArgument)
571
+ const applicationLifecycle = resolveApplicationLifecycle(config);
572
+ // Application URL precedence:
573
+ // 1. --app-url (highest priority)
574
+ // 2. feltdb.config.json applicationUrl
575
+ // 3. Deterministically discovered running application
576
+ // 4. No application (lowest priority - allowed)
577
+ const hasAppUrlArg = args.includes('--app-url');
578
+ const appUrlArgument = hasAppUrlArg ? args[args.indexOf('--app-url') + 1] : undefined;
579
+ if (hasAppUrlArg && !appUrlArgument)
501
580
  throw new Error('--app-url requires a URL');
502
- const hasAppPort = args.includes('--app-port') || args.includes('--port');
503
- const appPortValue = args.includes('--app-port')
504
- ? args[args.indexOf('--app-port') + 1]
505
- : args.includes('--port') ? args[args.indexOf('--port') + 1] : undefined;
506
- if (!hasAppUrl && hasAppPort && !appPortValue)
507
- throw new Error('--app-port requires a port');
508
- const appPort = appPortValue ? Number(appPortValue) : undefined;
509
581
  if (appUrlArgument) {
510
582
  try {
511
583
  const parsed = new URL(appUrlArgument);
@@ -516,31 +588,54 @@ async function handleDev(args) {
516
588
  throw new Error(`Invalid --app-url: ${appUrlArgument}`);
517
589
  }
518
590
  }
519
- if (!hasAppUrl && appPortValue && (!Number.isInteger(appPort) || appPort < 1 || appPort > 65535))
520
- throw new Error(`Invalid --app-port: ${appPortValue}`);
521
- let discoveredApplication;
522
- if (!hasAppUrl && !appPort) {
523
- discoveredApplication = await discoverApplicationUrl(projectDir);
524
- }
525
- else {
591
+ let appUrl;
592
+ let detectedApplication;
593
+ // Priority 1: --app-url takes highest priority
594
+ if (appUrlArgument) {
595
+ appUrl = appUrlArgument;
526
596
  try {
527
- discoveredApplication = { project: detectApplication(projectDir) };
597
+ detectedApplication = detectApplication(projectDir);
528
598
  }
529
599
  catch {
530
- discoveredApplication = { project: { packageManager: 'npm', framework: 'External' } };
600
+ detectedApplication = { packageManager: 'npm', framework: 'External', feltDBEnabled: false, feltDBSignals: [] };
601
+ }
602
+ }
603
+ else {
604
+ // Priority 2: Check feltdb.config.json applicationUrl
605
+ if (config.applicationUrl) {
606
+ appUrl = config.applicationUrl;
607
+ try {
608
+ detectedApplication = detectApplication(projectDir);
609
+ }
610
+ catch {
611
+ detectedApplication = { packageManager: 'npm', framework: 'External', feltDBEnabled: false, feltDBSignals: [] };
612
+ }
613
+ }
614
+ else {
615
+ // Priority 3: Try to discover an existing running application
616
+ if (applicationLifecycle === 'managed') {
617
+ detectedApplication = detectApplication(projectDir);
618
+ appUrl = applicationUrlCandidate(detectedApplication);
619
+ }
620
+ else {
621
+ const discovered = await discoverApplicationUrl(projectDir);
622
+ detectedApplication = discovered.project;
623
+ appUrl = discovered.url;
624
+ }
531
625
  }
532
626
  }
533
- const appUrl = appUrlArgument || (appPort ? `http://127.0.0.1:${appPort}` : discoveredApplication.url);
534
- if (!appUrl) {
535
- throw new Error('No application URL was detected.\nStart your application and run:\n feltdb dev --app-url http://127.0.0.1:<port>\nOr configure applicationUrl in feltdb.config.json.');
627
+ if (detectedApplication?.feltDBEnabled) {
628
+ console.log(`Attaching existing FeltDB application (${detectedApplication.feltDBSignals.join(', ')})\n`);
536
629
  }
537
630
  const requestedStudioPort = Number(args.includes('--studio-port') ? args[args.indexOf('--studio-port') + 1] || '7701' : '7701');
538
631
  const studioPort = String(await availablePort(requestedStudioPort));
539
632
  if (studioPort !== String(requestedStudioPort))
540
- console.log(`Port ${requestedStudioPort} is busy; using ${studioPort} for Studio.`);
633
+ console.log(`⚠ Port ${requestedStudioPort} is busy; using ${studioPort} for Studio.\n`);
541
634
  process.env.STUDIO_PORT = studioPort;
542
635
  let selfHostedStarted = false;
543
636
  let localAuthority = null;
637
+ let managedApplication = null;
638
+ let developmentSession = null;
544
639
  const stopSelfHosted = () => {
545
640
  if (selfHostedStarted) {
546
641
  spawnSync('docker', ['compose', 'stop', 'feltdb'], { cwd: process.cwd(), stdio: 'ignore' });
@@ -570,62 +665,184 @@ async function handleDev(args) {
570
665
  });
571
666
  process.env.VITE_FELTDB_URL = localAuthority.endpoint;
572
667
  }
668
+ const authorityUrl = process.env.VITE_FELTDB_URL;
669
+ if (!authorityUrl)
670
+ throw new Error('Development session authority URL was not resolved');
671
+ const discoveryPort = Number(args.includes('--discovery-port') ? args[args.indexOf('--discovery-port') + 1] || '7799' : '7799');
672
+ developmentSession = createDevelopmentSession({
673
+ sessionId: `dev_${randomBytes(16).toString('hex')}`,
674
+ workspaceId: workspace.workspaceId,
675
+ projectId: workspace.projectId,
676
+ namespace: runtimeNamespace || config.namespace,
677
+ runtime: config.runtime,
678
+ storage: config.storage,
679
+ lifecycle: applicationLifecycle,
680
+ authorityUrl,
681
+ studioUrl: `http://127.0.0.1:${studioPort}`,
682
+ pairingUrl: `http://127.0.0.1:${discoveryPort}`,
683
+ applicationUrl: appUrl,
684
+ });
685
+ transitionDevelopmentSession(developmentSession, 'STARTING');
573
686
  // Generate pairing token for the workspace
574
687
  // This enables browsers and IDEs to discover and connect to the workspace
575
688
  let pairingToken = null;
576
689
  let pairingDiscoveryServer = null;
577
- if (workspace) {
578
- const token = generatePairingToken();
579
- token.workspaceId = workspace.workspaceId;
580
- token.endpoint = process.env.VITE_FELTDB_URL;
581
- token.authorityEndpoint = token.endpoint;
582
- token.namespace = runtimeNamespace || config.namespace;
583
- if (!token.endpoint) {
584
- throw new Error('Development Workspace pairing requires a FeltDB authority endpoint');
690
+ try {
691
+ if (workspace) {
692
+ const token = generatePairingToken();
693
+ token.workspaceId = developmentSession.workspaceId;
694
+ token.endpoint = developmentSession.authorityUrl;
695
+ token.authorityEndpoint = token.endpoint;
696
+ token.namespace = developmentSession.namespace;
697
+ if (!token.endpoint) {
698
+ throw new Error('Development Workspace pairing requires a FeltDB authority endpoint');
699
+ }
700
+ persistPairingToken(process.cwd(), token);
701
+ pairingDiscoveryServer = await startPairingDiscoveryServer(token, discoveryPort, '127.0.0.1', {
702
+ sessionId: developmentSession.sessionId,
703
+ workspaceId: developmentSession.workspaceId,
704
+ namespace: developmentSession.namespace,
705
+ runtime: developmentSession.runtime,
706
+ authorityUrl: developmentSession.authorityUrl,
707
+ applicationUrl: developmentSession.applicationUrl,
708
+ });
709
+ const pairingAddress = pairingDiscoveryServer.address();
710
+ developmentSession.pairingUrl = `http://127.0.0.1:${pairingAddress.port}`;
711
+ pairingToken = token.token;
585
712
  }
586
- persistPairingToken(process.cwd(), token);
587
- const discoveryPort = Number(args.includes('--discovery-port') ? args[args.indexOf('--discovery-port') + 1] || '7799' : '7799');
588
- pairingDiscoveryServer = await startPairingDiscoveryServer(token, discoveryPort);
589
- pairingToken = token.token;
590
713
  }
591
- console.log('FeltDB Dev Server');
592
- console.log(` Authority: ${process.env.VITE_FELTDB_URL}`);
593
- console.log(` Studio: http://127.0.0.1:${studioPort}`);
594
- console.log(` Pairing: http://127.0.0.1:${(pairingDiscoveryServer?.address()).port}`);
595
- console.log('Application');
596
- console.log(` Framework: ${discoveredApplication.project.framework}`);
597
- console.log(' Dev command: (managed externally)');
598
- console.log(` URL: ${appUrl}`);
599
- console.log(` Namespace: ${config.namespace}`);
600
- console.log(` Runtime: ${config.runtime}`);
601
- console.log(` Storage: ${config.storage}`);
602
- console.log(` Distributed: ${config.distributed}`);
603
- if (workspace) {
604
- console.log(` Workspace: ${workspace.workspaceId}\n`);
605
- console.log(`🔗 Development Workspace`);
606
- if (pairingToken) {
607
- console.log(` Pairing Code: ${pairingToken}`);
608
- console.log(` Pairing discovery: http://127.0.0.1:${(pairingDiscoveryServer?.address()).port}`);
609
- console.log(` Clients can use this code to auto-discover the workspace`);
714
+ catch (error) {
715
+ transitionDevelopmentSession(developmentSession, 'FAILED');
716
+ await localAuthority?.close();
717
+ stopSelfHosted();
718
+ throw error;
719
+ }
720
+ let stopping = null;
721
+ const stopAll = () => stopping ?? (stopping = (async () => {
722
+ if (developmentSession && developmentSession.state !== 'STOPPING' && developmentSession.state !== 'STOPPED') {
723
+ transitionDevelopmentSession(developmentSession, 'STOPPING');
610
724
  }
725
+ await managedApplication?.stop();
726
+ if (pairingDiscoveryServer?.listening) {
727
+ await new Promise(resolve => pairingDiscoveryServer.close(() => resolve()));
728
+ }
729
+ await localAuthority?.close();
730
+ stopSelfHosted();
731
+ if (developmentSession?.state === 'STOPPING')
732
+ transitionDevelopmentSession(developmentSession, 'STOPPED');
733
+ })());
734
+ process.once('exit', () => { void stopAll(); });
735
+ process.once('SIGINT', async () => { await stopAll(); process.exit(130); });
736
+ process.once('SIGTERM', async () => { await stopAll(); process.exit(143); });
737
+ if (applicationLifecycle === 'managed') {
738
+ try {
739
+ if (!detectedApplication)
740
+ detectedApplication = detectApplication(projectDir);
741
+ if (!appUrl) {
742
+ throw new Error(`Managed application lifecycle requires an expected application URL from --app-url, feltdb.config.json applicationUrl, FELTDB_APP_URL, or an explicit port in package.json.scripts.dev (workspace: ${workspace.workspaceId}; project: ${projectDir})`);
743
+ }
744
+ const timeoutArgument = args.includes('--application-timeout')
745
+ ? args[args.indexOf('--application-timeout') + 1]
746
+ : undefined;
747
+ const applicationTimeoutMs = timeoutArgument === undefined ? 15000 : Number(timeoutArgument);
748
+ if (!Number.isFinite(applicationTimeoutMs) || applicationTimeoutMs <= 0)
749
+ throw new Error('--application-timeout must be a positive number of milliseconds');
750
+ managedApplication = startManagedApplication({
751
+ projectDir,
752
+ project: detectedApplication,
753
+ environment: { ...process.env, ...developmentSessionEnvironment(developmentSession) },
754
+ });
755
+ console.log(`Starting managed application: ${managedApplication.command}`);
756
+ await waitForManagedApplication({ application: managedApplication, expectedUrl: appUrl, timeoutMs: applicationTimeoutMs, workspaceId: workspace.workspaceId, projectDir });
757
+ console.log(`Managed application ready: ${appUrl}\n`);
758
+ }
759
+ catch (error) {
760
+ transitionDevelopmentSession(developmentSession, 'FAILED');
761
+ await stopAll();
762
+ throw error;
763
+ }
764
+ }
765
+ // Output startup information
766
+ console.log('✨ Development Workspace');
767
+ if (workspace) {
768
+ console.log(` Workspace ID: ${workspace.workspaceId}`);
611
769
  }
612
770
  console.log();
771
+ console.log('🔗 Development Workspace');
772
+ if (pairingToken) {
773
+ console.log(` Pairing Code: ${pairingToken}`);
774
+ console.log(` Pairing discovery: ${developmentSession.pairingUrl}`);
775
+ }
776
+ console.log();
777
+ console.log('FeltDB Dev Server');
778
+ console.log(` Namespace: ${developmentSession.namespace}`);
779
+ console.log(` Runtime: ${developmentSession.runtime}`);
780
+ console.log(` Storage: ${developmentSession.storage}`);
781
+ console.log(` Distributed: ${config.distributed}`);
782
+ console.log(` Application: ${applicationLifecycle}`);
783
+ console.log();
784
+ if (appUrl) {
785
+ console.log(`Application: ${appUrl}`);
786
+ console.log(`Authority: ${developmentSession.authorityUrl}`);
787
+ console.log();
788
+ console.log(`Using existing application at ${appUrl}`);
789
+ console.log(`app=${encodeURIComponent(appUrl)}\n`);
790
+ }
791
+ else {
792
+ console.log('❌ No application URL is configured.\n');
793
+ console.log('Start your application, then run:\n');
794
+ console.log(' feltdb dev --app-url http://127.0.0.1:<port>\n');
795
+ console.log('Or set applicationUrl in feltdb.config.json\n');
796
+ console.log('FeltDB workspace is ready at:');
797
+ console.log('Authority: ' + developmentSession.authorityUrl);
798
+ console.log('Studio: ' + developmentSession.studioUrl);
799
+ console.log();
800
+ }
613
801
  const open = !args.includes('--no-open');
614
- console.log(`Application: ${appUrl}`);
615
- console.log(`Studio: http://127.0.0.1:${studioPort}\n`);
616
- console.log(`Using existing application at ${appUrl}\n`);
617
- const stopAll = () => { pairingDiscoveryServer?.close(); void localAuthority?.close(); stopSelfHosted(); };
618
- process.once('exit', stopAll);
619
- process.once('SIGINT', () => { stopAll(); process.exit(130); });
620
- process.once('SIGTERM', () => { stopAll(); process.exit(143); });
621
- await handleStudio([
622
- '--port', studioPort,
623
- '--namespace', runtimeNamespace || 'default',
624
- '--runtime', config.runtime || 'browser',
625
- '--app-url', appUrl,
626
- ...((config.runtime === 'self-hosted' || config.runtime === 'managed') && process.env.VITE_FELTDB_URL ? ['--connect', process.env.VITE_FELTDB_URL] : []),
627
- ...(open ? [] : ['--no-open']),
628
- ]);
802
+ try {
803
+ const studio = handleStudio([
804
+ '--port', studioPort,
805
+ '--namespace', developmentSession.namespace,
806
+ '--runtime', developmentSession.runtime,
807
+ '--workspace-id', developmentSession.workspaceId,
808
+ '--session-id', developmentSession.sessionId,
809
+ '--project-id', developmentSession.projectId,
810
+ '--authority-url', developmentSession.authorityUrl,
811
+ '--pairing-url', developmentSession.pairingUrl,
812
+ '--lifecycle', developmentSession.lifecycle,
813
+ ...(developmentSession.applicationUrl ? ['--app-url', developmentSession.applicationUrl] : []),
814
+ ...((developmentSession.runtime === 'self-hosted' || developmentSession.runtime === 'managed') ? ['--connect', developmentSession.authorityUrl] : []),
815
+ ...(open ? [] : ['--no-open']),
816
+ ], () => {
817
+ transitionDevelopmentSession(developmentSession, 'READY');
818
+ console.log(`\n${developmentSessionSummary(developmentSession)}\n`);
819
+ transitionDevelopmentSession(developmentSession, 'RUNNING');
820
+ });
821
+ if (managedApplication) {
822
+ await Promise.race([studio, managedApplication.exited.then(exit => {
823
+ throw new Error(`Managed application stopped during the development session (command: ${managedApplication.command}; exit: ${exit.code ?? exit.signal})`);
824
+ })]);
825
+ }
826
+ else
827
+ await studio;
828
+ }
829
+ finally {
830
+ await stopAll();
831
+ }
832
+ }
833
+ function runLocalVite(args, waitForExit) {
834
+ const executable = process.platform === 'win32' ? 'npm.cmd' : 'npm';
835
+ const child = spawn(executable, ['exec', '--', 'vite', ...args], {
836
+ cwd: process.cwd(),
837
+ stdio: 'inherit',
838
+ env: process.env,
839
+ });
840
+ if (!waitForExit)
841
+ return child;
842
+ return new Promise((resolve, reject) => {
843
+ child.once('error', reject);
844
+ child.once('exit', code => code === 0 ? resolve() : reject(new Error(`Vite exited with status ${code ?? 'unknown'}`)));
845
+ });
629
846
  }
630
847
  async function handleBuild(args) {
631
848
  console.log('🔨 Building FeltDB application...\n');
@@ -867,7 +1084,7 @@ async function handleDoctor() {
867
1084
  }
868
1085
  console.log('\n✅ System healthy\n');
869
1086
  }
870
- async function handleStudio(args) {
1087
+ async function handleStudio(args, onReady) {
871
1088
  const connectUrl = args.includes('--connect')
872
1089
  ? args[args.indexOf('--connect') + 1]
873
1090
  : undefined;
@@ -887,6 +1104,19 @@ async function handleStudio(args) {
887
1104
  const appUrl = args.includes('--app-url')
888
1105
  ? args[args.indexOf('--app-url') + 1]
889
1106
  : undefined;
1107
+ const sessionValue = (name) => args.includes(name) ? args[args.indexOf(name) + 1] : undefined;
1108
+ const session = {
1109
+ sessionId: sessionValue('--session-id'),
1110
+ workspaceId: sessionValue('--workspace-id'),
1111
+ projectId: sessionValue('--project-id'),
1112
+ authorityUrl: sessionValue('--authority-url'),
1113
+ pairingUrl: sessionValue('--pairing-url'),
1114
+ lifecycle: sessionValue('--lifecycle'),
1115
+ namespace,
1116
+ runtime,
1117
+ applicationUrl: appUrl,
1118
+ bridgeUrl: sessionValue('--pairing-url'),
1119
+ };
890
1120
  if (connectUrl) {
891
1121
  console.log(`Connecting to: ${connectUrl}`);
892
1122
  console.log(`Remote Studio: http://localhost:${port}\n`);
@@ -897,19 +1127,21 @@ async function handleStudio(args) {
897
1127
  const require = createRequire(import.meta.url);
898
1128
  const packageFile = require.resolve('@feltdb/core/package.json');
899
1129
  const root = path.join(path.dirname(packageFile), 'dist', 'studio-app');
1130
+ if (!fs.existsSync(path.join(root, 'index.html'))) {
1131
+ console.log('⚠️ Studio application artifacts not found.');
1132
+ return;
1133
+ }
900
1134
  const projectFlow = ['feltdb.flow', ...fs.readdirSync(process.cwd()).filter(value => value.endsWith('.flow'))]
901
1135
  .map(value => path.resolve(process.cwd(), value))
902
1136
  .find((value, index, values) => values.indexOf(value) === index && fs.existsSync(value));
903
- if (!fs.existsSync(path.join(root, 'index.html')))
904
- throw new Error('Studio application artifact is missing; reinstall @feltdb/core');
905
1137
  const mime = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8', '.svg': 'image/svg+xml', '.json': 'application/json' };
906
- const server = http.createServer((request, response) => {
1138
+ const server = http.createServer(async (request, response) => {
907
1139
  const pathname = decodeURIComponent(new URL(request.url || '/', 'http://localhost').pathname);
908
1140
  if (pathname === '/_feltdb/config') {
909
1141
  response.setHeader('Content-Type', 'application/json; charset=utf-8');
910
1142
  response.setHeader('Cache-Control', 'no-store');
911
1143
  response.setHeader('Pragma', 'no-cache');
912
- response.end(JSON.stringify({ token: connectUrl ? process.env.VITE_FELTDB_API_KEY || '' : '' }));
1144
+ response.end(JSON.stringify({ token: connectUrl ? process.env.VITE_FELTDB_API_KEY || '' : '', session }));
913
1145
  return;
914
1146
  }
915
1147
  if (pathname === '/_feltdb/project') {
@@ -923,6 +1155,52 @@ async function handleStudio(args) {
923
1155
  response.end(JSON.stringify({ namespace, filename: path.basename(projectFlow), source: fs.readFileSync(projectFlow, 'utf8') }));
924
1156
  return;
925
1157
  }
1158
+ if (pathname === '/_feltdb/runtime') {
1159
+ if (!session.bridgeUrl || !session.sessionId) {
1160
+ response.statusCode = 404;
1161
+ response.end('No development session');
1162
+ return;
1163
+ }
1164
+ try {
1165
+ const upstream = await fetch(`${session.bridgeUrl}/api/v1/development/runtimes?sessionId=${encodeURIComponent(session.sessionId)}`);
1166
+ response.statusCode = upstream.status;
1167
+ response.setHeader('Content-Type', 'application/json; charset=utf-8');
1168
+ response.setHeader('Cache-Control', 'no-store');
1169
+ response.end(await upstream.text());
1170
+ }
1171
+ catch (error) {
1172
+ response.statusCode = 503;
1173
+ response.end(JSON.stringify({ error: String(error) }));
1174
+ }
1175
+ return;
1176
+ }
1177
+ if (pathname === '/_feltdb/observations') {
1178
+ if (!session.bridgeUrl || !session.sessionId) {
1179
+ response.statusCode = 404;
1180
+ response.end('No development session');
1181
+ return;
1182
+ }
1183
+ const query = new URL(request.url || '/', 'http://localhost').searchParams;
1184
+ const runtimeInstanceId = query.get('runtimeInstanceId');
1185
+ if (!runtimeInstanceId) {
1186
+ response.statusCode = 400;
1187
+ response.end('runtimeInstanceId is required');
1188
+ return;
1189
+ }
1190
+ const parameters = new URLSearchParams({ sessionId: session.sessionId, limit: query.get('limit') || '100', cursor: query.get('cursor') || '0' });
1191
+ try {
1192
+ const upstream = await fetch(`${session.bridgeUrl}/api/v1/development/runtime/${encodeURIComponent(runtimeInstanceId)}/observations?${parameters}`);
1193
+ response.statusCode = upstream.status;
1194
+ response.setHeader('Content-Type', 'application/json; charset=utf-8');
1195
+ response.setHeader('Cache-Control', 'no-store');
1196
+ response.end(await upstream.text());
1197
+ }
1198
+ catch (error) {
1199
+ response.statusCode = 503;
1200
+ response.end(JSON.stringify({ error: String(error) }));
1201
+ }
1202
+ return;
1203
+ }
926
1204
  const requested = pathname === '/' ? 'index.html' : pathname.replace(/^\/+/, '');
927
1205
  const candidate = path.resolve(root, requested);
928
1206
  const file = candidate.startsWith(`${path.resolve(root)}${path.sep}`) && fs.existsSync(candidate) && fs.statSync(candidate).isFile() ? candidate : path.join(root, 'index.html');
@@ -936,9 +1214,18 @@ async function handleStudio(args) {
936
1214
  parameters.set('connect', connectUrl);
937
1215
  if (appUrl)
938
1216
  parameters.set('app', appUrl);
1217
+ if (session.workspaceId)
1218
+ parameters.set('workspace', session.workspaceId);
1219
+ if (session.sessionId)
1220
+ parameters.set('session', session.sessionId);
1221
+ if (session.authorityUrl)
1222
+ parameters.set('authority', session.authorityUrl);
1223
+ if (session.pairingUrl)
1224
+ parameters.set('pairing', session.pairingUrl);
939
1225
  const query = `?${parameters.toString()}`;
940
1226
  const studioUrl = `http://127.0.0.1:${port}/${query}`;
941
1227
  console.log(`FeltDB Studio ready at ${studioUrl}`);
1228
+ onReady?.();
942
1229
  console.log('Use Ctrl+C to stop the server.');
943
1230
  if (open) {
944
1231
  const command = process.platform === 'darwin' ? ['open', studioUrl] : process.platform === 'win32' ? ['cmd', '/c', 'start', '', studioUrl] : ['xdg-open', studioUrl];
@@ -964,6 +1251,7 @@ Commands:
964
1251
  connect Connect to remote FeltDB
965
1252
  status Show connection status
966
1253
  dev Start development server
1254
+ investigations List or show durable runtime investigations
967
1255
  build Build FeltDB application
968
1256
  check Validate configuration
969
1257
  inspect Inspect runtime state