@feltdb/core 0.6.10 → 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 (59) 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 +234 -64
  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/create/cli.js +4 -2
  8. package/dist/create/create.js +42 -6
  9. package/dist/create/frameworks.js +18 -0
  10. package/dist/create/package-versions.js +1 -1
  11. package/dist/create/workspace-initialization.js +4 -5
  12. package/dist/db.d.ts +4 -1
  13. package/dist/db.d.ts.map +1 -1
  14. package/dist/db.js +56 -7
  15. package/dist/development-runtime-bridge.d.ts +60 -0
  16. package/dist/development-runtime-bridge.d.ts.map +1 -0
  17. package/dist/development-runtime-bridge.js +171 -0
  18. package/dist/index-core.d.ts +2 -0
  19. package/dist/index-core.d.ts.map +1 -1
  20. package/dist/index-core.js +2 -0
  21. package/dist/studio/app.d.ts +2 -1
  22. package/dist/studio/app.d.ts.map +1 -1
  23. package/dist/studio/components/StateExplorer.d.ts +2 -1
  24. package/dist/studio/components/StateExplorer.d.ts.map +1 -1
  25. package/dist/studio/components/index.js +1 -1
  26. package/dist/studio/{components-9kDSWiGL.js → components-C5p2TfIU.js} +25 -14
  27. package/dist/studio/index.js +234 -160
  28. package/dist/studio-app/assets/index-DospFFYE.js +28 -0
  29. package/dist/studio-app/index.html +1 -1
  30. package/dist/workspace/browser.d.ts +2 -2
  31. package/dist/workspace/browser.d.ts.map +1 -1
  32. package/dist/workspace/browser.js +1 -1
  33. package/dist/workspace/development-node.d.ts +16 -0
  34. package/dist/workspace/development-node.d.ts.map +1 -1
  35. package/dist/workspace/development-node.js +85 -1
  36. package/dist/workspace/index.d.ts +2 -2
  37. package/dist/workspace/index.d.ts.map +1 -1
  38. package/dist/workspace/index.js +1 -1
  39. package/dist/workspace/investigation-lifecycle-manager.d.ts +2 -0
  40. package/dist/workspace/investigation-lifecycle-manager.d.ts.map +1 -1
  41. package/dist/workspace/investigation-lifecycle-manager.js +33 -3
  42. package/dist/workspace/investigation-supervisor.d.ts.map +1 -1
  43. package/dist/workspace/investigation-supervisor.js +3 -1
  44. package/dist/workspace/runtime-investigation.d.ts +4 -0
  45. package/dist/workspace/runtime-investigation.d.ts.map +1 -1
  46. package/dist/workspace/runtime-investigation.js +4 -0
  47. package/dist/workspace/runtime-observation.d.ts +4 -1
  48. package/dist/workspace/runtime-observation.d.ts.map +1 -1
  49. package/dist/workspace/runtime-observation.js +20 -2
  50. package/dist/workspace/runtime-observer.d.ts +6 -0
  51. package/dist/workspace/runtime-observer.d.ts.map +1 -1
  52. package/dist/workspace/runtime-observer.js +4 -0
  53. package/dist/workspace/workspace-identity.d.ts +1 -1
  54. package/dist/workspace/workspace-identity.d.ts.map +1 -1
  55. package/dist/workspace/workspace-identity.js +4 -4
  56. package/dist/workspace/workspace-types.d.ts +95 -0
  57. package/dist/workspace/workspace-types.d.ts.map +1 -1
  58. package/package.json +1 -1
  59. package/dist/studio-app/assets/index-D3rT0SJi.js +0 -28
