@feltdb/core 0.6.10 → 0.6.12
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.
- package/dist/cli/application-lifecycle.js +112 -0
- package/dist/cli/application.js +10 -0
- package/dist/cli/commands.js +234 -64
- package/dist/cli/development-session.js +54 -0
- package/dist/cli/index.js +1 -1
- package/dist/cli/workspace-integration.js +151 -20
- package/dist/create/cli.js +4 -2
- package/dist/create/create.js +42 -6
- package/dist/create/frameworks.js +18 -0
- package/dist/create/package-versions.js +1 -1
- package/dist/create/workspace-initialization.js +4 -5
- package/dist/db.d.ts +4 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +56 -7
- package/dist/development-runtime-bridge.d.ts +60 -0
- package/dist/development-runtime-bridge.d.ts.map +1 -0
- package/dist/development-runtime-bridge.js +171 -0
- package/dist/index-core.d.ts +2 -0
- package/dist/index-core.d.ts.map +1 -1
- package/dist/index-core.js +2 -0
- package/dist/studio/app.d.ts +2 -1
- package/dist/studio/app.d.ts.map +1 -1
- package/dist/studio/components/StateExplorer.d.ts +2 -1
- package/dist/studio/components/StateExplorer.d.ts.map +1 -1
- package/dist/studio/components/index.js +1 -1
- package/dist/studio/{components-9kDSWiGL.js → components-C5p2TfIU.js} +25 -14
- package/dist/studio/index.js +234 -160
- package/dist/studio-app/assets/index-DospFFYE.js +28 -0
- package/dist/studio-app/index.html +1 -1
- package/dist/workspace/browser.d.ts +3 -3
- package/dist/workspace/browser.d.ts.map +1 -1
- package/dist/workspace/browser.js +1 -1
- package/dist/workspace/development-node.d.ts +16 -0
- package/dist/workspace/development-node.d.ts.map +1 -1
- package/dist/workspace/development-node.js +85 -1
- package/dist/workspace/index.d.ts +3 -3
- package/dist/workspace/index.d.ts.map +1 -1
- package/dist/workspace/index.js +1 -1
- package/dist/workspace/investigation-lifecycle-manager.d.ts +2 -0
- package/dist/workspace/investigation-lifecycle-manager.d.ts.map +1 -1
- package/dist/workspace/investigation-lifecycle-manager.js +33 -3
- package/dist/workspace/investigation-supervisor.d.ts.map +1 -1
- package/dist/workspace/investigation-supervisor.js +3 -1
- package/dist/workspace/runtime-investigation.d.ts +4 -0
- package/dist/workspace/runtime-investigation.d.ts.map +1 -1
- package/dist/workspace/runtime-investigation.js +4 -0
- package/dist/workspace/runtime-observation.d.ts +4 -1
- package/dist/workspace/runtime-observation.d.ts.map +1 -1
- package/dist/workspace/runtime-observation.js +20 -2
- package/dist/workspace/runtime-observer.d.ts +6 -0
- package/dist/workspace/runtime-observer.d.ts.map +1 -1
- package/dist/workspace/runtime-observer.js +4 -0
- package/dist/workspace/workspace-connection.d.ts +33 -1
- package/dist/workspace/workspace-connection.d.ts.map +1 -1
- package/dist/workspace/workspace-connection.js +117 -2
- package/dist/workspace/workspace-identity.d.ts +1 -1
- package/dist/workspace/workspace-identity.d.ts.map +1 -1
- package/dist/workspace/workspace-identity.js +4 -4
- package/dist/workspace/workspace-types.d.ts +95 -0
- package/dist/workspace/workspace-types.d.ts.map +1 -1
- package/package.json +1 -1
- 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
|
+
}
|
package/dist/cli/application.js
CHANGED
|
@@ -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,
|
package/dist/cli/commands.js
CHANGED
|
@@ -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
|
-
//
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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
|
-
|
|
624
|
-
|
|
625
|
-
|
|
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
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
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:
|
|
774
|
+
console.log(` Pairing discovery: ${developmentSession.pairingUrl}`);
|
|
692
775
|
}
|
|
693
776
|
console.log();
|
|
694
777
|
console.log('FeltDB Dev Server');
|
|
695
|
-
console.log(` Namespace: ${
|
|
696
|
-
console.log(` Runtime: ${
|
|
697
|
-
console.log(` 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: ${
|
|
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: ' +
|
|
714
|
-
console.log('Studio:
|
|
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
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
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.
|
|
26
|
+
const VERSION = '0.6.12';
|
|
27
27
|
function prompt(question) {
|
|
28
28
|
const rl = readline.createInterface({
|
|
29
29
|
input: process.stdin,
|