@@ -0,0 +1,112 @@
1
+ import path from 'path';
2
+ import { spawn } from 'child_process';
3
+ import { applicationIsRunning } from './application.js';
4
+ export function resolveApplicationLifecycle(config) {
5
+ const lifecycle = config.application?.lifecycle;
6
+ if (lifecycle === undefined)
7
+ return 'attached';
8
+ if (lifecycle !== 'managed' && lifecycle !== 'attached') {
9
+ throw new Error(`Invalid application.lifecycle "${lifecycle}"; expected managed or attached`);
10
+ }
11
+ return lifecycle;
12
+ }
13
+ function commandTokens(source) {
14
+ const tokens = [];
15
+ let token = '';
16
+ let quote = '';
17
+ for (let index = 0; index < source.length; index += 1) {
18
+ const character = source[index];
19
+ if (quote) {
20
+ if (character === quote)
21
+ quote = '';
22
+ else if (character === '\\' && quote === '"' && index + 1 < source.length)
23
+ token += source[++index];
24
+ else
25
+ token += character;
26
+ }
27
+ else if (character === '"' || character === "'")
28
+ quote = character;
29
+ else if (/\s/.test(character)) {
30
+ if (token) {
31
+ tokens.push(token);
32
+ token = '';
33
+ }
34
+ }
35
+ else
36
+ token += character;
37
+ }
38
+ if (quote)
39
+ throw new Error(`Cannot run managed application command with an unclosed quote: ${source}`);
40
+ if (token)
41
+ tokens.push(token);
42
+ return tokens;
43
+ }
44
+ export function resolveManagedApplicationCommand(project) {
45
+ const source = project.devScript?.trim();
46
+ if (!source)
47
+ throw new Error('Managed application lifecycle requires package.json.scripts.dev');
48
+ const tokens = commandTokens(source);
49
+ const environment = {};
50
+ while (tokens[0] && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[0])) {
51
+ const assignment = tokens.shift();
52
+ const separator = assignment.indexOf('=');
53
+ environment[assignment.slice(0, separator)] = assignment.slice(separator + 1);
54
+ }
55
+ const executable = tokens.shift();
56
+ if (!executable)
57
+ throw new Error(`Managed application command is empty: ${source}`);
58
+ if (executable === 'feltdb' || (executable === 'npm' && tokens[0] === 'run')) {
59
+ throw new Error(`Refusing recursive managed application command: ${source}`);
60
+ }
61
+ if (/[|&;<>`]/.test(source)) {
62
+ throw new Error(`Managed application command must be directly executable without shell operators: ${source}`);
63
+ }
64
+ return { source, executable, args: tokens, environment };
65
+ }
66
+ export function startManagedApplication(options) {
67
+ const command = resolveManagedApplicationCommand(options.project);
68
+ const localBin = path.join(options.projectDir, 'node_modules', '.bin');
69
+ const inherited = options.environment ?? process.env;
70
+ const child = spawn(command.executable, command.args, {
71
+ cwd: options.projectDir,
72
+ stdio: 'inherit',
73
+ shell: false,
74
+ env: {
75
+ ...inherited,
76
+ ...command.environment,
77
+ PATH: `${localBin}${path.delimiter}${inherited.PATH || ''}`,
78
+ },
79
+ });
80
+ const exited = new Promise((resolve, reject) => {
81
+ child.once('error', reject);
82
+ child.once('exit', (code, signal) => resolve({ code, signal }));
83
+ });
84
+ return {
85
+ command: command.source,
86
+ child,
87
+ exited,
88
+ async stop() {
89
+ if (child.exitCode !== null || child.signalCode !== null)
90
+ return;
91
+ child.kill('SIGTERM');
92
+ await Promise.race([exited.catch(() => undefined), new Promise(resolve => setTimeout(resolve, 2000))]);
93
+ if (child.exitCode === null && child.signalCode === null)
94
+ child.kill('SIGKILL');
95
+ },
96
+ };
97
+ }
98
+ export async function waitForManagedApplication(options) {
99
+ const startedAt = Date.now();
100
+ while (Date.now() - startedAt < options.timeoutMs) {
101
+ if (await applicationIsRunning(options.expectedUrl, Math.min(500, options.timeoutMs)))
102
+ return;
103
+ const exit = await Promise.race([
104
+ options.application.exited,
105
+ new Promise(resolve => setTimeout(() => resolve(null), 100)),
106
+ ]);
107
+ if (exit) {
108
+ throw new Error(`Managed application exited before readiness (command: ${options.application.command}; expected URL: ${options.expectedUrl}; workspace: ${options.workspaceId}; project: ${options.projectDir}; exit: ${exit.code ?? exit.signal})`);
109
+ }
110
+ }
111
+ throw new Error(`Managed application readiness timed out after ${options.timeoutMs}ms (command: ${options.application.command}; expected URL: ${options.expectedUrl}; workspace: ${options.workspaceId}; project: ${options.projectDir})`);
112
+ }
@@ -44,12 +44,22 @@ export function detectApplication(root) {
44
44
  const dependencies = { ...manifest.dependencies, ...manifest.devDependencies };
45
45
  const framework = FRAMEWORKS.find(candidate => candidate.packages.some(name => name in dependencies) || (script ? candidate.pattern.test(script) : false));
46
46
  const feltConfigFile = path.join(root, 'feltdb.config.json');
47
+ const flowFile = path.join(root, 'feltdb.flow');
47
48
  const feltConfig = fs.existsSync(feltConfigFile) ? readJson(feltConfigFile, 'Cannot inspect application: malformed feltdb.config.json') : {};
49
+ const feltDBSignals = [];
50
+ if ('@feltdb/core' in dependencies)
51
+ feltDBSignals.push('dependency');
52
+ if (fs.existsSync(feltConfigFile))
53
+ feltDBSignals.push('config');
54
+ if (fs.existsSync(flowFile))
55
+ feltDBSignals.push('flow');
48
56
  const configuredUrl = typeof feltConfig.appUrl === 'string' ? feltConfig.appUrl
49
57
  : typeof feltConfig.applicationUrl === 'string' ? feltConfig.applicationUrl : undefined;
50
58
  return {
51
59
  packageManager: packageManagerAt(root, typeof manifest.packageManager === 'string' ? manifest.packageManager : undefined),
52
60
  framework: framework?.name || (script ? 'Custom' : 'Unknown'),
61
+ feltDBEnabled: feltDBSignals.length > 0,
62
+ feltDBSignals,
53
63
  devScript: script,
54
64
  configuredPort: scriptPort(script),
55
65
  configuredUrl,
@@ -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 { randomBytes } from 'crypto';
10
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;
@@ -512,6 +515,7 @@ async function handleDev(args) {
512
515
  console.log(' --studio-port <port> FeltDB Studio port (default: 7701)');
513
516
  console.log(' --authority-port <port> Authority port (default: 7700)');
514
517
  console.log(' --discovery-port <port> Pairing discovery port (default: 7799)');
518
+ console.log(' --application-timeout <ms> Managed application readiness timeout (default: 15000)');
515
519
  console.log(' --no-open Do not open Studio in browser');
516
520
  return;
517
521
  }
@@ -542,30 +546,18 @@ async function handleDev(args) {
542
546
  }
543
547
  // Investigation lifecycle manager will be initialized when needed
544
548
  // const investigationManager = new InvestigationLifecycleManager(projectDir);
545
- // Create feltdb.config.json if it doesn't exist
546
- if (!fs.existsSync(configPath)) {
547
- const packageJsonPath = path.join(projectDir, 'package.json');
548
- let namespace = path.basename(projectDir);
549
- if (fs.existsSync(packageJsonPath)) {
550
- try {
551
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
552
- namespace = packageJson.name || namespace;
553
- }
554
- catch {
555
- // Use default namespace if package.json parsing fails
556
- }
557
- }
558
- const defaultConfig = {
559
- namespace,
560
- runtime: 'browser',
561
- storage: 'indexeddb',
562
- distributed: true,
563
- applicationUrl: null,
564
- };
565
- fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2) + '\n');
566
- console.log(`✨ Created feltdb.config.json\n`);
567
- }
568
- const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
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;
569
561
  if (config.runtime === 'managed') {
570
562
  (_a = process.env).VITE_FELTDB_URL || (_a.VITE_FELTDB_URL = process.env.VITE_FELTDB_MANAGED_URL);
571
563
  (_b = process.env).VITE_FELTDB_API_KEY || (_b.VITE_FELTDB_API_KEY = process.env.VITE_FELTDB_MANAGED_API_KEY);
@@ -576,6 +568,7 @@ async function handleDev(args) {
576
568
  const runtimeNamespace = config.runtime === 'managed'
577
569
  ? process.env.VITE_FELTDB_MANAGED_NAMESPACE || config.namespace
578
570
  : config.namespace;
571
+ const applicationLifecycle = resolveApplicationLifecycle(config);
579
572
  // Application URL precedence:
580
573
  // 1. --app-url (highest priority)
581
574
  // 2. feltdb.config.json applicationUrl
@@ -604,7 +597,7 @@ async function handleDev(args) {
604
597
  detectedApplication = detectApplication(projectDir);
605
598
  }
606
599
  catch {
607
- detectedApplication = { packageManager: 'npm', framework: 'External' };
600
+ detectedApplication = { packageManager: 'npm', framework: 'External', feltDBEnabled: false, feltDBSignals: [] };
608
601
  }
609
602
  }
610
603
  else {
@@ -615,16 +608,25 @@ async function handleDev(args) {
615
608
  detectedApplication = detectApplication(projectDir);
616
609
  }
617
610
  catch {
618
- detectedApplication = { packageManager: 'npm', framework: 'External' };
611
+ detectedApplication = { packageManager: 'npm', framework: 'External', feltDBEnabled: false, feltDBSignals: [] };
619
612
  }
620
613
  }
621
614
  else {
622
615
  // Priority 3: Try to discover an existing running application
623
- const discovered = await discoverApplicationUrl(projectDir);
624
- detectedApplication = discovered.project;
625
- appUrl = discovered.url;
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
+ }
626
625
  }
627
626
  }
627
+ if (detectedApplication?.feltDBEnabled) {
628
+ console.log(`Attaching existing FeltDB application (${detectedApplication.feltDBSignals.join(', ')})\n`);
629
+ }
628
630
  const requestedStudioPort = Number(args.includes('--studio-port') ? args[args.indexOf('--studio-port') + 1] || '7701' : '7701');
629
631
  const studioPort = String(await availablePort(requestedStudioPort));
630
632
  if (studioPort !== String(requestedStudioPort))
@@ -632,6 +634,8 @@ async function handleDev(args) {
632
634
  process.env.STUDIO_PORT = studioPort;
633
635
  let selfHostedStarted = false;
634
636
  let localAuthority = null;
637
+ let managedApplication = null;
638
+ let developmentSession = null;
635
639
  const stopSelfHosted = () => {
636
640
  if (selfHostedStarted) {
637
641
  spawnSync('docker', ['compose', 'stop', 'feltdb'], { cwd: process.cwd(), stdio: 'ignore' });
@@ -661,23 +665,102 @@ async function handleDev(args) {
661
665
  });
662
666
  process.env.VITE_FELTDB_URL = localAuthority.endpoint;
663
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');
664
686
  // Generate pairing token for the workspace
665
687
  // This enables browsers and IDEs to discover and connect to the workspace
666
688
  let pairingToken = null;
667
689
  let pairingDiscoveryServer = null;
668
- if (workspace) {
669
- const token = generatePairingToken();
670
- token.workspaceId = workspace.workspaceId;
671
- token.endpoint = process.env.VITE_FELTDB_URL;
672
- token.authorityEndpoint = token.endpoint;
673
- token.namespace = runtimeNamespace || config.namespace;
674
- if (!token.endpoint) {
675
- 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;
712
+ }
713
+ }
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');
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;
676
763
  }
677
- persistPairingToken(process.cwd(), token);
678
- const discoveryPort = Number(args.includes('--discovery-port') ? args[args.indexOf('--discovery-port') + 1] || '7799' : '7799');
679
- pairingDiscoveryServer = await startPairingDiscoveryServer(token, discoveryPort);
680
- pairingToken = token.token;
681
764
  }
682
765
  // Output startup information
683
766
  console.log('✨ Development Workspace');
@@ -688,18 +771,19 @@ async function handleDev(args) {
688
771
  console.log('🔗 Development Workspace');
689
772
  if (pairingToken) {
690
773
  console.log(` Pairing Code: ${pairingToken}`);
691
- console.log(` Pairing discovery: http://127.0.0.1:${(pairingDiscoveryServer?.address()).port}`);
774
+ console.log(` Pairing discovery: ${developmentSession.pairingUrl}`);
692
775
  }
693
776
  console.log();
694
777
  console.log('FeltDB Dev Server');
695
- console.log(` Namespace: ${config.namespace}`);
696
- console.log(` Runtime: ${config.runtime}`);
697
- console.log(` Storage: ${config.storage}`);
778
+ console.log(` Namespace: ${developmentSession.namespace}`);
779
+ console.log(` Runtime: ${developmentSession.runtime}`);
780
+ console.log(` Storage: ${developmentSession.storage}`);
698
781
  console.log(` Distributed: ${config.distributed}`);
782
+ console.log(` Application: ${applicationLifecycle}`);
699
783
  console.log();
700
784
  if (appUrl) {
701
785
  console.log(`Application: ${appUrl}`);
702
- console.log(`Authority: ${process.env.VITE_FELTDB_URL}`);
786
+ console.log(`Authority: ${developmentSession.authorityUrl}`);
703
787
  console.log();
704
788
  console.log(`Using existing application at ${appUrl}`);
705
789
  console.log(`app=${encodeURIComponent(appUrl)}\n`);
@@ -710,23 +794,41 @@ async function handleDev(args) {
710
794
  console.log(' feltdb dev --app-url http://127.0.0.1:<port>\n');
711
795
  console.log('Or set applicationUrl in feltdb.config.json\n');
712
796
  console.log('FeltDB workspace is ready at:');
713
- console.log('Authority: ' + process.env.VITE_FELTDB_URL);
714
- console.log('Studio: http://127.0.0.1:' + studioPort);
797
+ console.log('Authority: ' + developmentSession.authorityUrl);
798
+ console.log('Studio: ' + developmentSession.studioUrl);
715
799
  console.log();
716
800
  }
717
- const stopAll = () => { pairingDiscoveryServer?.close(); void localAuthority?.close(); stopSelfHosted(); };
718
- process.once('exit', stopAll);
719
- process.once('SIGINT', () => { stopAll(); process.exit(130); });
720
- process.once('SIGTERM', () => { stopAll(); process.exit(143); });
721
801
  const open = !args.includes('--no-open');
722
- await handleStudio([
723
- '--port', studioPort,
724
- '--namespace', runtimeNamespace || 'default',
725
- '--runtime', config.runtime || 'browser',
726
- ...(appUrl ? ['--app-url', appUrl] : []),
727
- ...((config.runtime === 'self-hosted' || config.runtime === 'managed') && process.env.VITE_FELTDB_URL ? ['--connect', process.env.VITE_FELTDB_URL] : []),
728
- ...(open ? [] : ['--no-open']),
729
- ]);
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
+ }
730
832
  }
731
833
  function runLocalVite(args, waitForExit) {
732
834
  const executable = process.platform === 'win32' ? 'npm.cmd' : 'npm';
@@ -982,7 +1084,7 @@ async function handleDoctor() {
982
1084
  }
983
1085
  console.log('\n✅ System healthy\n');
984
1086
  }
985
- async function handleStudio(args) {
1087
+ async function handleStudio(args, onReady) {
986
1088
  const connectUrl = args.includes('--connect')
987
1089
  ? args[args.indexOf('--connect') + 1]
988
1090
  : undefined;
@@ -1002,6 +1104,19 @@ async function handleStudio(args) {
1002
1104
  const appUrl = args.includes('--app-url')
1003
1105
  ? args[args.indexOf('--app-url') + 1]
1004
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
+ };
1005
1120
  if (connectUrl) {
1006
1121
  console.log(`Connecting to: ${connectUrl}`);
1007
1122
  console.log(`Remote Studio: http://localhost:${port}\n`);
@@ -1020,13 +1135,13 @@ async function handleStudio(args) {
1020
1135
  .map(value => path.resolve(process.cwd(), value))
1021
1136
  .find((value, index, values) => values.indexOf(value) === index && fs.existsSync(value));
1022
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' };
1023
- const server = http.createServer((request, response) => {
1138
+ const server = http.createServer(async (request, response) => {
1024
1139
  const pathname = decodeURIComponent(new URL(request.url || '/', 'http://localhost').pathname);
1025
1140
  if (pathname === '/_feltdb/config') {
1026
1141
  response.setHeader('Content-Type', 'application/json; charset=utf-8');
1027
1142
  response.setHeader('Cache-Control', 'no-store');
1028
1143
  response.setHeader('Pragma', 'no-cache');
1029
- 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 }));
1030
1145
  return;
1031
1146
  }
1032
1147
  if (pathname === '/_feltdb/project') {
@@ -1040,6 +1155,52 @@ async function handleStudio(args) {
1040
1155
  response.end(JSON.stringify({ namespace, filename: path.basename(projectFlow), source: fs.readFileSync(projectFlow, 'utf8') }));
1041
1156
  return;
1042
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
+ }
1043
1204
  const requested = pathname === '/' ? 'index.html' : pathname.replace(/^\/+/, '');
1044
1205
  const candidate = path.resolve(root, requested);
1045
1206
  const file = candidate.startsWith(`${path.resolve(root)}${path.sep}`) && fs.existsSync(candidate) && fs.statSync(candidate).isFile() ? candidate : path.join(root, 'index.html');
@@ -1053,9 +1214,18 @@ async function handleStudio(args) {
1053
1214
  parameters.set('connect', connectUrl);
1054
1215
  if (appUrl)
1055
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);
1056
1225
  const query = `?${parameters.toString()}`;
1057
1226
  const studioUrl = `http://127.0.0.1:${port}/${query}`;
1058
1227
  console.log(`FeltDB Studio ready at ${studioUrl}`);
1228
+ onReady?.();
1059
1229
  console.log('Use Ctrl+C to stop the server.');
1060
1230
  if (open) {
1061
1231
  const command = process.platform === 'darwin' ? ['open', studioUrl] : process.platform === 'win32' ? ['cmd', '/c', 'start', '', studioUrl] : ['xdg-open', studioUrl];
@@ -0,0 +1,54 @@
1
+ const TRANSITIONS = {
2
+ CREATING: ['STARTING', 'FAILED'],
3
+ STARTING: ['READY', 'STOPPING', 'FAILED'],
4
+ READY: ['RUNNING', 'STOPPING', 'FAILED'],
5
+ RUNNING: ['STOPPING', 'FAILED'],
6
+ STOPPING: ['STOPPED'],
7
+ STOPPED: [],
8
+ FAILED: ['STOPPING', 'STOPPED'],
9
+ };
10
+ export function createDevelopmentSession(input) {
11
+ for (const key of ['sessionId', 'workspaceId', 'projectId', 'namespace', 'runtime', 'storage', 'authorityUrl', 'studioUrl', 'pairingUrl']) {
12
+ if (!input[key])
13
+ throw new Error(`Cannot create development session without ${key}`);
14
+ }
15
+ return { ...input, state: 'CREATING' };
16
+ }
17
+ export function transitionDevelopmentSession(session, state) {
18
+ if (session.state === state)
19
+ return;
20
+ if (!TRANSITIONS[session.state].includes(state))
21
+ throw new Error(`Invalid development session transition ${session.state} -> ${state}`);
22
+ session.state = state;
23
+ }
24
+ export function developmentSessionEnvironment(session) {
25
+ return {
26
+ FELTDB_WORKSPACE_ID: session.workspaceId,
27
+ FELTDB_DEV_SESSION_ID: session.sessionId,
28
+ FELTDB_DEV_BRIDGE_URL: session.pairingUrl,
29
+ FELTDB_RUNTIME: session.runtime,
30
+ FELTDB_AUTHORITY_URL: session.authorityUrl,
31
+ FELTDB_APPLICATION_URL: session.applicationUrl,
32
+ FELTDB_WORKSPACE_ENDPOINT: session.authorityUrl,
33
+ FELTDB_NAMESPACE: session.namespace,
34
+ VITE_FELTDB_WORKSPACE_ID: session.workspaceId,
35
+ VITE_FELTDB_DEV_SESSION_ID: session.sessionId,
36
+ VITE_FELTDB_DEV_BRIDGE_URL: session.pairingUrl,
37
+ VITE_FELTDB_RUNTIME: session.runtime,
38
+ VITE_FELTDB_AUTHORITY_URL: session.authorityUrl,
39
+ VITE_FELTDB_APPLICATION_URL: session.applicationUrl,
40
+ VITE_FELTDB_WORKSPACE_ENDPOINT: session.authorityUrl,
41
+ VITE_FELTDB_NAMESPACE: session.namespace,
42
+ };
43
+ }
44
+ export function developmentSessionSummary(session) {
45
+ const rows = [
46
+ ['Session', session.sessionId], ['Workspace', session.workspaceId], ['Project', session.projectId], ['Namespace', session.namespace],
47
+ ['Runtime', session.runtime], ['Storage', session.storage], ['Lifecycle', session.lifecycle],
48
+ ['Application', session.applicationUrl], ['Authority', session.authorityUrl],
49
+ ['Studio', session.studioUrl], ['Pairing', session.pairingUrl],
50
+ ].filter((row) => Boolean(row[1]));
51
+ return ['FeltDB Development Session', ...rows.map(([label, value]) => `${label.padEnd(12)} ${value}`),
52
+ '✓ Authority', '✓ Pairing', '✓ Studio', ...(session.applicationUrl ? ['✓ Application'] : []),
53
+ 'FeltDB development environment ready.'].join('\n');
54
+ }
package/dist/cli/index.js CHANGED
@@ -23,7 +23,7 @@ import * as path from 'path';
23
23
  import * as readline from 'readline';
24
24
  import { getClient } from './api-client.js';
25
25
  import { loadFeltDBConfig, createDefaultConfig, validateModel, } from './config.js';
26
- const VERSION = '0.6.10';
26
+ const VERSION = '0.6.11';
27
27
  function prompt(question) {
28
28
  const rl = readline.createInterface({
29
29
  input: process.stdin,